mirror of
https://codeberg.org/StreamGraph/StreamGraph.git
synced 2024-11-13 19:49:55 +01:00
Add OBS and Twitch nodes. Improve UX significantly. Rework groups from the ground up with a new instancing feature. Open to the public. (#18)
After months of work and over a hundred commits on this repo alone (not to mention the old, half-working repos on GitHub), StreamGraph is finally ready to be shown to the public, even if in an incomplete state. This PR is a culmination of numerous design discussions, re-writes, and hours spent by both @Eroax and myself. Reviewed-on: https://codeberg.org/Eroax/StreamGraph/pulls/18 Co-authored-by: Lera Elvoé <yagich@poto.cafe> Co-committed-by: Lera Elvoé <yagich@poto.cafe>
This commit is contained in:
parent
dc7d53e3d2
commit
b55a462945
98 changed files with 12010 additions and 461 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,5 @@
|
|||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
|
||||
# distribution folder
|
||||
dist/*/
|
||||
|
|
674
COPYING
Normal file
674
COPYING
Normal file
|
@ -0,0 +1,674 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
17
README.md
Normal file
17
README.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# StreamGraph is a node graph-based virtual stream deck and livestream automation tool.
|
||||
## ⚠️ StreamGraph is at an EARLY ALPHA stage! Some things will break! ⚠️
|
||||
StreamGraph lets you automate your livestream with a powerful and easy to understand node graph workflow.
|
||||
|
||||
![StreamGraph screenshot](img/example1.png)
|
||||
|
||||
It can connect to Twitch and OBS, allowing you to take stream interactivity to the next level.
|
||||
|
||||
![StreamGraph screenshot](img/example2.png)
|
||||
|
||||
## Current state and capabilities
|
||||
**StreamGraph is at a very early stage of development**. We cannot guarantee stability at this point and things may change dramatically between releases. As development progresses, the API and nodes will stabilize. If you encounter any issues, please report them on [the issue tracker](https://codeberg.org/Eroax/StreamGraph/issues) with as much detail about how to reproduce them as possible.
|
||||
|
||||
The app can currently connect to Twitch and read and send chats in a single channel.
|
||||
It can also connect to a single OBS instance (utilizing OBS-WebSocket, which is bundled with modern versions of OBS).
|
||||
|
||||
For more information about nodes that currently exist, [check out the wiki.](https://codeberg.org/Eroax/StreamGraph/wiki)
|
120
THIRDPARTY.md
Normal file
120
THIRDPARTY.md
Normal file
|
@ -0,0 +1,120 @@
|
|||
# Third-party licenses
|
||||
|
||||
StreamGraph uses third-party code. Their licenses and attribution are reproduced below.
|
||||
|
||||
# Engine
|
||||
## Godot Engine
|
||||
Upstream: [GitHub](https://github.com/godotengine/godot)
|
||||
License: MIT
|
||||
### License text
|
||||
```
|
||||
Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md).
|
||||
Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
# Libraries
|
||||
## godot-uuid
|
||||
Upstream: [GitHub](https://github.com/binogure-studio/godot-uuid)
|
||||
License: MIT
|
||||
File(s): `/classes/UUID.gd`
|
||||
### License text
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Xavier Sellier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
## NoOBSWS
|
||||
Upstream: [GitHub](https://github.com/yagich/no-obs-ws)
|
||||
License: BSD 2-Clause
|
||||
File(s): `/addons/no-obs-ws/*`
|
||||
### License text
|
||||
```
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2023, Yagich
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
## NoTwitch
|
||||
Upstream: [Codeberg](https://codeberg.org/Eroax/NoTwitch)
|
||||
License: MIT
|
||||
File(s): `/addons/no_twitch/*`
|
||||
### License text
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Eroax
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
16
addons/no-obs-ws/Authenticator.gd
Normal file
16
addons/no-obs-ws/Authenticator.gd
Normal file
|
@ -0,0 +1,16 @@
|
|||
extends RefCounted
|
||||
|
||||
# The result of the authentication string creation process. Use getter for public access.
|
||||
var _auth_string: String:
|
||||
get = get_auth_string
|
||||
|
||||
|
||||
func _init(password: String, challenge: String, salt: String) -> void:
|
||||
var salted := password + salt
|
||||
var base64_secret := Marshalls.raw_to_base64(salted.sha256_buffer())
|
||||
var b64_secret_plus_challenge = base64_secret + challenge
|
||||
_auth_string = Marshalls.raw_to_base64(b64_secret_plus_challenge.sha256_buffer())
|
||||
|
||||
|
||||
func get_auth_string() -> String:
|
||||
return _auth_string
|
312
addons/no-obs-ws/NoOBSWS.gd
Normal file
312
addons/no-obs-ws/NoOBSWS.gd
Normal file
|
@ -0,0 +1,312 @@
|
|||
extends Node
|
||||
class_name NoOBSWS
|
||||
|
||||
const Authenticator := preload("res://addons/no-obs-ws/Authenticator.gd")
|
||||
const Enums := preload("res://addons/no-obs-ws/Utility/Enums.gd")
|
||||
|
||||
var _ws: WebSocketPeer
|
||||
# {request_id: RequestResponse}
|
||||
var _requests: Dictionary = {}
|
||||
var _batch_requests: Dictionary = {}
|
||||
|
||||
const WS_URL := "127.0.0.1:%s"
|
||||
|
||||
signal connection_ready()
|
||||
signal connection_failed()
|
||||
signal connection_closed_clean(code: int, reason: String)
|
||||
|
||||
signal error(message: String)
|
||||
|
||||
signal event_received(event: Message)
|
||||
|
||||
signal _auth_required()
|
||||
|
||||
|
||||
func connect_to_obsws(port: int, password: String = "") -> void:
|
||||
_ws = WebSocketPeer.new()
|
||||
_ws.connect_to_url(WS_URL % port)
|
||||
_auth_required.connect(_authenticate.bind(password))
|
||||
|
||||
|
||||
func make_generic_request(request_type: String, request_data: Dictionary = {}) -> RequestResponse:
|
||||
var response := RequestResponse.new()
|
||||
var message := Message.new()
|
||||
|
||||
var crypto := Crypto.new()
|
||||
var request_id := crypto.generate_random_bytes(16).hex_encode()
|
||||
|
||||
var data := {
|
||||
"request_type": request_type,
|
||||
"request_id": request_id,
|
||||
"request_data": request_data,
|
||||
}
|
||||
message._d.merge(data, true)
|
||||
|
||||
message.op_code = Enums.WebSocketOpCode.REQUEST
|
||||
|
||||
response.id = request_id
|
||||
response.type = request_type
|
||||
|
||||
_requests[request_id] = response
|
||||
|
||||
_send_message(message)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
func make_batch_request(halt_on_failure: bool = false, execution_type: Enums.RequestBatchExecutionType = Enums.RequestBatchExecutionType.SERIAL_REALTIME) -> BatchRequest:
|
||||
var batch_request := BatchRequest.new()
|
||||
|
||||
var crypto := Crypto.new()
|
||||
var request_id := crypto.generate_random_bytes(16).hex_encode()
|
||||
|
||||
batch_request._id = request_id
|
||||
batch_request._send_callback = _send_message
|
||||
|
||||
batch_request.halt_on_failure = halt_on_failure
|
||||
batch_request.execution_type = execution_type
|
||||
|
||||
_batch_requests[request_id] = batch_request
|
||||
|
||||
return batch_request
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if is_instance_valid(_ws):
|
||||
_poll_socket()
|
||||
|
||||
|
||||
func _poll_socket() -> void:
|
||||
_ws.poll()
|
||||
|
||||
var state = _ws.get_ready_state()
|
||||
match state:
|
||||
WebSocketPeer.STATE_OPEN:
|
||||
while _ws.get_available_packet_count():
|
||||
_handle_packet(_ws.get_packet())
|
||||
WebSocketPeer.STATE_CLOSING:
|
||||
pass
|
||||
WebSocketPeer.STATE_CLOSED:
|
||||
if _ws.get_close_code() == -1:
|
||||
connection_failed.emit()
|
||||
else:
|
||||
connection_closed_clean.emit(_ws.get_close_code(), _ws.get_close_reason())
|
||||
_ws = null
|
||||
|
||||
|
||||
func _handle_packet(packet: PackedByteArray) -> void:
|
||||
var message = Message.from_json(packet.get_string_from_utf8())
|
||||
#print("got message with code ", message.op_code)
|
||||
_handle_message(message)
|
||||
|
||||
|
||||
func _handle_message(message: Message) -> void:
|
||||
# print(message)
|
||||
match message.op_code:
|
||||
Enums.WebSocketOpCode.HELLO:
|
||||
if message.get("authentication") != null:
|
||||
_auth_required.emit(message)
|
||||
else:
|
||||
var m = Message.new()
|
||||
m.op_code = Enums.WebSocketOpCode.IDENTIFY
|
||||
_send_message(m)
|
||||
|
||||
Enums.WebSocketOpCode.IDENTIFIED:
|
||||
connection_ready.emit()
|
||||
|
||||
Enums.WebSocketOpCode.EVENT:
|
||||
event_received.emit(message)
|
||||
|
||||
Enums.WebSocketOpCode.REQUEST_RESPONSE:
|
||||
#print("Req Response")
|
||||
var id = message.get_data().get("request_id")
|
||||
if id == null:
|
||||
error.emit("Received request response, but there was no request id field.")
|
||||
return
|
||||
|
||||
var response = _requests.get(id) as RequestResponse
|
||||
if response == null:
|
||||
error.emit("Received request response, but there was no request made with that id.")
|
||||
return
|
||||
|
||||
response.message = message
|
||||
|
||||
response.response_received.emit()
|
||||
_requests.erase(id)
|
||||
|
||||
Enums.WebSocketOpCode.REQUEST_BATCH_RESPONSE:
|
||||
var id = message.get_data().get("request_id")
|
||||
if id == null:
|
||||
error.emit("Received batch request response, but there was no request id field.")
|
||||
return
|
||||
|
||||
var response = _batch_requests.get(id) as BatchRequest
|
||||
if response == null:
|
||||
error.emit("Received batch request response, but there was no request made with that id.")
|
||||
return
|
||||
|
||||
response.response = message
|
||||
|
||||
response.response_received.emit()
|
||||
_batch_requests.erase(id)
|
||||
|
||||
|
||||
func _send_message(message: Message) -> void:
|
||||
_ws.send_text(message.to_obsws_json())
|
||||
|
||||
|
||||
func _authenticate(message: Message, password: String) -> void:
|
||||
var authenticator = Authenticator.new(
|
||||
password,
|
||||
message.authentication.challenge,
|
||||
message.authentication.salt,
|
||||
)
|
||||
var auth_string = authenticator.get_auth_string()
|
||||
var m = Message.new()
|
||||
m.op_code = Enums.WebSocketOpCode.IDENTIFY
|
||||
m._d["authentication"] = auth_string
|
||||
#print("MY RESPONSE: ")
|
||||
#print(m)
|
||||
_send_message(m)
|
||||
|
||||
|
||||
class Message:
|
||||
var op_code: int
|
||||
var _d: Dictionary = {"rpc_version": 1}
|
||||
|
||||
func _get(property: StringName):
|
||||
if property in _d:
|
||||
return _d[property]
|
||||
else:
|
||||
return null
|
||||
|
||||
|
||||
func _get_property_list() -> Array:
|
||||
var prop_list = []
|
||||
_d.keys().map(
|
||||
func(x):
|
||||
var d = {
|
||||
"name": x,
|
||||
"type": typeof(_d[x])
|
||||
}
|
||||
prop_list.append(d)
|
||||
)
|
||||
return prop_list
|
||||
|
||||
|
||||
func to_obsws_json() -> String:
|
||||
var data = {
|
||||
"op": op_code,
|
||||
"d": {}
|
||||
}
|
||||
|
||||
data.d = snake_to_camel_recursive(_d)
|
||||
|
||||
return JSON.stringify(data)
|
||||
|
||||
|
||||
func get_data() -> Dictionary:
|
||||
return _d
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return var_to_str(_d)
|
||||
|
||||
|
||||
static func from_json(json: String) -> Message:
|
||||
var ev = Message.new()
|
||||
var dictified = JSON.parse_string(json)
|
||||
|
||||
if dictified == null:
|
||||
return null
|
||||
|
||||
dictified = dictified as Dictionary
|
||||
ev.op_code = dictified.get("op", -1)
|
||||
var data = dictified.get("d", null)
|
||||
if data == null:
|
||||
return null
|
||||
|
||||
data = data as Dictionary
|
||||
ev._d = camel_to_snake_recursive(data)
|
||||
|
||||
return ev
|
||||
|
||||
|
||||
static func camel_to_snake_recursive(d: Dictionary) -> Dictionary:
|
||||
var snaked = {}
|
||||
for prop in d:
|
||||
prop = prop as String
|
||||
if d[prop] is Dictionary:
|
||||
snaked[prop.to_snake_case()] = camel_to_snake_recursive(d[prop])
|
||||
else:
|
||||
snaked[prop.to_snake_case()] = d[prop]
|
||||
return snaked
|
||||
|
||||
|
||||
static func snake_to_camel_recursive(d: Dictionary) -> Dictionary:
|
||||
var cameled = {}
|
||||
for prop in d:
|
||||
prop = prop as String
|
||||
if d[prop] is Dictionary:
|
||||
cameled[prop.to_camel_case()] = snake_to_camel_recursive(d[prop])
|
||||
else:
|
||||
cameled[prop.to_camel_case()] = d[prop]
|
||||
return cameled
|
||||
|
||||
|
||||
class RequestResponse:
|
||||
signal response_received()
|
||||
|
||||
var id: String
|
||||
var type: String
|
||||
var message: Message
|
||||
|
||||
|
||||
class BatchRequest:
|
||||
signal response_received()
|
||||
|
||||
var _id: String
|
||||
var _send_callback: Callable
|
||||
|
||||
var halt_on_failure: bool = false
|
||||
var execution_type: Enums.RequestBatchExecutionType = Enums.RequestBatchExecutionType.SERIAL_REALTIME
|
||||
|
||||
var requests: Array[Message]
|
||||
# {String: int}
|
||||
var request_ids: Dictionary
|
||||
|
||||
var response: Message = null
|
||||
|
||||
func send() -> void:
|
||||
var message = Message.new()
|
||||
message.op_code = Enums.WebSocketOpCode.REQUEST_BATCH
|
||||
message._d["halt_on_failure"] = halt_on_failure
|
||||
message._d["execution_type"] = execution_type
|
||||
message._d["request_id"] = _id
|
||||
message._d["requests"] = []
|
||||
for r in requests:
|
||||
message._d.requests.append(Message.snake_to_camel_recursive(r.get_data()))
|
||||
|
||||
_send_callback.call(message)
|
||||
|
||||
|
||||
func add_request(request_type: String, request_id: String = "", request_data: Dictionary = {}) -> int:
|
||||
var message = Message.new()
|
||||
|
||||
if request_id == "":
|
||||
var crypto := Crypto.new()
|
||||
request_id = crypto.generate_random_bytes(16).hex_encode()
|
||||
|
||||
var data := {
|
||||
"request_type": request_type,
|
||||
"request_id": request_id,
|
||||
"request_data": request_data,
|
||||
}
|
||||
|
||||
message._d.merge(data, true)
|
||||
message.op_code = Enums.WebSocketOpCode.REQUEST
|
||||
|
||||
requests.append(message)
|
||||
request_ids[request_id] = requests.size() - 1
|
||||
|
||||
return request_ids[request_id]
|
64
addons/no-obs-ws/Utility/EnumGen.gd
Normal file
64
addons/no-obs-ws/Utility/EnumGen.gd
Normal file
|
@ -0,0 +1,64 @@
|
|||
static func generate_enums(protocol_json_path: String, output_to_path: String) -> void:
|
||||
var protocol := FileAccess.open(protocol_json_path, FileAccess.READ).get_as_text()
|
||||
var protocol_json: Dictionary = JSON.parse_string(protocol)
|
||||
var res := "# This file is automatically generated, please do not change it. If you wish to edit it, check /addons/deckobsws/Utility/EnumGen.gd\n\n"
|
||||
for e in protocol_json.enums:
|
||||
# if all are deprecated, don't make the enum
|
||||
var deprecated_count: int
|
||||
for enumlet in e.enumIdentifiers:
|
||||
if enumlet.deprecated:
|
||||
deprecated_count += 1
|
||||
|
||||
if deprecated_count == e.enumIdentifiers.size():
|
||||
continue
|
||||
|
||||
res += "enum %s {\n" % e.enumType
|
||||
|
||||
for enumlet in e.enumIdentifiers:
|
||||
var enumlet_value: int
|
||||
|
||||
match typeof(enumlet.enumValue):
|
||||
TYPE_FLOAT:
|
||||
enumlet_value = int(enumlet.enumValue)
|
||||
TYPE_STRING when "<<" in enumlet.enumValue:
|
||||
enumlet_value = tokenize_lbitshift(enumlet.enumValue)
|
||||
TYPE_STRING when "|" in enumlet.enumValue:
|
||||
var token: String = (enumlet.enumValue as String)\
|
||||
.trim_prefix("(")\
|
||||
.trim_suffix(")")
|
||||
var split := Array(token.split("|")).map(
|
||||
func(x: String):
|
||||
return x.strip_edges()
|
||||
)
|
||||
var calc: int
|
||||
for enum_partial in e.enumIdentifiers:
|
||||
if enum_partial.enumIdentifier not in split:
|
||||
continue
|
||||
|
||||
|
||||
calc |= tokenize_lbitshift(enum_partial.enumValue)
|
||||
enumlet_value = calc
|
||||
TYPE_STRING:
|
||||
enumlet_value = int(enumlet.enumValue)
|
||||
|
||||
res += "\t%s = %s,\n" % [
|
||||
(enumlet.enumIdentifier as String).to_snake_case().to_upper(),
|
||||
enumlet_value
|
||||
]
|
||||
|
||||
res += "}\n\n\n"
|
||||
|
||||
var result_file := FileAccess.open(output_to_path, FileAccess.WRITE)
|
||||
result_file.store_string(res.strip_edges() + "\n")
|
||||
|
||||
|
||||
static func tokenize_lbitshift(s: String) -> int:
|
||||
var tokens := Array(s\
|
||||
.trim_prefix("(")\
|
||||
.trim_suffix(")")\
|
||||
.split("<<")).map(
|
||||
func(x: String):
|
||||
return int(x)
|
||||
)
|
||||
|
||||
return tokens[0] << tokens [1]
|
96
addons/no-obs-ws/Utility/Enums.gd
Normal file
96
addons/no-obs-ws/Utility/Enums.gd
Normal file
|
@ -0,0 +1,96 @@
|
|||
# This file is automatically generated, please do not change it. If you wish to edit it, check /addons/deckobsws/Utility/EnumGen.gd
|
||||
|
||||
enum EventSubscription {
|
||||
NONE = 0,
|
||||
GENERAL = 1,
|
||||
CONFIG = 2,
|
||||
SCENES = 4,
|
||||
INPUTS = 8,
|
||||
TRANSITIONS = 16,
|
||||
FILTERS = 32,
|
||||
OUTPUTS = 64,
|
||||
SCENE_ITEMS = 128,
|
||||
MEDIA_INPUTS = 256,
|
||||
VENDORS = 512,
|
||||
UI = 1024,
|
||||
ALL = 2047,
|
||||
INPUT_VOLUME_METERS = 65536,
|
||||
INPUT_ACTIVE_STATE_CHANGED = 131072,
|
||||
INPUT_SHOW_STATE_CHANGED = 262144,
|
||||
SCENE_ITEM_TRANSFORM_CHANGED = 524288,
|
||||
}
|
||||
|
||||
|
||||
enum RequestBatchExecutionType {
|
||||
NONE = -1,
|
||||
SERIAL_REALTIME = 0,
|
||||
SERIAL_FRAME = 1,
|
||||
PARALLEL = 2,
|
||||
}
|
||||
|
||||
|
||||
enum RequestStatus {
|
||||
UNKNOWN = 0,
|
||||
NO_ERROR = 10,
|
||||
SUCCESS = 100,
|
||||
MISSING_REQUEST_TYPE = 203,
|
||||
UNKNOWN_REQUEST_TYPE = 204,
|
||||
GENERIC_ERROR = 205,
|
||||
UNSUPPORTED_REQUEST_BATCH_EXECUTION_TYPE = 206,
|
||||
MISSING_REQUEST_FIELD = 300,
|
||||
MISSING_REQUEST_DATA = 301,
|
||||
INVALID_REQUEST_FIELD = 400,
|
||||
INVALID_REQUEST_FIELD_TYPE = 401,
|
||||
REQUEST_FIELD_OUT_OF_RANGE = 402,
|
||||
REQUEST_FIELD_EMPTY = 403,
|
||||
TOO_MANY_REQUEST_FIELDS = 404,
|
||||
OUTPUT_RUNNING = 500,
|
||||
OUTPUT_NOT_RUNNING = 501,
|
||||
OUTPUT_PAUSED = 502,
|
||||
OUTPUT_NOT_PAUSED = 503,
|
||||
OUTPUT_DISABLED = 504,
|
||||
STUDIO_MODE_ACTIVE = 505,
|
||||
STUDIO_MODE_NOT_ACTIVE = 506,
|
||||
RESOURCE_NOT_FOUND = 600,
|
||||
RESOURCE_ALREADY_EXISTS = 601,
|
||||
INVALID_RESOURCE_TYPE = 602,
|
||||
NOT_ENOUGH_RESOURCES = 603,
|
||||
INVALID_RESOURCE_STATE = 604,
|
||||
INVALID_INPUT_KIND = 605,
|
||||
RESOURCE_NOT_CONFIGURABLE = 606,
|
||||
INVALID_FILTER_KIND = 607,
|
||||
RESOURCE_CREATION_FAILED = 700,
|
||||
RESOURCE_ACTION_FAILED = 701,
|
||||
REQUEST_PROCESSING_FAILED = 702,
|
||||
CANNOT_ACT = 703,
|
||||
}
|
||||
|
||||
|
||||
enum WebSocketCloseCode {
|
||||
DONT_CLOSE = 0,
|
||||
UNKNOWN_REASON = 4000,
|
||||
MESSAGE_DECODE_ERROR = 4002,
|
||||
MISSING_DATA_FIELD = 4003,
|
||||
INVALID_DATA_FIELD_TYPE = 4004,
|
||||
INVALID_DATA_FIELD_VALUE = 4005,
|
||||
UNKNOWN_OP_CODE = 4006,
|
||||
NOT_IDENTIFIED = 4007,
|
||||
ALREADY_IDENTIFIED = 4008,
|
||||
AUTHENTICATION_FAILED = 4009,
|
||||
UNSUPPORTED_RPC_VERSION = 4010,
|
||||
SESSION_INVALIDATED = 4011,
|
||||
UNSUPPORTED_FEATURE = 4012,
|
||||
}
|
||||
|
||||
|
||||
enum WebSocketOpCode {
|
||||
HELLO = 0,
|
||||
IDENTIFY = 1,
|
||||
IDENTIFIED = 2,
|
||||
REIDENTIFY = 3,
|
||||
EVENT = 5,
|
||||
REQUEST = 6,
|
||||
REQUEST_RESPONSE = 7,
|
||||
REQUEST_BATCH = 8,
|
||||
REQUEST_BATCH_RESPONSE = 9,
|
||||
}
|
5786
addons/no-obs-ws/Utility/protocol.json
Normal file
5786
addons/no-obs-ws/Utility/protocol.json
Normal file
File diff suppressed because it is too large
Load diff
7
addons/no-obs-ws/plugin.cfg
Normal file
7
addons/no-obs-ws/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="no-obs-ws"
|
||||
description="An obs-websocket client and translation layer for GDScript"
|
||||
author="Yagich"
|
||||
version="0.1"
|
||||
script="plugin.gd"
|
12
addons/no-obs-ws/plugin.gd
Normal file
12
addons/no-obs-ws/plugin.gd
Normal file
|
@ -0,0 +1,12 @@
|
|||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
# Initialization of the plugin goes here.
|
||||
pass
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
# Clean-up of the plugin goes here.
|
||||
pass
|
166
addons/no_twitch/chat_socket.gd
Normal file
166
addons/no_twitch/chat_socket.gd
Normal file
|
@ -0,0 +1,166 @@
|
|||
extends "res://addons/no_twitch/websocket_client.gd"
|
||||
|
||||
## Wrapper class around [Websocket_Client] that handles Twitch Chat
|
||||
|
||||
signal chat_received
|
||||
signal chat_received_raw
|
||||
signal chat_received_rich
|
||||
|
||||
signal chat_connected
|
||||
|
||||
var channels : Array[String]
|
||||
var extra_info : bool
|
||||
|
||||
var user_regex := RegEx.new()
|
||||
var user_pattern := r":([\w]+)!"
|
||||
|
||||
|
||||
func _init():
|
||||
|
||||
packet_received.connect(data_received)
|
||||
|
||||
user_regex.compile(user_pattern)
|
||||
|
||||
|
||||
## Connects to the Twitch IRC server.
|
||||
func connect_to_chat(token, extra = false, nick = "terribletwitch"):
|
||||
|
||||
extra_info = extra
|
||||
|
||||
connect_to_url("wss://irc-ws.chat.twitch.tv:443")
|
||||
await socket_open
|
||||
send_text("PASS oauth:" + token)
|
||||
send_text("NICK " + nick)
|
||||
|
||||
if extra:
|
||||
|
||||
send_text("CAP REQ :twitch.tv/commands twitch.tv/tags")
|
||||
|
||||
|
||||
|
||||
## Handles checking the received packet from [signal Websocket_Client.packet_received]
|
||||
func data_received(packet : PackedByteArray):
|
||||
|
||||
# Gets the text from the packet, strips the end and splits the different lines
|
||||
var messages = packet.get_string_from_utf8().strip_edges(false).split("\r\n")
|
||||
|
||||
for msg in messages:
|
||||
# Checks if this is a message that has tags enabled and if so, parses out the tags.
|
||||
var tags : Dictionary
|
||||
if msg.begins_with("@"):
|
||||
|
||||
# Grabs the actual end of the string with the message in it.
|
||||
var real_msg = msg.split(" ", false, 1)
|
||||
msg = real_msg[1]
|
||||
|
||||
# Loops through all the tags splitting them up by ; and then by = to get the keys and values.
|
||||
for tag in real_msg[0].split(";"):
|
||||
|
||||
var key_value = tag.split("=")
|
||||
tags[key_value[0]] = key_value[1]
|
||||
|
||||
|
||||
|
||||
parse_chat_msg(msg, tags)
|
||||
|
||||
|
||||
#@badge-info=subscriber/34;badges=broadcaster/1,subscriber/6,game-developer/1;client-nonce=b5009ae3ee034a7706d86fe221882925;color=#2E8B57;display-name=EroAxee;emotes=;first-msg=0;flags=;id=be05dae8-4067-4edf-83f2-e6be02974904;mod=0;returning-chatter=0;room-id=160349129;subscriber=1;tmi-sent-ts=1702009303249;turbo=0;user-id=160349129;user-type= :eroaxee!eroaxee@eroaxee.tmi.twitch.tv PRIVMSG #eroaxee :More
|
||||
|
||||
## Parses the given [param msg] [String]
|
||||
func parse_chat_msg(msg : String, tags : Dictionary):
|
||||
|
||||
var msg_dict : Dictionary
|
||||
|
||||
if msg == "PING :tmi.twitch.tv":
|
||||
|
||||
send_text(msg.replace("PING", "PONG"))
|
||||
|
||||
return
|
||||
|
||||
|
||||
var msg_notice = msg.split(" ")[1]
|
||||
match msg_notice:
|
||||
|
||||
"PRIVMSG":
|
||||
|
||||
var space_split = msg.split(" ", true, 3)
|
||||
|
||||
msg_dict["username"] = user_regex.search(msg).get_string(1)
|
||||
msg_dict["message"] = space_split[3].trim_prefix(":")
|
||||
msg_dict["channel"] = space_split[2].trim_prefix("#")
|
||||
msg_dict.merge(parse_tags(tags))
|
||||
prints(msg_dict.username, msg_dict.message, msg_dict.channel)
|
||||
|
||||
#(__username_regex.search(split[0]).get_string(1), split[3].right(1), split[2], tags)
|
||||
|
||||
if !tags.is_empty():
|
||||
|
||||
chat_received_rich.emit(msg_dict)
|
||||
return
|
||||
|
||||
chat_received.emit(msg_dict)
|
||||
|
||||
|
||||
# Connection Message
|
||||
"001":
|
||||
|
||||
prints("Connection Established", msg)
|
||||
chat_connected.emit()
|
||||
|
||||
# Chat Joining Message
|
||||
"JOIN":
|
||||
|
||||
pass
|
||||
|
||||
# Chat Leaving Message
|
||||
"PART":
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
#@badge-info=subscriber/34;badges=broadcaster/1,subscriber/6,game-developer/1;client-nonce=02d73777ab1fab1aee33ada1830d52b5;color=#2E8B57;display-name=EroAxee;emotes=;first-msg=0;flags=;id=4ff91a8c-b965-43f8-85a1-ddd541a2b438;mod=0;returning-chatter=0;room-id=160349129;subscriber=1;tmi-sent-ts=1701850826667;turbo=0;user-id=160349129;user-type= :eroaxee!eroaxee@eroaxee.tmi.twitch.tv PRIVMSG #eroaxee :Stuff
|
||||
|
||||
## Utility function that takes a Dictionary of tags from a Twitch message and parses them to be slightly more usable.
|
||||
func parse_tags(tags : Dictionary):
|
||||
|
||||
var new_tags : Dictionary
|
||||
for all in tags.keys():
|
||||
|
||||
if all == "badges":
|
||||
|
||||
tags[all] = tags[all].split(",")
|
||||
|
||||
|
||||
new_tags[all.replace("-", "_")] = tags[all]
|
||||
|
||||
|
||||
return new_tags
|
||||
#{ "@badge-info": "subscriber/34", "badges": "broadcaster/1,subscriber/6,game-developer/1", "client-nonce": "b2e3524806f51c94cadd61d338bc14ed", "color": "#2E8B57", "display-name": "EroAxee", "emotes": "", "first-msg": "0", "flags": "", "id": "494dc47e-0d9c-4407-83ec-309764e1adf3", "mod": "0", "returning-chatter": "0", "room-id": "160349129", "subscriber": "1", "tmi-sent-ts": "1701853794297", "turbo": "0", "user-id": "160349129", "user-type": "" }
|
||||
|
||||
## Wrapper function around [method WebSocketPeer.send_text]
|
||||
func send_chat(msg : String, channel : String = ""):
|
||||
|
||||
if channel.is_empty():
|
||||
|
||||
channel = channels[0]
|
||||
|
||||
|
||||
channel = channel.strip_edges()
|
||||
|
||||
send_text("PRIVMSG #" + channel + " :" + msg + "\r\n")
|
||||
|
||||
|
||||
## Utility function that handles joining the supplied [param channel]'s chat.
|
||||
func join_chat(channel : String):
|
||||
|
||||
send_text("JOIN #" + channel + "\r\n")
|
||||
channels.append(channel)
|
||||
|
||||
|
||||
## Utility function that handles leaving the supplied [param channel]'s chat.
|
||||
func leave_chat(channel : String):
|
||||
|
||||
send_chat("PART #" + channel)
|
||||
channels.erase(channel)
|
||||
|
33
addons/no_twitch/demo/Chat_Join.gd
Normal file
33
addons/no_twitch/demo/Chat_Join.gd
Normal file
|
@ -0,0 +1,33 @@
|
|||
extends HBoxContainer
|
||||
|
||||
var channel : String
|
||||
|
||||
func _ready():
|
||||
|
||||
%Channel_Input.text_changed.connect(update_channel)
|
||||
%Join_Chat.pressed.connect(join_chat)
|
||||
%Start_Chat_Connection.pressed.connect(start_chat_connection)
|
||||
|
||||
%Chat_Msg.text_submitted.connect(send_chat)
|
||||
%Send_Button.pressed.connect(func(): send_chat(%Chat_Msg.text))
|
||||
|
||||
|
||||
func update_channel(new_channel):
|
||||
|
||||
channel = new_channel
|
||||
|
||||
|
||||
func start_chat_connection():
|
||||
|
||||
%Twitch_Connection.setup_chat_connection(channel)
|
||||
|
||||
|
||||
func join_chat():
|
||||
|
||||
%Twitch_Connection.join_channel(channel)
|
||||
|
||||
|
||||
func send_chat(chat : String):
|
||||
|
||||
%Twitch_Connection.send_chat_to_channel(chat, %Channel.text)
|
||||
|
27
addons/no_twitch/demo/demo_scene.gd
Normal file
27
addons/no_twitch/demo/demo_scene.gd
Normal file
|
@ -0,0 +1,27 @@
|
|||
extends Control
|
||||
|
||||
func _ready():
|
||||
|
||||
$Twitch_Connection.token_received.connect(save_token)
|
||||
|
||||
load_token()
|
||||
|
||||
|
||||
func save_token(token):
|
||||
|
||||
var res = TokenSaver.new()
|
||||
res.token = token
|
||||
ResourceSaver.save(res, "user://token.tres")
|
||||
|
||||
|
||||
func load_token():
|
||||
|
||||
if !FileAccess.file_exists("user://token.tres"):
|
||||
|
||||
return
|
||||
|
||||
|
||||
var res = ResourceLoader.load("user://token.tres")
|
||||
$Twitch_Connection.token = res.token
|
||||
|
||||
|
81
addons/no_twitch/demo/demo_scene.tscn
Normal file
81
addons/no_twitch/demo/demo_scene.tscn
Normal file
|
@ -0,0 +1,81 @@
|
|||
[gd_scene load_steps=5 format=3 uid="uid://dhss3lpo1mhke"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/no_twitch/twitch_connection.gd" id="1_13a4v"]
|
||||
[ext_resource type="Script" path="res://addons/no_twitch/demo/demo_scene.gd" id="1_ebv0f"]
|
||||
[ext_resource type="Script" path="res://addons/no_twitch/demo/test_button.gd" id="1_hhhwv"]
|
||||
[ext_resource type="Script" path="res://addons/no_twitch/demo/Chat_Join.gd" id="2_b8f4l"]
|
||||
|
||||
[node name="Control" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_ebv0f")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -206.0
|
||||
offset_top = -68.0
|
||||
offset_right = 211.0
|
||||
offset_bottom = 68.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Authenticate" type="Button" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Authenticate"
|
||||
script = ExtResource("1_hhhwv")
|
||||
|
||||
[node name="Start_Chat_Connection" type="Button" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Start Connection"
|
||||
|
||||
[node name="Chat_Join" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
script = ExtResource("2_b8f4l")
|
||||
|
||||
[node name="Channel_Input" type="LineEdit" parent="VBoxContainer/Chat_Join"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Channel"
|
||||
|
||||
[node name="Join_Chat" type="Button" parent="VBoxContainer/Chat_Join"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Join"
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Channel" type="LineEdit" parent="VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 0.45
|
||||
placeholder_text = "Channel"
|
||||
|
||||
[node name="Chat_Msg" type="LineEdit" parent="VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Chat Message"
|
||||
|
||||
[node name="Send_Button" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Send"
|
||||
|
||||
[node name="Twitch_Connection" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("1_13a4v")
|
9
addons/no_twitch/demo/test_button.gd
Normal file
9
addons/no_twitch/demo/test_button.gd
Normal file
|
@ -0,0 +1,9 @@
|
|||
extends Button
|
||||
|
||||
|
||||
@onready var twitch_connection : Twitch_Connection = $"../../Twitch_Connection"
|
||||
|
||||
func _pressed():
|
||||
|
||||
twitch_connection.authenticate_with_twitch()
|
||||
|
4
addons/no_twitch/demo/token_saver.gd
Normal file
4
addons/no_twitch/demo/token_saver.gd
Normal file
|
@ -0,0 +1,4 @@
|
|||
extends Resource
|
||||
class_name TokenSaver
|
||||
|
||||
@export var token : String
|
7
addons/no_twitch/plugin.cfg
Normal file
7
addons/no_twitch/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="No Twitch"
|
||||
description=""
|
||||
author="Eroax"
|
||||
version="0.0.1"
|
||||
script="plugin.gd"
|
12
addons/no_twitch/plugin.gd
Normal file
12
addons/no_twitch/plugin.gd
Normal file
|
@ -0,0 +1,12 @@
|
|||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
|
||||
func _enter_tree():
|
||||
# Initialization of the plugin goes here.
|
||||
pass
|
||||
|
||||
|
||||
func _exit_tree():
|
||||
# Clean-up of the plugin goes here.
|
||||
pass
|
140
addons/no_twitch/twitch_connection.gd
Normal file
140
addons/no_twitch/twitch_connection.gd
Normal file
|
@ -0,0 +1,140 @@
|
|||
extends Node
|
||||
class_name Twitch_Connection
|
||||
|
||||
## Handler for a Connection to a Single Twitch Account
|
||||
##
|
||||
## Used for getting authentication and handling websocket connections.
|
||||
|
||||
signal token_received(token : String)
|
||||
|
||||
@export var client_id := "qyjg1mtby1ycs5scm1pvctos7yvyc1"
|
||||
@export var redirect_uri := "http://localhost"
|
||||
## Port that the redirect_uri will head to on your local system. Defaults to 80 for most cases. Linux tends to prefer 8000 or possibly 1338
|
||||
@export var redirect_port := "8000"
|
||||
|
||||
var twitch_url := "https://id.twitch.tv/oauth2/authorize?response_type=token&"
|
||||
|
||||
var auth_server : TCPServer
|
||||
## Websocket used for handling the chat connection.
|
||||
var chat_socket = preload("res://addons/no_twitch/chat_socket.gd").new()
|
||||
|
||||
var state := make_state()
|
||||
var token : String
|
||||
|
||||
func _ready():
|
||||
|
||||
chat_socket.chat_received.connect(check_chat_socket)
|
||||
|
||||
|
||||
## Handles the basic Twitch Authentication process to request and then later receive a Token (using [method check_auth_peer])
|
||||
func authenticate_with_twitch(client_id = client_id):
|
||||
auth_server = TCPServer.new()
|
||||
|
||||
OS.shell_open(create_auth_url())
|
||||
auth_server.listen(int(redirect_port))
|
||||
|
||||
|
||||
## Sets up a single chat connection. Joining 1 room with [param token] as it's "PASS" specifying what account it's on. And the optional [param default_chat] specifying the default room to join. While [param nick] specifies the "nickname" (Not the username on Twitch)
|
||||
func setup_chat_connection(default_chat : String = "", token : String = token, request_twitch_info = true, nick = "terribletwitch"):
|
||||
|
||||
# Temporary, closes the socket to allow connecting multiple times without needing to reset.
|
||||
chat_socket.close()
|
||||
# Connects to the Twitch IRC server.
|
||||
chat_socket.connect_to_chat(token, request_twitch_info)
|
||||
await chat_socket.socket_open
|
||||
|
||||
if !default_chat.is_empty():
|
||||
|
||||
chat_socket.join_chat(default_chat)
|
||||
|
||||
|
||||
|
||||
## Joins the given [param channel] over IRC. Essentially just sending JOIN #[param channel]
|
||||
func join_channel(channel : String):
|
||||
|
||||
chat_socket.join_chat(channel)
|
||||
|
||||
|
||||
func send_chat(msg : String, channel : String = ""):
|
||||
|
||||
chat_socket.send_chat(msg, channel)
|
||||
|
||||
|
||||
func _process(delta):
|
||||
|
||||
if auth_server and auth_server.is_listening() and auth_server.is_connection_available():
|
||||
|
||||
check_auth_peer(auth_server.take_connection())
|
||||
|
||||
|
||||
if chat_socket:
|
||||
|
||||
chat_socket.poll_socket()
|
||||
|
||||
|
||||
|
||||
## Utility function for creating a Twitch Authentication URL with the given
|
||||
## [param scopes], Twitch Client ID ([param id]) and [param redirect_uri].
|
||||
## [param id] defaults to [member client_id] and both [param redirect] and
|
||||
## [param redirect_port]
|
||||
func create_auth_url(scopes : Array[String] = ["chat:read", "chat:edit"], port := redirect_port, id : String = client_id, redirect : String = redirect_uri):
|
||||
|
||||
var str_scopes : String
|
||||
|
||||
for all in scopes:
|
||||
|
||||
str_scopes += " " + all
|
||||
str_scopes = str_scopes.strip_edges()
|
||||
|
||||
var full_redirect_uri := redirect
|
||||
if !port.is_empty():
|
||||
full_redirect_uri += ":" + port
|
||||
|
||||
var url = twitch_url + "client_id=" + id + "&redirect_uri=" + full_redirect_uri + "&scope=" + str_scopes + "&state=" + str(state)
|
||||
|
||||
return url
|
||||
|
||||
## Utility function for creating a "state" used for different requests for some extra security as a semi password.
|
||||
func make_state(len : int = 16) -> String:
|
||||
|
||||
var crypto = Crypto.new()
|
||||
var state = crypto.generate_random_bytes(len).hex_encode()
|
||||
|
||||
return state
|
||||
|
||||
|
||||
func check_auth_peer(peer : StreamPeerTCP):
|
||||
|
||||
var info = peer.get_utf8_string(peer.get_available_bytes())
|
||||
printraw(info)
|
||||
|
||||
var root := redirect_uri
|
||||
if !redirect_port.is_empty():
|
||||
root += ":" + redirect_port
|
||||
root += "/"
|
||||
|
||||
var script = "<script>fetch('" + root + "' + window.location.hash.substr(1))</script>"
|
||||
|
||||
peer.put_data(str("HTTP/1.1 200\n\n" + script).to_utf8_buffer())
|
||||
|
||||
|
||||
var resp_state = info.split("&state=")
|
||||
|
||||
# Ensures that the received state is correct.
|
||||
if !resp_state.size() > 1 or resp_state[0] == state:
|
||||
|
||||
return
|
||||
|
||||
|
||||
var token = info.split("access_token=")[1].split("&scope=")[0].strip_edges()
|
||||
printraw("Token: ", token, "\n")
|
||||
self.token = token
|
||||
token_received.emit(token)
|
||||
|
||||
|
||||
func check_chat_socket(dict):
|
||||
|
||||
prints(dict.user, dict.message)
|
||||
|
||||
|
||||
|
58
addons/no_twitch/websocket_client.gd
Normal file
58
addons/no_twitch/websocket_client.gd
Normal file
|
@ -0,0 +1,58 @@
|
|||
extends WebSocketPeer
|
||||
class_name Websocket_Client
|
||||
|
||||
## Helper Class for handling the freaking polling of a WebSocketPeer
|
||||
|
||||
## Emitted when [method WebSocketPeer.get_ready_state()] returns
|
||||
## [member WebSocketPeer.STATE_OPEN]
|
||||
signal socket_open
|
||||
## Emitted when [method WebSocketPeer.get_ready_state()] returns
|
||||
## [member WebSocketPeer.STATE_CLOSED]
|
||||
signal socket_closed(close_code, close_reason)
|
||||
## Emitted when [method WebSocketPeer.get_ready_state()] returns
|
||||
## [member WebSocketPeer.STATE_CONNECTING]
|
||||
signal socket_connecting
|
||||
## Emitted when [method WebSocketPeer.get_ready_state()] returns
|
||||
## [member WebSocketPeer.STATE_CLOSING]
|
||||
signal socket_closing
|
||||
## Emitted when [method WebSocketPeer.get_available_packets()] returns greater
|
||||
## than 0. Or, when a packet has been received.
|
||||
signal packet_received(packet_data)
|
||||
|
||||
## Works as a wrapper around [method WebSocketPeer.poll] to handle the logic of
|
||||
## checking get_ready_state() more simply.
|
||||
func poll_socket():
|
||||
|
||||
poll()
|
||||
|
||||
var state = get_ready_state()
|
||||
|
||||
match state:
|
||||
|
||||
STATE_OPEN:
|
||||
|
||||
socket_open.emit(get_connected_host(), get_connected_port())
|
||||
|
||||
if get_available_packet_count() > 0:
|
||||
|
||||
packet_received.emit(get_packet())
|
||||
|
||||
|
||||
|
||||
STATE_CONNECTING:
|
||||
|
||||
socket_connecting.emit()
|
||||
|
||||
|
||||
STATE_CLOSING:
|
||||
|
||||
socket_closing.emit()
|
||||
|
||||
|
||||
STATE_CLOSED:
|
||||
|
||||
socket_closed.emit(get_close_code(), get_close_reason())
|
||||
|
||||
|
||||
|
||||
|
12
classes/connections/connections.gd
Normal file
12
classes/connections/connections.gd
Normal file
|
@ -0,0 +1,12 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
class_name Connections
|
||||
|
||||
static var obs_websocket
|
||||
static var twitch
|
||||
|
||||
static func _twitch_chat_received(msg_dict : Dictionary):
|
||||
|
||||
DeckHolder.send_event(&"twitch_chat", msg_dict)
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
class_name Deck
|
||||
## A deck/graph with nodes.
|
||||
##
|
||||
|
@ -16,11 +19,13 @@ var save_path: String = ""
|
|||
var is_group: bool = false
|
||||
## List of groups belonging to this deck, in the format of[br]
|
||||
## [code]Dictionary[String -> Deck.id, Deck][/code]
|
||||
var groups: Dictionary = {}
|
||||
## A unique identifier for this deck.
|
||||
#var groups: Dictionary = {}
|
||||
## A unique identifier for this deck, or an ID for the group this deck represents.
|
||||
var id: String = ""
|
||||
## If this is a group, this is the local ID of this instance of the group.
|
||||
var instance_id: String = ""
|
||||
## The parent deck of this deck, if this is a group.
|
||||
var _belonging_to: Deck # for groups
|
||||
#var _belonging_to: Deck # for groups
|
||||
## The ID of this group's input node. Used only if [member is_group] is [code]true[/code].
|
||||
var group_input_node: String
|
||||
## The ID of this group's input node. Used only if [member is_group] is [code]true[/code].
|
||||
|
@ -28,13 +33,25 @@ var group_output_node: String
|
|||
## The ID of the group node this group is represented by, contained in this deck's parent deck.
|
||||
## Used only if [member is_group] is [code]true[/code].
|
||||
## @experimental
|
||||
var group_node: String
|
||||
#var group_node: String
|
||||
|
||||
var emit_group_signals: bool = true
|
||||
var emit_node_added_signal: bool = true
|
||||
|
||||
## Emitted when a node has been added to this deck.
|
||||
signal node_added(node: DeckNode)
|
||||
## Emitted when a node has been removed from this deck.
|
||||
signal node_removed(node: DeckNode)
|
||||
|
||||
#region group signals
|
||||
signal node_added_to_group(node: DeckNode, assign_id: String, assign_to_self: bool, deck: Deck)
|
||||
signal node_removed_from_group(node_id: String, remove_connections: bool, deck: Deck)
|
||||
signal nodes_connected_in_group(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int, deck: Deck)
|
||||
signal nodes_disconnected_in_group(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int, deck: Deck)
|
||||
signal node_port_value_updated(node_id: String, port_idx: int, new_value: Variant, deck: Deck)
|
||||
signal node_renamed(node_id: String, new_name: String, deck: Deck)
|
||||
signal node_moved(node_id: String, new_position: Dictionary, deck: Deck)
|
||||
#endregion
|
||||
|
||||
## Instantiate a node by its' [member DeckNode.node_type] and add it to this deck.[br]
|
||||
## See [method add_node_inst] for parameter descriptions.
|
||||
|
@ -59,9 +76,32 @@ func add_node_inst(node: DeckNode, assign_id: String = "", assign_to_self: bool
|
|||
node._id = uuid
|
||||
else:
|
||||
nodes[assign_id] = node
|
||||
node._id = assign_id
|
||||
|
||||
if emit_node_added_signal:
|
||||
node_added.emit(node)
|
||||
|
||||
if is_group && emit_group_signals:
|
||||
node_added_to_group.emit(node, node._id, assign_to_self, self)
|
||||
|
||||
node.port_value_updated.connect(
|
||||
func(port_idx: int, new_value: Variant):
|
||||
if is_group && emit_group_signals:
|
||||
node_port_value_updated.emit(node._id, port_idx, new_value, self)
|
||||
)
|
||||
|
||||
node.renamed.connect(
|
||||
func(new_name: String):
|
||||
if is_group && emit_group_signals:
|
||||
node_renamed.emit(node._id, new_name, self)
|
||||
)
|
||||
|
||||
node.position_updated.connect(
|
||||
func(new_position: Dictionary):
|
||||
if is_group && emit_group_signals:
|
||||
node_moved.emit(node._id, new_position, self)
|
||||
)
|
||||
|
||||
return node
|
||||
|
||||
|
||||
|
@ -71,26 +111,36 @@ func get_node(uuid: String) -> DeckNode:
|
|||
|
||||
|
||||
## Attempt to connect two nodes. Returns [code]true[/code] if the connection succeeded.
|
||||
func connect_nodes(from_node: DeckNode, to_node: DeckNode, from_output_port: int, to_input_port: int) -> bool:
|
||||
func connect_nodes(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> bool:
|
||||
var from_node := get_node(from_node_id)
|
||||
var to_node := get_node(to_node_id)
|
||||
# check that we can do the type conversion
|
||||
var type_a: DeckType.Types = from_node.get_output_ports()[from_output_port].type
|
||||
var type_b: DeckType.Types = to_node.get_input_ports()[to_input_port].type
|
||||
if !DeckType.can_convert(type_a, type_b):
|
||||
print("Can not convert from %s to %s." % [DeckType.type_str(type_a), DeckType.type_str(type_b)])
|
||||
DeckHolder.logger.log_deck(
|
||||
"Can not convert from %s to %s." % [DeckType.type_str(type_a), DeckType.type_str(type_b)],
|
||||
Logger.LogType.ERROR
|
||||
)
|
||||
return false
|
||||
|
||||
# TODO: prevent duplicate connections
|
||||
|
||||
if is_group && emit_group_signals:
|
||||
nodes_connected_in_group.emit(from_node_id, to_node_id, from_output_port, to_input_port, self)
|
||||
|
||||
from_node.add_outgoing_connection(from_output_port, to_node._id, to_input_port)
|
||||
return true
|
||||
|
||||
|
||||
## Remove a connection from two nodes.
|
||||
func disconnect_nodes(from_node: DeckNode, to_node: DeckNode, from_output_port: int, to_input_port: int) -> void:
|
||||
var hash = {to_node._id: to_input_port}.hash()
|
||||
|
||||
from_node.remove_outgoing_connection(from_output_port, hash)
|
||||
func disconnect_nodes(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> void:
|
||||
var from_node := get_node(from_node_id)
|
||||
var to_node := get_node(to_node_id)
|
||||
from_node.remove_outgoing_connection(from_output_port, to_node_id, to_input_port)
|
||||
to_node.remove_incoming_connection(to_input_port)
|
||||
if is_group && emit_group_signals:
|
||||
nodes_disconnected_in_group.emit(from_node_id, to_node_id, from_output_port, to_input_port, self)
|
||||
|
||||
|
||||
## Returns true if this deck has no nodes and no variables.
|
||||
|
@ -99,12 +149,38 @@ func is_empty() -> bool:
|
|||
|
||||
|
||||
## Remove a node from this deck.
|
||||
func remove_node(uuid: String) -> void:
|
||||
var node = nodes.get(uuid)
|
||||
func remove_node(uuid: String, remove_connections: bool = false, force: bool = false, keep_group_instances: bool = false) -> void:
|
||||
var node := get_node(uuid)
|
||||
if node == null:
|
||||
return
|
||||
|
||||
if !node.user_can_delete && !force:
|
||||
return
|
||||
|
||||
if node.node_type == "group_node" && !keep_group_instances:
|
||||
DeckHolder.close_group_instance(node.group_id, node.group_instance_id)
|
||||
|
||||
if remove_connections:
|
||||
var outgoing_connections := node.outgoing_connections.duplicate(true)
|
||||
|
||||
for output_port: int in outgoing_connections:
|
||||
for to_node: String in outgoing_connections[output_port]:
|
||||
for to_port: int in outgoing_connections[output_port][to_node]:
|
||||
disconnect_nodes(uuid, to_node, output_port, to_port)
|
||||
|
||||
var incoming_connections := node.incoming_connections.duplicate(true)
|
||||
|
||||
for input_port: int in incoming_connections:
|
||||
for from_node: String in incoming_connections[input_port]:
|
||||
disconnect_nodes(from_node, uuid, incoming_connections[input_port][from_node], input_port)
|
||||
|
||||
nodes.erase(uuid)
|
||||
|
||||
node_removed.emit(node)
|
||||
|
||||
if is_group && emit_group_signals:
|
||||
node_removed_from_group.emit(uuid, remove_connections, self)
|
||||
|
||||
|
||||
## Group the [param nodes_to_group] into a new deck and return it.
|
||||
## Returns [code]null[/code] on failure.[br]
|
||||
|
@ -113,70 +189,216 @@ func group_nodes(nodes_to_group: Array) -> Deck:
|
|||
if nodes_to_group.is_empty():
|
||||
return null
|
||||
|
||||
# don't include nodes that can't be grouped/deleted
|
||||
nodes_to_group = nodes_to_group.filter(
|
||||
func(x: DeckNode):
|
||||
return x.user_can_delete
|
||||
)
|
||||
|
||||
var node_ids_to_keep := nodes_to_group.map(
|
||||
func(x: DeckNode):
|
||||
return x._id
|
||||
)
|
||||
|
||||
var group := Deck.new()
|
||||
group.is_group = true
|
||||
group._belonging_to = self
|
||||
var group_id := UUID.v4()
|
||||
group.id = group_id
|
||||
var group := DeckHolder.add_empty_group()
|
||||
|
||||
var midpoint := Vector2()
|
||||
|
||||
var rightmost := -INF
|
||||
var leftmost := INF
|
||||
for node: DeckNode in nodes_to_group:
|
||||
if node.node_type == "group_node": # for recursive grouping
|
||||
var _group_id: String = node.group_id
|
||||
var _group: Deck = groups[_group_id]
|
||||
groups.erase(_group)
|
||||
group.groups[_group_id] = _group
|
||||
_group._belonging_to = group
|
||||
#if node.node_type == "group_node":
|
||||
#var _group_id: String = node.group_id
|
||||
#var _group: Deck = groups[_group_id]
|
||||
#groups.erase(_group)
|
||||
#group.groups[_group_id] = _group
|
||||
#_group._belonging_to = group
|
||||
|
||||
for from_port: int in node.outgoing_connections:
|
||||
for connection: Dictionary in node.outgoing_connections[from_port]:
|
||||
if !(connection.keys()[0] in node_ids_to_keep):
|
||||
disconnect_nodes(node, get_node(connection.keys()[0]), from_port, connection.values()[0])
|
||||
if node.position.x > rightmost:
|
||||
rightmost = node.position.x
|
||||
if node.position.x < leftmost:
|
||||
leftmost = node.position.x
|
||||
|
||||
for to_port: int in node.incoming_connections:
|
||||
for from_node: String in node.incoming_connections[to_port]:
|
||||
var outgoing_connections := node.outgoing_connections.duplicate(true)
|
||||
|
||||
for from_port: int in outgoing_connections:
|
||||
for to_node: String in outgoing_connections[from_port]:
|
||||
for to_port: int in outgoing_connections[from_port][to_node]:
|
||||
if !(to_node in node_ids_to_keep):
|
||||
disconnect_nodes(node._id, to_node, from_port, to_port)
|
||||
|
||||
var incoming_connections := node.incoming_connections.duplicate(true)
|
||||
|
||||
for to_port: int in incoming_connections:
|
||||
for from_node: String in incoming_connections[to_port]:
|
||||
if !(from_node in node_ids_to_keep):
|
||||
disconnect_nodes(get_node(from_node), node, node.incoming_connections[to_port].values()[0], to_port)
|
||||
disconnect_nodes(from_node, node._id, incoming_connections[to_port][from_node], to_port)
|
||||
|
||||
midpoint += node.position_as_vector2()
|
||||
remove_node(node._id)
|
||||
remove_node(node._id, false, true)
|
||||
group.add_node_inst(node, node._id)
|
||||
|
||||
midpoint /= nodes_to_group.size()
|
||||
|
||||
emit_node_added_signal = false
|
||||
var _group_node := add_node_type("group_node")
|
||||
_group_node.group_id = group_id
|
||||
_group_node.group_id = group.id
|
||||
_group_node.group_instance_id = group.instance_id
|
||||
_group_node.position.x = midpoint.x
|
||||
_group_node.position.y = midpoint.y
|
||||
_group_node.position_updated.emit(_group_node.position)
|
||||
group.group_node = _group_node._id
|
||||
#group.group_node = _group_node._id
|
||||
node_added.emit(_group_node)
|
||||
emit_node_added_signal = true
|
||||
|
||||
var input_node := group.add_node_type("group_input")
|
||||
var output_node := group.add_node_type("group_output")
|
||||
group.group_input_node = input_node._id
|
||||
group.group_output_node = output_node._id
|
||||
|
||||
_group_node.input_node = input_node
|
||||
_group_node.output_node = output_node
|
||||
_group_node.setup_connections()
|
||||
input_node.position.x = leftmost - 350
|
||||
output_node.position.x = rightmost + 350
|
||||
input_node.position.y = midpoint.y
|
||||
output_node.position.y = midpoint.y
|
||||
input_node.position_updated.emit(input_node.position)
|
||||
output_node.position_updated.emit(output_node.position)
|
||||
|
||||
groups[group_id] = group
|
||||
input_node.group_node = _group_node
|
||||
output_node.group_node = _group_node
|
||||
|
||||
_group_node.input_node_id = input_node._id
|
||||
_group_node.output_node_id = output_node._id
|
||||
#_group_node.setup_connections()
|
||||
_group_node.init_io()
|
||||
|
||||
return group
|
||||
|
||||
|
||||
## Get a group belonging to this deck by its ID.
|
||||
func get_group(uuid: String) -> Deck:
|
||||
return groups.get(uuid)
|
||||
#func get_group(uuid: String) -> Deck:
|
||||
#return groups.get(uuid)
|
||||
|
||||
|
||||
func copy_nodes(nodes_to_copy: Array[String]) -> Dictionary:
|
||||
var d := {"nodes": {}}
|
||||
|
||||
for node_id: String in nodes_to_copy:
|
||||
d.nodes[node_id] = get_node(node_id).to_dict()
|
||||
|
||||
for node: String in d.nodes:
|
||||
var outgoing_connections: Dictionary = d.nodes[node].outgoing_connections.duplicate(true)
|
||||
|
||||
for from_port: int in outgoing_connections:
|
||||
for to_node: String in outgoing_connections[from_port]:
|
||||
if !(to_node in nodes_to_copy):
|
||||
(d.nodes[node].outgoing_connections[from_port] as Dictionary).erase(to_node)
|
||||
|
||||
var incoming_connections: Dictionary = d.nodes[node].incoming_connections.duplicate(true)
|
||||
|
||||
for to_port: int in incoming_connections:
|
||||
for from_node: String in incoming_connections[to_port]:
|
||||
if !(from_node in nodes_to_copy):
|
||||
(d.nodes[node].incoming_connections[to_port] as Dictionary).erase(from_node)
|
||||
|
||||
for node: Dictionary in d.nodes.values().slice(1):
|
||||
node.position.x = node.position.x - d.nodes.values()[0].position.x
|
||||
node.position.y = node.position.y - d.nodes.values()[0].position.y
|
||||
|
||||
d.nodes.values()[0].position.x = 0
|
||||
d.nodes.values()[0].position.y = 0
|
||||
|
||||
return d
|
||||
|
||||
|
||||
func copy_nodes_json(nodes_to_copy: Array[String]) -> String:
|
||||
return JSON.stringify(copy_nodes(nodes_to_copy))
|
||||
|
||||
|
||||
func allocate_ids(count: int) -> Array[String]:
|
||||
var res: Array[String] = []
|
||||
for i in count:
|
||||
res.append(UUID.v4())
|
||||
return res
|
||||
|
||||
|
||||
func paste_nodes_from_dict(nodes: Dictionary, position: Vector2 = Vector2()) -> void:
|
||||
if !nodes.get("nodes"):
|
||||
return
|
||||
|
||||
var new_ids := allocate_ids(nodes.nodes.size())
|
||||
var ids_map := {}
|
||||
for i: int in nodes.nodes.keys().size():
|
||||
var node_id: String = nodes.nodes.keys()[i]
|
||||
ids_map[node_id] = new_ids[i]
|
||||
|
||||
for node_id: String in nodes.nodes:
|
||||
nodes.nodes[node_id]._id = ids_map[node_id]
|
||||
|
||||
nodes.nodes[node_id].position.x += position.x
|
||||
nodes.nodes[node_id].position.y += position.y
|
||||
|
||||
var outgoing_connections: Dictionary = nodes.nodes[node_id].outgoing_connections as Dictionary
|
||||
var outgoing_connections_res := {}
|
||||
for from_port in outgoing_connections:
|
||||
outgoing_connections_res[from_port] = {}
|
||||
for to_node_id in outgoing_connections[from_port]:
|
||||
outgoing_connections_res[from_port][ids_map[to_node_id]] = outgoing_connections[from_port][to_node_id]
|
||||
|
||||
var incoming_connections: Dictionary = nodes.nodes[node_id].incoming_connections as Dictionary
|
||||
var incoming_connections_res := {}
|
||||
for to_port in incoming_connections:
|
||||
incoming_connections_res[to_port] = {}
|
||||
for from_node_id in incoming_connections[to_port]:
|
||||
incoming_connections_res[to_port][ids_map[from_node_id]] = incoming_connections[to_port][from_node_id]
|
||||
|
||||
nodes.nodes[node_id].outgoing_connections = outgoing_connections_res
|
||||
nodes.nodes[node_id].incoming_connections = incoming_connections_res
|
||||
|
||||
var node := DeckNode.from_dict(nodes.nodes[node_id])
|
||||
if node.node_type == "group_node":
|
||||
var group := DeckHolder.make_new_group_instance(node.group_id)
|
||||
node.group_instance_id = group.instance_id
|
||||
group.get_node(group.group_input_node).group_node = node
|
||||
group.get_node(group.group_output_node).group_node = node
|
||||
node.input_node = group.get_node(group.group_input_node)
|
||||
node.output_node = group.get_node(group.group_output_node)
|
||||
node.init_io()
|
||||
add_node_inst(node, ids_map[node_id])
|
||||
|
||||
|
||||
func paste_nodes_from_json(json: String, position: Vector2 = Vector2()) -> void:
|
||||
paste_nodes_from_dict(JSON.parse_string(json), position)
|
||||
|
||||
|
||||
func duplicate_nodes(nodes: Array[String]) -> void:
|
||||
if nodes.is_empty():
|
||||
return
|
||||
|
||||
var position := get_node(nodes[0]).position_as_vector2() + Vector2(50, 50)
|
||||
var d := copy_nodes(nodes)
|
||||
paste_nodes_from_dict(d, position)
|
||||
|
||||
|
||||
func send_event(event_name: StringName, event_data: Dictionary = {}) -> void:
|
||||
for node: DeckNode in nodes.values():
|
||||
node._event_received(event_name, event_data)
|
||||
|
||||
|
||||
func get_referenced_groups() -> Array[String]:
|
||||
# this is expensive
|
||||
# recursively returns a list of all groups referenced by this deck
|
||||
var res: Array[String] = []
|
||||
for node_id: String in nodes:
|
||||
var node := get_node(node_id)
|
||||
if node.node_type != "group_node":
|
||||
continue
|
||||
res.append(node.group_id)
|
||||
res.append_array(DeckHolder.get_deck(node.group_id).get_referenced_groups())
|
||||
return res
|
||||
|
||||
|
||||
## Returns a [Dictionary] representation of this deck.
|
||||
func to_dict(with_meta: bool = true) -> Dictionary:
|
||||
func to_dict(with_meta: bool = true, group_ids: Array = []) -> Dictionary:
|
||||
var inner := {
|
||||
"nodes": {},
|
||||
"variable_stack": variable_stack,
|
||||
|
@ -184,14 +406,18 @@ func to_dict(with_meta: bool = true) -> Dictionary:
|
|||
"groups": {}
|
||||
}
|
||||
|
||||
for node_id in nodes.keys():
|
||||
for node_id: String in nodes.keys():
|
||||
inner["nodes"][node_id] = nodes[node_id].to_dict(with_meta)
|
||||
if (nodes[node_id] as DeckNode).node_type == "group_node":
|
||||
if !(nodes[node_id].group_id in group_ids):
|
||||
inner["groups"][nodes[node_id].group_id] = DeckHolder.get_deck(nodes[node_id].group_id).to_dict(with_meta, group_ids)
|
||||
group_ids.append(nodes[node_id].group_id)
|
||||
|
||||
for group_id in groups.keys():
|
||||
inner["groups"][group_id] = groups[group_id].to_dict(with_meta)
|
||||
#for group_id in groups.keys():
|
||||
#inner["groups"][group_id] = groups[group_id].to_dict(with_meta)
|
||||
|
||||
if is_group:
|
||||
inner["group_node"] = group_node
|
||||
inner["instance_id"] = instance_id
|
||||
inner["group_input_node"] = group_input_node
|
||||
inner["group_output_node"] = group_output_node
|
||||
|
||||
|
@ -217,52 +443,32 @@ static func from_dict(data: Dictionary, path: String = "") -> Deck:
|
|||
var nodes_data: Dictionary = data.deck.nodes as Dictionary
|
||||
|
||||
for node_id in nodes_data:
|
||||
var node := deck.add_node_type(nodes_data[node_id].node_type, node_id, false)
|
||||
node._id = node_id
|
||||
node.name = nodes_data[node_id].name
|
||||
node._belonging_to = deck
|
||||
node.position = nodes_data[node_id].position
|
||||
|
||||
for prop in nodes_data[node_id].props:
|
||||
node.set(prop, nodes_data[node_id].props[prop])
|
||||
|
||||
node._pre_connection()
|
||||
|
||||
for connection_id in nodes_data[node_id].outgoing_connections:
|
||||
var connection_data = nodes_data[node_id].outgoing_connections[connection_id]
|
||||
for connection in connection_data:
|
||||
connection[connection.keys()[0]] = int(connection.values()[0])
|
||||
node.outgoing_connections[int(connection_id)] = connection_data
|
||||
|
||||
for connection_id in nodes_data[node_id].incoming_connections:
|
||||
var connection_data = nodes_data[node_id].incoming_connections[connection_id]
|
||||
for connection in connection_data:
|
||||
connection_data[connection] = int(connection_data[connection])
|
||||
node.incoming_connections[int(connection_id)] = connection_data
|
||||
|
||||
for i in node.ports.size():
|
||||
var port_value: Variant
|
||||
if (nodes_data[node_id].port_values as Array).size() <= i:
|
||||
port_value = null
|
||||
else:
|
||||
port_value = (nodes_data[node_id].port_values as Array)[i]
|
||||
node.ports[i].value = port_value
|
||||
|
||||
for key in nodes_data[node_id].meta:
|
||||
node.set_meta(key, str_to_var(nodes_data[node_id].meta[key]))
|
||||
|
||||
node._post_load()
|
||||
var node := DeckNode.from_dict(nodes_data[node_id])
|
||||
deck.add_node_inst(node, node_id)
|
||||
|
||||
var groups_data: Dictionary = data.deck.groups as Dictionary
|
||||
|
||||
for group_id: String in groups_data:
|
||||
var group := Deck.from_dict(groups_data[group_id])
|
||||
group._belonging_to = deck
|
||||
group.is_group = true
|
||||
deck.groups[group_id] = group
|
||||
group.group_node = groups_data[group_id]["deck"]["group_node"]
|
||||
group.group_input_node = groups_data[group_id]["deck"]["group_input_node"]
|
||||
group.group_output_node = groups_data[group_id]["deck"]["group_output_node"]
|
||||
deck.get_node(group.group_node).init_io()
|
||||
for node_id: String in deck.nodes:
|
||||
var node := deck.get_node(node_id)
|
||||
if node.node_type != "group_node":
|
||||
continue
|
||||
var group_id: String = node.group_id
|
||||
var group_instance_id: String = node.group_instance_id
|
||||
var group_data: Dictionary = groups_data[group_id]
|
||||
var group := DeckHolder.add_group_from_dict(group_data, group_id, group_instance_id)
|
||||
group.get_node(group.group_input_node).group_node = node
|
||||
group.get_node(group.group_output_node).group_node = node
|
||||
node.init_io()
|
||||
|
||||
#for group_id: String in groups_data:
|
||||
#var group := Deck.from_dict(groups_data[group_id])
|
||||
#group._belonging_to = deck
|
||||
#group.is_group = true
|
||||
#deck.groups[group_id] = group
|
||||
#group.group_node = groups_data[group_id]["deck"]["group_node"]
|
||||
#group.group_input_node = groups_data[group_id]["deck"]["group_input_node"]
|
||||
#group.group_output_node = groups_data[group_id]["deck"]["group_output_node"]
|
||||
#deck.get_node(group.group_node).init_io()
|
||||
|
||||
|
||||
return deck
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
class_name DeckHolder
|
||||
## @experimental
|
||||
## A static class holding references to all decks opened in the current session.
|
||||
|
||||
## List of decks opened this session.
|
||||
static var decks: Array[Deck]
|
||||
#static var decks: Array[Deck]
|
||||
static var decks: Dictionary # Dictionary[String -> id, (Deck|Dictionary[String -> instance_id, Deck])]
|
||||
|
||||
static var logger := Logger.new()
|
||||
|
||||
|
||||
## Returns a new empty deck and assigns a new random ID to it.
|
||||
static func add_empty_deck() -> Deck:
|
||||
var deck := Deck.new()
|
||||
DeckHolder.decks.append(deck)
|
||||
var uuid := UUID.v4()
|
||||
decks[uuid] = deck
|
||||
deck.id = uuid
|
||||
return deck
|
||||
|
||||
|
@ -21,12 +27,200 @@ static func open_deck_from_file(path: String) -> Deck:
|
|||
if f.get_error() != OK:
|
||||
return null
|
||||
|
||||
var deck := Deck.from_dict(JSON.parse_string(f.get_as_text()), path)
|
||||
DeckHolder.decks.append(deck)
|
||||
|
||||
var deck := open_deck_from_dict(JSON.parse_string(f.get_as_text()), path)
|
||||
return deck
|
||||
|
||||
|
||||
## Unloads a deck.
|
||||
static func close_deck(deck: Deck) -> void:
|
||||
DeckHolder.decks.erase(deck)
|
||||
static func open_deck_from_dict(data: Dictionary, path := "") -> Deck:
|
||||
var deck := Deck.from_dict(data, path)
|
||||
decks[deck.id] = deck
|
||||
return deck
|
||||
|
||||
|
||||
static func add_group_from_dict(data: Dictionary, deck_id: String, instance_id: String) -> Deck:
|
||||
var group := Deck.from_dict(data)
|
||||
group.instance_id = instance_id
|
||||
group.is_group = true
|
||||
group.group_input_node = data.deck.group_input_node
|
||||
group.group_output_node = data.deck.group_output_node
|
||||
var instances: Dictionary = decks.get(deck_id, {})
|
||||
instances[instance_id] = group
|
||||
decks[deck_id] = instances
|
||||
connect_group_signals(group)
|
||||
return group
|
||||
|
||||
|
||||
static func make_new_group_instance(group_id: String) -> Deck:
|
||||
var group := get_deck(group_id)
|
||||
var data := group.to_dict()
|
||||
return add_group_from_dict(data, group_id, UUID.v4())
|
||||
|
||||
|
||||
static func add_empty_group() -> Deck:
|
||||
var group := Deck.new()
|
||||
group.is_group = true
|
||||
group.id = UUID.v4()
|
||||
group.instance_id = UUID.v4()
|
||||
decks[group.id] = {group.instance_id: group}
|
||||
connect_group_signals(group)
|
||||
return group
|
||||
|
||||
|
||||
static func connect_group_signals(group: Deck) -> void:
|
||||
group.node_added_to_group.connect(DeckHolder._on_node_added_to_group)
|
||||
group.node_removed_from_group.connect(DeckHolder._on_node_removed_from_group)
|
||||
group.nodes_connected_in_group.connect(DeckHolder._on_nodes_connected_in_group)
|
||||
group.nodes_disconnected_in_group.connect(DeckHolder._on_nodes_disconnected_in_group)
|
||||
group.node_port_value_updated.connect(DeckHolder._on_node_port_value_updated)
|
||||
group.node_renamed.connect(DeckHolder._on_node_renamed)
|
||||
group.node_moved.connect(DeckHolder._on_node_moved)
|
||||
|
||||
|
||||
static func get_deck(id: String) -> Deck:
|
||||
if !decks.has(id):
|
||||
return null
|
||||
|
||||
if !(decks[id] is Dictionary):
|
||||
return decks[id]
|
||||
else:
|
||||
return (decks[id] as Dictionary).values()[0]
|
||||
|
||||
|
||||
static func get_group_instance(group_id: String, instance_id: String) -> Deck:
|
||||
if !decks.has(group_id):
|
||||
return null
|
||||
|
||||
if decks[group_id] is Dictionary:
|
||||
return (decks[group_id] as Dictionary).get(instance_id)
|
||||
else:
|
||||
return null
|
||||
|
||||
|
||||
static func close_group_instance(group_id: String, instance_id: String) -> void:
|
||||
# this is kinda dumb, but to close groups that may be dangling
|
||||
# when all instances are closed, we have to get that list
|
||||
# *before* we close the instance
|
||||
var dangling_groups := get_deck(group_id).get_referenced_groups()
|
||||
|
||||
var group_instances: Dictionary = decks.get(group_id, {}) as Dictionary
|
||||
group_instances.erase(instance_id)
|
||||
if group_instances.is_empty():
|
||||
for group in dangling_groups:
|
||||
close_all_group_instances(group)
|
||||
decks.erase(group_id)
|
||||
|
||||
|
||||
static func close_all_group_instances(group_id: String) -> void:
|
||||
if decks.get(group_id) is Dictionary:
|
||||
decks.erase(group_id)
|
||||
|
||||
|
||||
## Unloads a deck. Returns a list of groups that are closed as a result of
|
||||
## closing this deck.
|
||||
static func close_deck(deck_id: String) -> Array:
|
||||
if decks.get(deck_id) is Deck:
|
||||
var deck: Deck = decks[deck_id] as Deck
|
||||
var groups := deck.get_referenced_groups()
|
||||
for group in groups:
|
||||
close_all_group_instances(group)
|
||||
decks.erase(deck_id)
|
||||
return groups
|
||||
|
||||
return []
|
||||
|
||||
|
||||
static func send_event(event_name: StringName, event_data: Dictionary = {}) -> void:
|
||||
for deck_id: String in decks:
|
||||
if decks[deck_id] is Deck:
|
||||
(decks[deck_id] as Deck).send_event(event_name, event_data)
|
||||
else:
|
||||
for deck_instance_id: String in decks[deck_id]:
|
||||
(decks[deck_id][deck_instance_id] as Deck).send_event(event_name, event_data)
|
||||
|
||||
|
||||
#region group signal callbacks
|
||||
static func _on_node_added_to_group(node: DeckNode, assign_id: String, assign_to_self: bool, deck: Deck) -> void:
|
||||
var group_id := deck.id
|
||||
for instance_id: String in decks[group_id]:
|
||||
if instance_id == deck.instance_id:
|
||||
continue
|
||||
|
||||
var instance: Deck = get_group_instance(group_id, instance_id)
|
||||
instance.emit_group_signals = false
|
||||
var node_duplicate := DeckNode.from_dict(node.to_dict())
|
||||
instance.add_node_inst(node_duplicate, assign_id, assign_to_self)
|
||||
instance.emit_group_signals = true
|
||||
|
||||
|
||||
static func _on_node_removed_from_group(node_id: String, remove_connections: bool, deck: Deck) -> void:
|
||||
var group_id := deck.id
|
||||
for instance_id: String in decks[group_id]:
|
||||
if instance_id == deck.instance_id:
|
||||
continue
|
||||
|
||||
var instance: Deck = get_group_instance(group_id, instance_id)
|
||||
instance.emit_group_signals = false
|
||||
instance.remove_node(node_id, remove_connections)
|
||||
instance.emit_group_signals = true
|
||||
|
||||
|
||||
static func _on_nodes_connected_in_group(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int, deck: Deck) -> void:
|
||||
var group_id := deck.id
|
||||
for instance_id: String in decks[group_id]:
|
||||
if instance_id == deck.instance_id:
|
||||
continue
|
||||
|
||||
var instance: Deck = get_group_instance(group_id, instance_id)
|
||||
instance.emit_group_signals = false
|
||||
instance.connect_nodes(from_node_id, to_node_id, from_output_port, to_input_port)
|
||||
instance.emit_group_signals = true
|
||||
|
||||
|
||||
static func _on_nodes_disconnected_in_group(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int, deck: Deck) -> void:
|
||||
var group_id := deck.id
|
||||
for instance_id: String in decks[group_id]:
|
||||
if instance_id == deck.instance_id:
|
||||
continue
|
||||
|
||||
var instance: Deck = get_group_instance(group_id, instance_id)
|
||||
instance.emit_group_signals = false
|
||||
instance.disconnect_nodes(from_node_id, to_node_id, from_output_port, to_input_port)
|
||||
instance.emit_group_signals = true
|
||||
|
||||
|
||||
static func _on_node_port_value_updated(node_id: String, port_idx: int, new_value: Variant, deck: Deck) -> void:
|
||||
var group_id := deck.id
|
||||
for instance_id: String in decks[group_id]:
|
||||
if instance_id == deck.instance_id:
|
||||
continue
|
||||
|
||||
var instance: Deck = get_group_instance(group_id, instance_id)
|
||||
instance.emit_group_signals = false
|
||||
instance.get_node(node_id).get_all_ports()[port_idx].set_value_no_signal(new_value)
|
||||
instance.emit_group_signals = true
|
||||
|
||||
|
||||
static func _on_node_renamed(node_id: String, new_name: String, deck: Deck) -> void:
|
||||
var group_id := deck.id
|
||||
for instance_id: String in decks[group_id]:
|
||||
if instance_id == deck.instance_id:
|
||||
continue
|
||||
|
||||
var instance: Deck = get_group_instance(group_id, instance_id)
|
||||
instance.emit_group_signals = false
|
||||
instance.get_node(node_id).name = new_name
|
||||
instance.emit_group_signals = true
|
||||
|
||||
|
||||
static func _on_node_moved(node_id: String, new_position: Dictionary, deck: Deck) -> void:
|
||||
var group_id := deck.id
|
||||
for instance_id: String in decks[group_id]:
|
||||
if instance_id == deck.instance_id:
|
||||
continue
|
||||
|
||||
var instance: Deck = get_group_instance(group_id, instance_id)
|
||||
instance.emit_group_signals = false
|
||||
instance.get_node(node_id).position = new_position.duplicate()
|
||||
instance.emit_group_signals = true
|
||||
|
||||
#endregion
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
class_name DeckNode
|
||||
## A node in a [Deck].
|
||||
##
|
||||
|
@ -8,7 +11,7 @@ class_name DeckNode
|
|||
var name: String
|
||||
|
||||
## A map of outgoing connections from this node, in the format[br]
|
||||
## [code]Dictionary[int -> output port, Array[Dictionary[String -> DeckNode#_id, int -> input port]]][/code]
|
||||
## [code]Dictionary[int -> output port, Dictionary[String -> DeckNode#_id, Array[int -> input port]]][/code]
|
||||
var outgoing_connections: Dictionary
|
||||
## A map of incoming connections to this node, in the format[br]
|
||||
## [code]Dictionary[int -> input port, [Dictionary[String -> DeckNode#_id, int -> output port]][/code]
|
||||
|
@ -41,6 +44,10 @@ var props_to_serialize: Array[StringName]
|
|||
## Only used by renderers.
|
||||
var position: Dictionary = {"x": 0.0, "y": 0.0}
|
||||
|
||||
## If [code]true[/code], the user can delete this node by normal means.
|
||||
## The parent [Deck] can still delete the node by other means.
|
||||
var user_can_delete: bool = true
|
||||
|
||||
enum PortType{
|
||||
INPUT, ## Input port type (slot on the left).
|
||||
OUTPUT, ## Output port type (slot on the right).
|
||||
|
@ -64,6 +71,10 @@ signal incoming_connection_added(from_port: int)
|
|||
## Emitted when a connection to this node has been removed.
|
||||
signal incoming_connection_removed(from_port: int)
|
||||
|
||||
signal port_value_updated(port_idx: int, new_value: Variant)
|
||||
|
||||
signal renamed(new_name: String)
|
||||
|
||||
|
||||
## Add an input port to this node. Usually only used at initialization.
|
||||
func add_input_port(type: DeckType.Types, label: String, descriptor: String = "") -> void:
|
||||
|
@ -85,10 +96,15 @@ func add_port(type: DeckType.Types, label: String, port_type: PortType, index_of
|
|||
var port := Port.new(type, label, ports.size(), port_type, index_of_type, descriptor)
|
||||
ports.append(port)
|
||||
port_added.emit(ports.size() - 1)
|
||||
port.value_updated.connect(
|
||||
func(new_value: Variant) -> void:
|
||||
port_value_updated.emit(port.index, new_value)
|
||||
)
|
||||
ports_updated.emit()
|
||||
|
||||
|
||||
## Remove a port from this node.
|
||||
## @deprecated
|
||||
func remove_port(port_idx: int) -> void:
|
||||
outgoing_connections.erase(port_idx)
|
||||
incoming_connections.erase(port_idx)
|
||||
|
@ -101,15 +117,13 @@ func send(from_output_port: int, data: Variant, extra_data: Array = []) -> void:
|
|||
if outgoing_connections.get(from_output_port) == null:
|
||||
return
|
||||
|
||||
for connection in outgoing_connections[from_output_port]:
|
||||
connection = connection as Dictionary
|
||||
# key is node uuid
|
||||
# value is input port on destination node
|
||||
for node in connection:
|
||||
get_node(node)._receive(connection[node], data, extra_data)
|
||||
for node: String in outgoing_connections[from_output_port]:
|
||||
for input_port: int in outgoing_connections[from_output_port][node]:
|
||||
get_node(node)._receive(input_port, data, extra_data)
|
||||
|
||||
|
||||
## Virtual function that's called when this node receives data from another node's [method send] call.
|
||||
@warning_ignore("unused_parameter")
|
||||
func _receive(to_input_port: int, data: Variant, extra_data: Array = []) -> void:
|
||||
pass
|
||||
|
||||
|
@ -117,9 +131,11 @@ func _receive(to_input_port: int, data: Variant, extra_data: Array = []) -> void
|
|||
## Add a connection from the output port at [param from_port] to [param to_node]'s input port
|
||||
## at [param to_port].
|
||||
func add_outgoing_connection(from_port: int, to_node: String, to_port: int) -> void:
|
||||
var port_connections: Array = outgoing_connections.get(from_port, [])
|
||||
port_connections.append({to_node: to_port})
|
||||
outgoing_connections[from_port] = port_connections
|
||||
var inner: Dictionary = outgoing_connections.get(from_port, {}) as Dictionary
|
||||
var inner_ports: Array = inner.get(to_node, []) as Array
|
||||
inner_ports.append(to_port)
|
||||
inner[to_node] = inner_ports
|
||||
outgoing_connections[from_port] = inner
|
||||
get_node(to_node).add_incoming_connection(to_port, _id, from_port)
|
||||
outgoing_connection_added.emit(from_port)
|
||||
|
||||
|
@ -144,33 +160,38 @@ func request_value(on_port: int) -> Variant:
|
|||
return node._value_request(connection.values()[0])
|
||||
|
||||
|
||||
## Asynchronous version of [method request_value].
|
||||
func request_value_async(on_port: int) -> Variant:
|
||||
if !incoming_connections.has(on_port):
|
||||
return null
|
||||
|
||||
var connection: Dictionary = incoming_connections[on_port]
|
||||
var node := get_node(connection.keys()[0])
|
||||
return await node._value_request(connection.values()[0])
|
||||
|
||||
|
||||
## Virtual function that's called when this node has been requested a value from the output port
|
||||
## at [param from_port].
|
||||
@warning_ignore("unused_parameter")
|
||||
func _value_request(from_port: int) -> Variant:
|
||||
return null
|
||||
|
||||
|
||||
## Remove an outgoing connection from this node.
|
||||
## Does [b]not[/b] remove the other node's incoming connection equivalent.
|
||||
func remove_outgoing_connection(from_port: int, connection_hash: int) -> void:
|
||||
var port_connections: Array = (outgoing_connections.get(from_port, []) as Array).duplicate(true)
|
||||
if port_connections.is_empty():
|
||||
func remove_outgoing_connection(from_port: int, to_node: String, to_port: int) -> void:
|
||||
var connections: Dictionary = outgoing_connections.get(from_port, {}) as Dictionary
|
||||
if connections.is_empty():
|
||||
return
|
||||
|
||||
var incoming_connection := {}
|
||||
var inner_ports: Array = connections.get(to_node, []) as Array
|
||||
inner_ports.erase(to_port)
|
||||
|
||||
var to_remove: int = -1
|
||||
for i in port_connections.size():
|
||||
if port_connections[i].hash() == connection_hash:
|
||||
if inner_ports.is_empty():
|
||||
(outgoing_connections[from_port] as Dictionary).erase(to_node)
|
||||
if (outgoing_connections[from_port] as Dictionary).is_empty():
|
||||
outgoing_connections.erase(from_port)
|
||||
|
||||
to_remove = i
|
||||
|
||||
if to_remove == -1:
|
||||
print('nothing to remove')
|
||||
return
|
||||
|
||||
port_connections.remove_at(to_remove)
|
||||
outgoing_connections[from_port] = port_connections
|
||||
outgoing_connection_removed.emit(from_port)
|
||||
|
||||
|
||||
|
@ -181,6 +202,16 @@ func remove_incoming_connection(to_port: int) -> void:
|
|||
incoming_connection_removed.emit(to_port)
|
||||
|
||||
|
||||
func rename(new_name: String) -> void:
|
||||
name = new_name
|
||||
renamed.emit(new_name)
|
||||
|
||||
|
||||
@warning_ignore("unused_parameter")
|
||||
func _event_received(event_name: StringName, event_data: Dictionary = {}) -> void:
|
||||
pass
|
||||
|
||||
|
||||
## Returns a list of all input ports.
|
||||
func get_input_ports() -> Array[Port]:
|
||||
return ports.filter(
|
||||
|
@ -254,17 +285,34 @@ func _post_load() -> void:
|
|||
pass
|
||||
|
||||
|
||||
## A helper function to get a value on an input port. Returns the best match in the following
|
||||
## order of priority:[br]
|
||||
## 1. The direct result of [method request value]. [br]
|
||||
## 2. The result of [method Port.value_callback]. [br]
|
||||
## 3. The input [Port] at index [param input_port]'s stored [member Port.value]. [br]
|
||||
## 4. [code]null[/code].
|
||||
func resolve_input_port_value(input_port: int) -> Variant:
|
||||
if request_value(input_port) != null:
|
||||
return request_value(input_port)
|
||||
elif get_input_ports()[input_port].value_callback.get_object() && get_input_ports()[input_port].value_callback.call() != null:
|
||||
return get_input_ports()[input_port].value_callback.call()
|
||||
elif get_input_ports()[input_port].value != null:
|
||||
return get_input_ports()[input_port].value
|
||||
else:
|
||||
return null
|
||||
|
||||
|
||||
## Returns a [Dictionary] representation of this node.
|
||||
func to_dict(with_meta: bool = true) -> Dictionary:
|
||||
var d := {
|
||||
"_id": _id,
|
||||
"name": name,
|
||||
"outgoing_connections": outgoing_connections,
|
||||
"incoming_connections": incoming_connections,
|
||||
"outgoing_connections": outgoing_connections.duplicate(true),
|
||||
"incoming_connections": incoming_connections.duplicate(true),
|
||||
"props": {},
|
||||
"node_type": node_type,
|
||||
"port_values": [],
|
||||
"position": position,
|
||||
"position": position.duplicate(),
|
||||
}
|
||||
|
||||
for prop in props_to_serialize:
|
||||
|
@ -282,6 +330,48 @@ func to_dict(with_meta: bool = true) -> Dictionary:
|
|||
return d
|
||||
|
||||
|
||||
static func from_dict(data: Dictionary) -> DeckNode:
|
||||
var node := NodeDB.instance_node(data.node_type)
|
||||
#node._id = data._id
|
||||
node.name = data.name
|
||||
node.position = data.position
|
||||
|
||||
for prop in data.props:
|
||||
node.set(prop, data.props[prop])
|
||||
|
||||
node._pre_connection()
|
||||
|
||||
for from_port in data.outgoing_connections:
|
||||
var connection_data: Dictionary = data.outgoing_connections[from_port]
|
||||
node.outgoing_connections[int(from_port)] = {}
|
||||
for to_node in connection_data:
|
||||
var input_ports: Array = connection_data[to_node]
|
||||
node.outgoing_connections[int(from_port)][to_node] = []
|
||||
for to_input_port in input_ports:
|
||||
node.outgoing_connections[int(from_port)][to_node].append(int(to_input_port))
|
||||
|
||||
for to_port in data.incoming_connections:
|
||||
var connection_data = data.incoming_connections[to_port]
|
||||
for connection in connection_data:
|
||||
connection_data[connection] = int(connection_data[connection])
|
||||
node.incoming_connections[int(to_port)] = connection_data
|
||||
|
||||
for i in node.ports.size():
|
||||
var port_value: Variant
|
||||
if (data.port_values as Array).size() <= i:
|
||||
port_value = null
|
||||
else:
|
||||
port_value = (data.port_values as Array)[i]
|
||||
node.ports[i].value = port_value
|
||||
|
||||
for key in data.meta:
|
||||
node.set_meta(key, str_to_var(data.meta[key]))
|
||||
|
||||
node._post_load()
|
||||
|
||||
return node
|
||||
|
||||
|
||||
## Returns the node's [member position] as a [Vector2].
|
||||
func position_as_vector2() -> Vector2:
|
||||
return Vector2(position.x, position.y)
|
||||
|
|
42
classes/deck/logger.gd
Normal file
42
classes/deck/logger.gd
Normal file
|
@ -0,0 +1,42 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
class_name Logger
|
||||
|
||||
enum LogType {
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
enum LogCategory {
|
||||
NODE,
|
||||
DECK,
|
||||
SYSTEM,
|
||||
RENDERER,
|
||||
}
|
||||
|
||||
signal log_message(text: String, type: LogType, category: LogCategory)
|
||||
|
||||
|
||||
func log_node(text: Variant, type: LogType = LogType.INFO) -> void:
|
||||
self.log(str(text), type, LogCategory.NODE)
|
||||
|
||||
|
||||
func log_deck(text: Variant, type: LogType = LogType.INFO) -> void:
|
||||
self.log(str(text), type, LogCategory.DECK)
|
||||
|
||||
|
||||
func log_system(text: Variant, type: LogType = LogType.INFO) -> void:
|
||||
self.log(str(text), type, LogCategory.SYSTEM)
|
||||
|
||||
|
||||
func log_renderer(text: Variant, type: LogType = LogType.INFO) -> void:
|
||||
self.log(str(text), type, LogCategory.RENDERER)
|
||||
|
||||
|
||||
func log(text: String, type: LogType, category: LogCategory) -> void:
|
||||
log_message.emit(text, type, category)
|
||||
|
||||
if OS.has_feature("editor"):
|
||||
prints(LogType.keys()[type].capitalize(), LogCategory.keys()[category].capitalize(), text)
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends Node
|
||||
class_name NodeDB_
|
||||
|
||||
|
@ -29,7 +32,7 @@ func _init() -> void:
|
|||
dir.list_dir_begin()
|
||||
var current_file := dir.get_next()
|
||||
while current_file != "":
|
||||
print(current_file)
|
||||
#print(current_file)
|
||||
if current_file.ends_with(".gd"):
|
||||
var script_path := BASE_NODE_PATH.path_join(current_file)
|
||||
var node: DeckNode = load(script_path).new() as DeckNode
|
||||
|
@ -74,12 +77,12 @@ func save_node_index() -> void:
|
|||
func load_node_index() -> bool:
|
||||
var f := FileAccess.open(NODE_INDEX_CACHE_PATH, FileAccess.READ)
|
||||
if f == null:
|
||||
print("node index file does not exist")
|
||||
DeckHolder.logger.log_system("node index file does not exist", Logger.LogType.ERROR)
|
||||
return false
|
||||
|
||||
var data: Dictionary = JSON.parse_string(f.get_as_text()) as Dictionary
|
||||
if data.is_empty():
|
||||
print("node index file exists, but is empty")
|
||||
DeckHolder.logger.log_system("node index file exists, but is empty", Logger.LogType.ERROR)
|
||||
return false
|
||||
|
||||
for node_type in data:
|
||||
|
@ -87,7 +90,7 @@ func load_node_index() -> bool:
|
|||
var nd := NodeDescriptor.from_dictionary(nd_dict)
|
||||
nodes[node_type] = nd
|
||||
|
||||
print("node index file exists, loaded")
|
||||
DeckHolder.logger.log_system("node index file exists, loaded")
|
||||
return true
|
||||
|
||||
## Sets a specific [member DeckNode.node_type] to be a "favorite" for use in
|
||||
|
|
24
classes/deck/nodes/bool_constant.gd
Normal file
24
classes/deck/nodes/bool_constant.gd
Normal file
|
@ -0,0 +1,24 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Bool Constant"
|
||||
node_type = name.to_snake_case()
|
||||
category = "general"
|
||||
description = "A checkbox."
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.BOOL,
|
||||
"Value",
|
||||
"checkbox"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(_from_port: int) -> Variant:
|
||||
if ports[0].value_callback.get_object():
|
||||
return ports[0].value_callback.call()
|
||||
else:
|
||||
return ports[0].value
|
|
@ -1,10 +1,13 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Button"
|
||||
node_type = "button"
|
||||
description = "a button"
|
||||
description = "A button to trigger certain nodes that have a trigger input."
|
||||
category = "general"
|
||||
|
||||
add_output_port(
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var thread : Thread
|
||||
|
@ -5,17 +8,17 @@ var thread : Thread
|
|||
func _init():
|
||||
name = "Delay"
|
||||
node_type = name.to_snake_case()
|
||||
description = "A Node that passes through the input after the set time."
|
||||
description = "A node that passes through its' input after the set time."
|
||||
category = "general"
|
||||
|
||||
add_output_port(DeckType.Types.STRING, "Value", "")
|
||||
add_output_port(DeckType.Types.ANY, "Value")
|
||||
|
||||
add_input_port(DeckType.Types.NUMERIC, "Delay Time", "field")
|
||||
add_input_port(DeckType.Types.NUMERIC, "Value", "field")
|
||||
add_input_port(DeckType.Types.ANY, "Value", "field")
|
||||
|
||||
|
||||
|
||||
func _receive(to_input_port : int, data: Variant, extra_data: Array = []) -> void:
|
||||
func _receive(_to_input_port : int, data: Variant, _extra_data: Array = []) -> void:
|
||||
|
||||
thread = Thread.new()
|
||||
thread.start(handle_delay.bind(data))
|
||||
|
@ -30,7 +33,7 @@ func handle_delay(data):
|
|||
pass
|
||||
|
||||
|
||||
print("Delay over")
|
||||
#print("Delay over")
|
||||
send(0, data)
|
||||
|
||||
|
||||
|
|
39
classes/deck/nodes/dictionary_get_key.gd
Normal file
39
classes/deck/nodes/dictionary_get_key.gd
Normal file
|
@ -0,0 +1,39 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Get Dictionary Key"
|
||||
node_type = "dictionary_get_key"
|
||||
description = "Returns the value of a key from a dictionary input, if it exists, or null otherwise."
|
||||
category = "general"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Dictionary"
|
||||
)
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.STRING,
|
||||
"Key",
|
||||
"field"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.ANY,
|
||||
"Value"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(_on_port: int) -> Variant:
|
||||
var d = request_value(0)
|
||||
if d == null:
|
||||
return null
|
||||
|
||||
var key = resolve_input_port_value(1)
|
||||
if key == null:
|
||||
return null
|
||||
|
||||
return d.get(key)
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var expr = Expression.new()
|
||||
|
@ -5,30 +8,43 @@ var expr = Expression.new()
|
|||
func _init():
|
||||
name = "Expression"
|
||||
node_type = name.to_snake_case()
|
||||
description = "A Node holding a block of executable GDScript code."
|
||||
description = "A node returning the result of a mathematical expression."
|
||||
category = "general"
|
||||
|
||||
props_to_serialize = []
|
||||
|
||||
add_output_port(DeckType.Types.ANY, "Expression Text", "codeblock")
|
||||
# TODO: order of ports is changed
|
||||
# due to https://github.com/godotengine/godot/issues/85558
|
||||
# when it's fixed, switch it back
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Expression Input"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.ANY,
|
||||
"Expression Text",
|
||||
"codeblock"
|
||||
)
|
||||
|
||||
|
||||
|
||||
func _value_request(from_port : int) -> Variant:
|
||||
func _value_request(_from_port : int) -> Variant:
|
||||
|
||||
var text = get_output_ports()[0].value_callback.call()
|
||||
|
||||
var err = expr.parse(text)
|
||||
var err = expr.parse(text, ["deck_var", "input"])
|
||||
if err != OK:
|
||||
|
||||
DeckHolder.logger.log_node("Expression parse failed: %s" % err, Logger.LogType.ERROR)
|
||||
printerr(err)
|
||||
return null
|
||||
|
||||
|
||||
var res = expr.execute()
|
||||
var res = expr.execute([_belonging_to.variable_stack, request_value(0)])
|
||||
if expr.has_execute_failed():
|
||||
|
||||
printerr("Expression Execution Failed: ", text)
|
||||
DeckHolder.logger.log_node("Expression Execution Failed: %s" % text, Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Get Deck Var"
|
||||
node_type = name.to_snake_case()
|
||||
description = "retrieve a deck variable"
|
||||
description = "Retrieve a deck variable."
|
||||
category = "general"
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.STRING,
|
||||
DeckType.Types.ANY,
|
||||
"Variable",
|
||||
"field"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(from_port: int) -> Variant:
|
||||
func _value_request(_from_port: int) -> Variant:
|
||||
var key = ports[0].value_callback.call()
|
||||
return _belonging_to.variable_stack[key]
|
||||
return _belonging_to.variable_stack.get(key)
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var output_count: int:
|
||||
get:
|
||||
return get_all_ports().size() - 1
|
||||
|
||||
var group_node: DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Group input"
|
||||
node_type = "group_input"
|
||||
props_to_serialize = [&"output_count"]
|
||||
appears_in_search = false
|
||||
user_can_delete = false
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.ANY,
|
||||
|
@ -31,11 +37,14 @@ func _on_outgoing_connection_added(port_idx: int) -> void:
|
|||
|
||||
func _on_outgoing_connection_removed(port_idx: int) -> void:
|
||||
var last_connected_port := 0
|
||||
#for port: int in outgoing_connections.keys().slice(1):
|
||||
#if !(outgoing_connections[port] as Array).is_empty():
|
||||
#last_connected_port = port
|
||||
for port: int in outgoing_connections.keys().slice(1):
|
||||
if !(outgoing_connections[port] as Array).is_empty():
|
||||
if !(outgoing_connections.get(port, {}) as Dictionary).is_empty():
|
||||
last_connected_port = port
|
||||
|
||||
prints("l:", last_connected_port, "p:", port_idx)
|
||||
#prints("l:", last_connected_port, "p:", port_idx)
|
||||
|
||||
if port_idx < last_connected_port:
|
||||
return
|
||||
|
@ -68,5 +77,4 @@ func _post_load() -> void:
|
|||
|
||||
|
||||
func _value_request(from_port: int) -> Variant:
|
||||
var group_node := _belonging_to._belonging_to.get_node(_belonging_to.group_node)
|
||||
return group_node.request_value(group_node.get_input_ports()[from_port].index_of_type)
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var group_id: String
|
||||
var group_instance_id: String
|
||||
var input_node: DeckNode
|
||||
var output_node: DeckNode
|
||||
|
||||
var input_node_id: String
|
||||
var output_node_id: String
|
||||
|
||||
var extra_ports: Array
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Group"
|
||||
node_type = "group_node"
|
||||
props_to_serialize = [&"group_id", &"extra_ports"]
|
||||
props_to_serialize = [&"group_id", &"group_instance_id", &"extra_ports", &"input_node_id", &"output_node_id"]
|
||||
appears_in_search = false
|
||||
|
||||
|
||||
func _pre_connection() -> void:
|
||||
for port_type: PortType in extra_ports:
|
||||
var index_of_type: int
|
||||
match port_type:
|
||||
PortType.OUTPUT:
|
||||
add_output_port(DeckType.Types.ANY, "Output %s" % get_output_ports().size())
|
||||
|
@ -25,10 +31,16 @@ func _pre_connection() -> void:
|
|||
|
||||
|
||||
func init_io() -> void:
|
||||
var group: Deck = _belonging_to.groups.get(group_id) as Deck
|
||||
#var group: Deck = _belonging_to.groups.get(group_id) as Deck
|
||||
var group := DeckHolder.get_group_instance(group_id, group_instance_id)
|
||||
if !group:
|
||||
return
|
||||
|
||||
if input_node && input_node.ports_updated.is_connected(recalculate_ports):
|
||||
input_node.ports_updated.disconnect(recalculate_ports)
|
||||
if output_node && output_node.ports_updated.is_connected(recalculate_ports):
|
||||
output_node.ports_updated.disconnect(recalculate_ports)
|
||||
|
||||
input_node = group.get_node(group.group_input_node)
|
||||
output_node = group.get_node(group.group_output_node)
|
||||
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var input_count: int:
|
||||
get:
|
||||
return get_all_ports().size() - 1
|
||||
|
||||
var group_node: DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Group output"
|
||||
node_type = "group_output"
|
||||
props_to_serialize = [&"input_count"]
|
||||
appears_in_search = false
|
||||
user_can_delete = false
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.ANY,
|
||||
|
@ -36,7 +42,7 @@ func _on_incoming_connection_removed(port_idx: int) -> void:
|
|||
if !(incoming_connections[port] as Dictionary).is_empty():
|
||||
last_connected_port = port
|
||||
|
||||
prints("l:", last_connected_port, "p:", port_idx)
|
||||
#prints("l:", last_connected_port, "p:", port_idx)
|
||||
|
||||
if port_idx < last_connected_port:
|
||||
return
|
||||
|
@ -69,5 +75,4 @@ func _post_load() -> void:
|
|||
|
||||
|
||||
func _receive(to_input_port: int, data: Variant, extra_data: Array = []) -> void:
|
||||
var group_node := _belonging_to._belonging_to.get_node(_belonging_to.group_node)
|
||||
group_node.send(group_node.get_output_ports()[to_input_port].index_of_type, data, extra_data)
|
||||
|
|
38
classes/deck/nodes/if_true.gd
Normal file
38
classes/deck/nodes/if_true.gd
Normal file
|
@ -0,0 +1,38 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Pass If True"
|
||||
node_type = "if_true"
|
||||
description = "Pass input if and only if the condition input is true."
|
||||
category = "general"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.BOOL,
|
||||
"Condition",
|
||||
"checkbox"
|
||||
)
|
||||
add_input_port(
|
||||
DeckType.Types.ANY,
|
||||
"Input"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.ANY,
|
||||
"Output"
|
||||
)
|
||||
|
||||
|
||||
func _receive(to_input_port: int, data: Variant, extra_data: Array = []) -> void:
|
||||
if to_input_port != 1:
|
||||
return
|
||||
|
||||
if resolve_input_port_value(0):
|
||||
send(0, data, extra_data)
|
||||
elif ports[0].value:
|
||||
send(0, data, extra_data)
|
||||
|
||||
|
46
classes/deck/nodes/obs_decompose_transform.gd
Normal file
46
classes/deck/nodes/obs_decompose_transform.gd
Normal file
|
@ -0,0 +1,46 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Decompose OBS Transform"
|
||||
node_type = "obs_decompose_transform"
|
||||
description = "Splits an OBS transform from one object into multiple outputs."
|
||||
category = "obs"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Transform"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.NUMERIC,
|
||||
"Rotation"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.NUMERIC,
|
||||
"Position X"
|
||||
)
|
||||
add_output_port(
|
||||
DeckType.Types.NUMERIC,
|
||||
"Position Y"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(on_port: int) -> Variant:
|
||||
var t = request_value(0)
|
||||
if t == null:
|
||||
return null
|
||||
|
||||
match on_port:
|
||||
0:
|
||||
return (t as Dictionary).get("rotation")
|
||||
1:
|
||||
return (t as Dictionary).get("position_x")
|
||||
2:
|
||||
return (t as Dictionary).get("position_y")
|
||||
_:
|
||||
return null
|
53
classes/deck/nodes/obs_scene_list.gd
Normal file
53
classes/deck/nodes/obs_scene_list.gd
Normal file
|
@ -0,0 +1,53 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var noobs: NoOBSWS
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Scene Selector"
|
||||
node_type = "obs_scene_list"
|
||||
description = ""
|
||||
category = "obs"
|
||||
|
||||
props_to_serialize = []
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.STRING,
|
||||
"Select a scene",
|
||||
"singlechoice"
|
||||
)
|
||||
|
||||
add_virtual_port(
|
||||
DeckType.Types.BOOL,
|
||||
"Refresh",
|
||||
"button"
|
||||
)
|
||||
|
||||
|
||||
func _receive(on_virtual_port: int, _data: Variant, _extra_data: Array = []) -> void:
|
||||
if on_virtual_port != 0:
|
||||
return
|
||||
|
||||
if noobs == null:
|
||||
noobs = Connections.obs_websocket
|
||||
|
||||
var items: Array
|
||||
var req := noobs.make_generic_request("GetSceneList")
|
||||
await req.response_received
|
||||
items = req.message.response_data.scenes
|
||||
var new_desc := "singlechoice"
|
||||
for scene in items:
|
||||
new_desc += ":" + scene.sceneName
|
||||
new_desc.trim_suffix(":")
|
||||
get_output_ports()[0].descriptor = new_desc
|
||||
ports_updated.emit()
|
||||
|
||||
|
||||
func _value_request(_on_input_port: int) -> Variant:
|
||||
if ports[0].value != null:
|
||||
return ports[0].value
|
||||
|
||||
return null
|
78
classes/deck/nodes/obs_search_source.gd
Normal file
78
classes/deck/nodes/obs_search_source.gd
Normal file
|
@ -0,0 +1,78 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var noobs: NoOBSWS
|
||||
|
||||
var cached_scene_name: String
|
||||
var cached_source_name: String
|
||||
var cached_id # TODO: evaluate if this caching is actually needed
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Get Source ID"
|
||||
node_type = "obs_search_source"
|
||||
description = "Searches for an OBS source in a scene and returns its output."
|
||||
category = "obs"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.STRING,
|
||||
"Scene name",
|
||||
"field"
|
||||
)
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.STRING,
|
||||
"Source name",
|
||||
"field"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.NUMERIC,
|
||||
"Source ID"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(_on_output_port: int) -> Variant:
|
||||
if noobs == null:
|
||||
noobs = Connections.obs_websocket
|
||||
|
||||
var scene_name: String
|
||||
if request_value(0) != null:
|
||||
scene_name = request_value(0)
|
||||
elif get_input_ports()[0].value_callback.get_object() && get_input_ports()[0].value_callback.call() != "":
|
||||
scene_name = get_input_ports()[0].value_callback.call()
|
||||
if scene_name.is_empty():
|
||||
return null
|
||||
|
||||
var source_name: String
|
||||
if request_value(1) != null:
|
||||
source_name = request_value(1)
|
||||
elif get_input_ports()[1].value_callback.get_object() && get_input_ports()[1].value_callback.call() != "":
|
||||
source_name = get_input_ports()[1].value_callback.call()
|
||||
if source_name.is_empty():
|
||||
return null
|
||||
|
||||
if cached_scene_name == scene_name && cached_source_name == scene_name:
|
||||
return cached_id
|
||||
|
||||
cached_scene_name = scene_name
|
||||
cached_source_name = source_name
|
||||
|
||||
var req := noobs.make_generic_request(
|
||||
"GetSceneItemId",
|
||||
{
|
||||
"scene_name": scene_name,
|
||||
"source_name": source_name
|
||||
}
|
||||
)
|
||||
await req.response_received
|
||||
|
||||
var data := req.message.get_data()
|
||||
#if int(data.request_status.code) != NoOBSWS.Enums.RequestStatus.NO_ERROR:
|
||||
#return null
|
||||
cached_id = data.response_data.scene_item_id
|
||||
|
||||
return data.response_data.scene_item_id
|
||||
|
108
classes/deck/nodes/obs_set_source_transform.gd
Normal file
108
classes/deck/nodes/obs_set_source_transform.gd
Normal file
|
@ -0,0 +1,108 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var noobs: NoOBSWS
|
||||
|
||||
var lock := false # TODO: evaluate if this locking is actually needed
|
||||
|
||||
enum InputPorts{
|
||||
SCENE_NAME,
|
||||
SOURCE_ID,
|
||||
XFORM,
|
||||
SET,
|
||||
}
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Set Source Transform"
|
||||
node_type = "obs_set_source_transform"
|
||||
description = "Sets an OBS source's transform, which includes position and rotation, among other things."
|
||||
category = "obs"
|
||||
|
||||
props_to_serialize = []
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.STRING,
|
||||
"Scene name",
|
||||
"field"
|
||||
)
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.NUMERIC,
|
||||
"Source ID",
|
||||
"field"
|
||||
)
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Transform"
|
||||
)
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.BOOL,
|
||||
"Set",
|
||||
"button"
|
||||
)
|
||||
|
||||
|
||||
func _receive(to_input_port: int, _data: Variant, _extra_data: Array = []) -> void:
|
||||
#{ "scene_item_transform": { "alignment": 5, "bounds_alignment": 0, "bounds_height": 0, "bounds_type": "OBS_BOUNDS_NONE", "bounds_width": 0, "crop_bottom": 0, "crop_left": 0, "crop_right": 0, "crop_top": 0, "height": 257, "position_x": 1800, "position_y": 414, "rotation": 0, "scale_x": 1, "scale_y": 1, "source_height": 257, "source_width": 146, "width": 146 }}
|
||||
if to_input_port != 3:
|
||||
return
|
||||
|
||||
if noobs == null:
|
||||
noobs = Connections.obs_websocket
|
||||
|
||||
if lock:
|
||||
return
|
||||
|
||||
var scene_name: String
|
||||
if request_value(InputPorts.SCENE_NAME) != null:
|
||||
scene_name = request_value(InputPorts.SCENE_NAME)
|
||||
elif get_input_ports()[InputPorts.SCENE_NAME].value_callback.get_object() && get_input_ports()[InputPorts.SCENE_NAME].value_callback.call() != "":
|
||||
scene_name = get_input_ports()[InputPorts.SCENE_NAME].value_callback.call()
|
||||
|
||||
if scene_name.is_empty():
|
||||
return
|
||||
|
||||
var source_id: float
|
||||
var res_as = await request_value_async(InputPorts.SOURCE_ID)
|
||||
if res_as != null:
|
||||
source_id = res_as
|
||||
elif get_input_ports()[InputPorts.SOURCE_ID].value_callback.get_object() && get_input_ports()[InputPorts.SOURCE_ID].value_callback.call() != null:
|
||||
source_id = get_input_ports()[InputPorts.SOURCE_ID].value_callback.call()
|
||||
|
||||
var xform: Dictionary
|
||||
if request_value(InputPorts.XFORM) != null:
|
||||
xform = request_value(InputPorts.XFORM)
|
||||
elif get_input_ports()[InputPorts.XFORM].value_callback.get_object() && get_input_ports()[InputPorts.XFORM].value_callback.call() != null:
|
||||
xform = get_input_ports()[InputPorts.XFORM].value_callback.call()
|
||||
|
||||
if xform.is_empty():
|
||||
return
|
||||
|
||||
lock = true
|
||||
|
||||
#noobs.make_generic_request("SetSceneItemTransform",
|
||||
#{
|
||||
#"scene_name": scene_name,
|
||||
#"scene_item_id": source_id,
|
||||
#"scene_item_transform": xform,
|
||||
#})
|
||||
#var sleep := noobs.make_generic_request("Sleep", {"sleep_frames": 1})
|
||||
#sleep.response_received.connect(func(): lock = false)
|
||||
var b := noobs.make_batch_request(false, NoOBSWS.Enums.RequestBatchExecutionType.SERIAL_FRAME)
|
||||
b.add_request(
|
||||
"SetSceneItemTransform",
|
||||
"",
|
||||
{
|
||||
"scene_name": scene_name,
|
||||
"scene_item_id": source_id,
|
||||
"scene_item_transform": xform,
|
||||
}
|
||||
)
|
||||
b.add_request("Sleep", "", {"sleep_frames": 1})
|
||||
b.response_received.connect(func(): lock = false)
|
||||
b.send()
|
48
classes/deck/nodes/obs_switch_scene.gd
Normal file
48
classes/deck/nodes/obs_switch_scene.gd
Normal file
|
@ -0,0 +1,48 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var noobs: NoOBSWS
|
||||
|
||||
func _init() -> void:
|
||||
name = "Set Scene"
|
||||
node_type = "obs_set_scene"
|
||||
description = "Sets the current scene in OBS."
|
||||
category = "obs"
|
||||
|
||||
props_to_serialize = []
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.STRING,
|
||||
"Scene Name",
|
||||
"field"
|
||||
)
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.BOOL,
|
||||
"Switch",
|
||||
"button"
|
||||
)
|
||||
|
||||
|
||||
func _receive(on_input_port: int, _data: Variant, _extra_data: Array = []) -> void:
|
||||
if on_input_port != 1:
|
||||
return
|
||||
|
||||
if noobs == null:
|
||||
noobs = Connections.obs_websocket
|
||||
|
||||
var scene_name: String
|
||||
if request_value(0) != null:
|
||||
scene_name = request_value(0)
|
||||
elif get_input_ports()[0].value_callback.get_object() && get_input_ports()[0].value_callback.call() != "":
|
||||
scene_name = get_input_ports()[0].value_callback.call()
|
||||
|
||||
if scene_name.is_empty():
|
||||
return
|
||||
|
||||
noobs.make_generic_request(
|
||||
"SetCurrentProgramScene",
|
||||
{"scene_name": scene_name}
|
||||
)
|
31
classes/deck/nodes/obs_vector_to_position.gd
Normal file
31
classes/deck/nodes/obs_vector_to_position.gd
Normal file
|
@ -0,0 +1,31 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Vector to OBS Position"
|
||||
node_type = "obs_vector_to_position"
|
||||
description = "Transforms a Vector into a position vector accepted by OBS transform inputs."
|
||||
category = "obs"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Vector"
|
||||
)
|
||||
add_output_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Position"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(_on_port: int) -> Variant:
|
||||
var v = request_value(0)
|
||||
if !v:
|
||||
return null
|
||||
|
||||
if !(v as Dictionary).has("x") || !(v as Dictionary).has("y"):
|
||||
return null
|
||||
|
||||
return {"position_x": v.x, "position_y": v.y}
|
62
classes/deck/nodes/obs_websocket_generic_request.gd
Normal file
62
classes/deck/nodes/obs_websocket_generic_request.gd
Normal file
|
@ -0,0 +1,62 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var noobs: NoOBSWS
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "OBS WS Generic Request"
|
||||
node_type = "obs_generic_request"
|
||||
description = "Makes an OBS request and sends its result through. Use if if you really know what you're doing."
|
||||
category = "obs"
|
||||
|
||||
props_to_serialize = []
|
||||
|
||||
add_virtual_port(
|
||||
DeckType.Types.STRING,
|
||||
"Request type",
|
||||
"field"
|
||||
)
|
||||
|
||||
add_virtual_port(
|
||||
DeckType.Types.STRING,
|
||||
"Request data",
|
||||
"codeblock"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Result"
|
||||
)
|
||||
|
||||
add_virtual_port(
|
||||
DeckType.Types.BOOL,
|
||||
"Request",
|
||||
"button"
|
||||
)
|
||||
|
||||
|
||||
func _receive(on_virtual_port: int, _data: Variant, _extra_data: Array = []) -> void:
|
||||
if on_virtual_port != 2:
|
||||
return
|
||||
|
||||
if noobs == null:
|
||||
noobs = Connections.obs_websocket
|
||||
|
||||
print(get_virtual_ports()[1].value)
|
||||
var e: Dictionary = type_convert(get_virtual_ports()[1].value, TYPE_DICTIONARY)
|
||||
print(e)
|
||||
if typeof(e) != TYPE_DICTIONARY:
|
||||
return
|
||||
|
||||
var req := noobs.make_generic_request(
|
||||
get_virtual_ports()[0].value,
|
||||
str_to_var(get_virtual_ports()[1].value)
|
||||
)
|
||||
|
||||
await req.response_received
|
||||
|
||||
var d := req.message.get_data()
|
||||
send(0, d)
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var times_activated := 0
|
||||
|
@ -6,14 +9,14 @@ var times_activated := 0
|
|||
func _init() -> void:
|
||||
name = "Print"
|
||||
node_type = name.to_snake_case()
|
||||
description = "print a value"
|
||||
description = "Print a value to the console."
|
||||
|
||||
props_to_serialize = [&"times_activated"]
|
||||
category = "general"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.STRING,
|
||||
"Text to print",
|
||||
DeckType.Types.ANY,
|
||||
"Data to print",
|
||||
"field"
|
||||
)
|
||||
|
||||
|
@ -34,17 +37,18 @@ func _receive(to_input_port: int, data: Variant, extra_data: Array = []) -> void
|
|||
if to_input_port != 1:
|
||||
return
|
||||
|
||||
var data_to_print
|
||||
if request_value(0) != null:
|
||||
data_to_print = request_value(0)
|
||||
elif get_input_ports()[0].value_callback.get_object() && get_input_ports()[0].value_callback.call() != "":
|
||||
data_to_print = get_input_ports()[0].value_callback.call()
|
||||
else:
|
||||
data_to_print = data
|
||||
var data_to_print = str(resolve_input_port_value(0))
|
||||
if data_to_print == null:
|
||||
data_to_print = str(data)
|
||||
if (data_to_print as String).is_empty():
|
||||
data_to_print = "<nothing>"
|
||||
|
||||
times_activated += 1
|
||||
|
||||
# var data_to_print = input_ports[0].value_callback.call()
|
||||
print(data_to_print)
|
||||
print("extra data: ", extra_data)
|
||||
#print(data_to_print)
|
||||
#print("extra data: ", extra_data)
|
||||
DeckHolder.logger.log_node(data_to_print)
|
||||
if !extra_data.is_empty():
|
||||
DeckHolder.logger.log_node(str("Extra data: ", extra_data))
|
||||
send(0, true)
|
||||
|
|
49
classes/deck/nodes/process_node.gd
Normal file
49
classes/deck/nodes/process_node.gd
Normal file
|
@ -0,0 +1,49 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var delta: float
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Process Loop"
|
||||
node_type = name.to_snake_case()
|
||||
description = "Sends a trigger output every frame, and returns the delta value (time since last frame in seconds)."
|
||||
category = "general"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.BOOL,
|
||||
"Enabled",
|
||||
"checkbox"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.BOOL,
|
||||
"Trigger"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.NUMERIC,
|
||||
"Delta"
|
||||
)
|
||||
|
||||
|
||||
func _event_received(event_name: StringName, event_data: Dictionary = {}) -> void:
|
||||
if event_name != &"process":
|
||||
return
|
||||
|
||||
var run = resolve_input_port_value(0) == true
|
||||
|
||||
if !run:
|
||||
return
|
||||
|
||||
delta = event_data.delta
|
||||
send(0, true)
|
||||
|
||||
|
||||
func _value_request(on_output_port: int) -> Variant:
|
||||
if on_output_port != 1:
|
||||
return null
|
||||
|
||||
return delta
|
|
@ -1,10 +1,13 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Set Deck Var"
|
||||
node_type = name.to_snake_case()
|
||||
description = "set deck variable"
|
||||
description = "Set a deck variable on trigger."
|
||||
category = "general"
|
||||
|
||||
add_input_port(
|
||||
|
@ -32,12 +35,12 @@ func _init() -> void:
|
|||
)
|
||||
|
||||
|
||||
func _receive(to_input_port: int, data: Variant, extra_data: Array = []) -> void:
|
||||
func _receive(to_input_port: int, _data: Variant, _extra_data: Array = []) -> void:
|
||||
if to_input_port != 2:
|
||||
return
|
||||
|
||||
var var_name: String = get_value_for_port(0, data)
|
||||
var var_value: Variant = get_value_for_port(1, data)
|
||||
var var_name: String = resolve_input_port_value(0)
|
||||
var var_value: Variant = resolve_input_port_value(1)
|
||||
|
||||
_belonging_to.variable_stack[var_name] = var_value
|
||||
|
||||
|
@ -45,10 +48,11 @@ func _receive(to_input_port: int, data: Variant, extra_data: Array = []) -> void
|
|||
|
||||
# this can probably go into DeckNode with a different name that makes it clear
|
||||
# that it prioritizes call-time resolution
|
||||
func get_value_for_port(port: int, data: Variant) -> Variant:
|
||||
# EDIT: done, see DeckNode#resolve_input_port_value
|
||||
func get_value_for_port(port: int) -> Variant:
|
||||
if request_value(port) != null:
|
||||
return request_value(port)
|
||||
elif ports[port].value_callback.call() != "":
|
||||
return ports[port].value_callback.call()
|
||||
elif get_input_ports()[port].value_callback.get_object() && get_input_ports()[port].value_callback.call() != null:
|
||||
return get_input_ports()[port].value_callback.call()
|
||||
else:
|
||||
return data
|
||||
return null
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
|
@ -5,6 +8,7 @@ func _init() -> void:
|
|||
node_type = "string_constant"
|
||||
name = "String Constant"
|
||||
category = "general"
|
||||
description = "A String field."
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.STRING,
|
||||
|
@ -12,7 +16,7 @@ func _init() -> void:
|
|||
"field"
|
||||
)
|
||||
|
||||
func _value_request(from_port: int) -> Variant:
|
||||
func _value_request(_from_port: int) -> Variant:
|
||||
if ports[0].value_callback.get_object():
|
||||
return ports[0].value_callback.call()
|
||||
else:
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
|
@ -26,7 +29,7 @@ func _init() -> void:
|
|||
add_input_port(DeckType.Types.BOOL, "Send", "button")
|
||||
|
||||
|
||||
func _receive(to_input_port: int, data: Variant, extra_data: Array = []) -> void:
|
||||
func _receive(to_input_port: int, data: Variant, _extra_data: Array = []) -> void:
|
||||
if to_input_port == 6:
|
||||
send(0, false)
|
||||
send(1, 1.0)
|
||||
|
|
56
classes/deck/nodes/twitch_chat_received.gd
Normal file
56
classes/deck/nodes/twitch_chat_received.gd
Normal file
|
@ -0,0 +1,56 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
var username := ""
|
||||
var message := ""
|
||||
var channel := ""
|
||||
var tags := {}
|
||||
|
||||
|
||||
func _init():
|
||||
name = "Twitch Chat Received"
|
||||
node_type = "twitch_chat_received"
|
||||
description = "Receives Twitch chat events from a Twitch connection."
|
||||
category = "twitch"
|
||||
|
||||
add_output_port(DeckType.Types.STRING, "Username")
|
||||
add_output_port(DeckType.Types.STRING, "Message")
|
||||
add_output_port(DeckType.Types.STRING, "Channel")
|
||||
add_output_port(DeckType.Types.DICTIONARY, "Tags")
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.BOOL,
|
||||
"On receive"
|
||||
)
|
||||
|
||||
|
||||
|
||||
func _event_received(event_name : StringName, event_data : Dictionary = {}):
|
||||
|
||||
if event_name != &"twitch_chat":
|
||||
|
||||
return
|
||||
|
||||
|
||||
username = event_data.username
|
||||
message = event_data.message
|
||||
channel = event_data.channel
|
||||
tags = event_data
|
||||
|
||||
send(4, true)
|
||||
|
||||
|
||||
func _value_request(on_port: int) -> Variant:
|
||||
match on_port:
|
||||
0:
|
||||
return username
|
||||
1:
|
||||
return message
|
||||
2:
|
||||
return channel
|
||||
3:
|
||||
return tags
|
||||
_:
|
||||
return null
|
52
classes/deck/nodes/twitch_send_chat.gd
Normal file
52
classes/deck/nodes/twitch_send_chat.gd
Normal file
|
@ -0,0 +1,52 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init():
|
||||
name = "Twitch Send Chat"
|
||||
node_type = "twitch_send_chat"
|
||||
description = "Sends a message to a Twitch chat."
|
||||
category = "twitch"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.STRING,
|
||||
"Message",
|
||||
"field"
|
||||
)
|
||||
add_input_port(
|
||||
DeckType.Types.STRING,
|
||||
"Channel",
|
||||
"field"
|
||||
)
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.BOOL,
|
||||
"Send",
|
||||
"button"
|
||||
)
|
||||
|
||||
|
||||
|
||||
func _receive(to_input_port, data: Variant, extra_data: Array = []):
|
||||
|
||||
if to_input_port != 2:
|
||||
|
||||
return
|
||||
|
||||
|
||||
var msg = resolve_input_port_value(0)
|
||||
var channel = resolve_input_port_value(1)
|
||||
|
||||
if channel == null:
|
||||
|
||||
channel = ""
|
||||
|
||||
if msg == null:
|
||||
|
||||
return
|
||||
|
||||
|
||||
Connections.twitch.send_chat(msg, channel)
|
||||
|
47
classes/deck/nodes/vector_add.gd
Normal file
47
classes/deck/nodes/vector_add.gd
Normal file
|
@ -0,0 +1,47 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Add Vectors"
|
||||
node_type = "vector_add"
|
||||
description = "Adds two 2D vectors."
|
||||
category = "math"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Vector A"
|
||||
)
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Vector B"
|
||||
)
|
||||
add_output_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Result"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(_on_port: int) -> Variant:
|
||||
var va = request_value(0)
|
||||
var vb = request_value(1)
|
||||
|
||||
if !va || !vb:
|
||||
DeckHolder.logger.log_node("Vector Add: one of the vectors is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
if !(va as Dictionary).has("x") || !(va as Dictionary).has("y"):
|
||||
DeckHolder.logger.log_node("Vector Add: one of the vectors is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
if !(vb as Dictionary).has("x") || !(vb as Dictionary).has("y"):
|
||||
DeckHolder.logger.log_node("Vector Add: one of the vectors is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
var res := {}
|
||||
res["x"] = va["x"] + vb["x"]
|
||||
res["y"] = va["y"] + vb["y"]
|
||||
|
||||
return res
|
33
classes/deck/nodes/vector_compose.gd
Normal file
33
classes/deck/nodes/vector_compose.gd
Normal file
|
@ -0,0 +1,33 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Compose Vector"
|
||||
node_type = "vector_compose"
|
||||
description = "Returns a vector from two numeric inputs."
|
||||
category = "math"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.NUMERIC,
|
||||
"X",
|
||||
"field"
|
||||
)
|
||||
add_input_port(
|
||||
DeckType.Types.NUMERIC,
|
||||
"Y",
|
||||
"field"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Vector"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(_on_port: int) -> Dictionary:
|
||||
var x = float(resolve_input_port_value(0))
|
||||
var y = float(resolve_input_port_value(1))
|
||||
return {"x": x, "y": y}
|
41
classes/deck/nodes/vector_decompose.gd
Normal file
41
classes/deck/nodes/vector_decompose.gd
Normal file
|
@ -0,0 +1,41 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Decompose Vector"
|
||||
node_type = "vector_decompose"
|
||||
description = "Returns the X and Y components of a vector."
|
||||
category = "math"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Vector"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.NUMERIC,
|
||||
"X"
|
||||
)
|
||||
add_output_port(
|
||||
DeckType.Types.NUMERIC,
|
||||
"Y"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(on_port: int) -> Variant:
|
||||
var v = request_value(0)
|
||||
if !v:
|
||||
DeckHolder.logger.log_node("Vector Decompose: the vector is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
if !(v as Dictionary).has("x") || !(v as Dictionary).has("y"):
|
||||
DeckHolder.logger.log_node("Vector Decompose: the vector is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
if on_port == 0:
|
||||
return v.x
|
||||
else:
|
||||
return v.y
|
44
classes/deck/nodes/vector_dot.gd
Normal file
44
classes/deck/nodes/vector_dot.gd
Normal file
|
@ -0,0 +1,44 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Vector Dot Product"
|
||||
node_type = "vector_dot"
|
||||
description = "Returns the dot product of two vectors."
|
||||
category = "math"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Vector A"
|
||||
)
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Vector B"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.NUMERIC,
|
||||
"Dot"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(_on_port: int) -> Variant:
|
||||
var va = request_value(0)
|
||||
var vb = request_value(1)
|
||||
|
||||
if !va || !vb:
|
||||
DeckHolder.logger.log_node("Vector Dot: one of the vectors is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
if !(va as Dictionary).has("x") || !(va as Dictionary).has("y"):
|
||||
DeckHolder.logger.log_node("Vector Dot: one of the vectors is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
if !(vb as Dictionary).has("x") || !(vb as Dictionary).has("y"):
|
||||
DeckHolder.logger.log_node("Vector Dot: one of the vectors is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
return va.x * vb.x + va.y * vb.y
|
42
classes/deck/nodes/vector_multiply.gd
Normal file
42
classes/deck/nodes/vector_multiply.gd
Normal file
|
@ -0,0 +1,42 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Multiply Vector by Scalar"
|
||||
node_type = "vector_multiply"
|
||||
description = "Multiplies a vector by a numeric value."
|
||||
category = "math"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Vector"
|
||||
)
|
||||
add_input_port(
|
||||
DeckType.Types.NUMERIC,
|
||||
"Scalar",
|
||||
"field"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Result"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(_on_port: int) -> Variant:
|
||||
var v = request_value(0)
|
||||
|
||||
if !v:
|
||||
DeckHolder.logger.log_node("Vector Mult: the vector is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
if !(v as Dictionary).has("x") || !(v as Dictionary).has("y"):
|
||||
DeckHolder.logger.log_node("Vector Mult: the vector is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
var s = float(resolve_input_port_value(1))
|
||||
|
||||
return {"x": v.x * s, "y": v.y * s}
|
44
classes/deck/nodes/vector_normalize.gd
Normal file
44
classes/deck/nodes/vector_normalize.gd
Normal file
|
@ -0,0 +1,44 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Normalize Vector"
|
||||
node_type = "vector_normalize"
|
||||
description = "Normalizes a vector so its' length (magnitude) is exactly 1."
|
||||
category = "math"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Vector"
|
||||
)
|
||||
|
||||
add_output_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Result"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(_on_port: int) -> Variant:
|
||||
var v = request_value(0)
|
||||
if !v:
|
||||
DeckHolder.logger.log_node("Vector Normalize: the vector is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
if !(v as Dictionary).has("x") || !(v as Dictionary).has("y"):
|
||||
DeckHolder.logger.log_node("Vector Normalize: the vector is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
var l: float = (v.x ** 2.0) + (v.y ** 2.0)
|
||||
var res := {"x": v.x, "y": v.y}
|
||||
if l != 0:
|
||||
l = sqrt(l)
|
||||
res.x = res.x / l
|
||||
res.y = res.y / l
|
||||
return res
|
||||
|
||||
DeckHolder.logger.log_node("Vector Normalize: the vector is length 0. Returning null.", Logger.LogType.ERROR)
|
||||
|
||||
return null
|
47
classes/deck/nodes/vector_subtract.gd
Normal file
47
classes/deck/nodes/vector_subtract.gd
Normal file
|
@ -0,0 +1,47 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "Subtract Vectors"
|
||||
node_type = "vector_subtract"
|
||||
description = "Subtracts each component of the given vectors."
|
||||
category = "math"
|
||||
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Vector A"
|
||||
)
|
||||
add_input_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Vector B"
|
||||
)
|
||||
add_output_port(
|
||||
DeckType.Types.DICTIONARY,
|
||||
"Result"
|
||||
)
|
||||
|
||||
|
||||
func _value_request(_on_port: int) -> Variant:
|
||||
var va = request_value(0)
|
||||
var vb = request_value(1)
|
||||
|
||||
if !va || !vb:
|
||||
DeckHolder.logger.log_node("Vector Sub: one of the vectors is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
if !(va as Dictionary).has("x") || !(va as Dictionary).has("y"):
|
||||
DeckHolder.logger.log_node("Vector Sub: one of the vectors is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
if !(vb as Dictionary).has("x") || !(vb as Dictionary).has("y"):
|
||||
DeckHolder.logger.log_node("Vector Sub: one of the vectors is invalid.", Logger.LogType.ERROR)
|
||||
return null
|
||||
|
||||
var res := {}
|
||||
res["x"] = va["x"] - vb["x"]
|
||||
res["y"] = va["y"] - vb["y"]
|
||||
|
||||
return res
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
class_name Port
|
||||
## A data type representing a port of a [DeckNode].
|
||||
##
|
||||
|
@ -25,7 +28,9 @@ var index_of_type: int
|
|||
var index: int
|
||||
|
||||
## The value of this port.
|
||||
var value: Variant: set = set_value
|
||||
var value: Variant
|
||||
|
||||
signal value_updated(new_value: Variant)
|
||||
|
||||
|
||||
func _init(
|
||||
|
@ -48,7 +53,13 @@ func _init(
|
|||
|
||||
|
||||
func set_value(v: Variant) -> void:
|
||||
set_value_no_signal(v)
|
||||
value_updated.emit(value)
|
||||
|
||||
|
||||
func set_value_no_signal(v: Variant) -> void:
|
||||
if v is Callable:
|
||||
value = v.call()
|
||||
return
|
||||
|
||||
value = v
|
||||
|
|
140
classes/deck/renderer_persistence.gd
Normal file
140
classes/deck/renderer_persistence.gd
Normal file
|
@ -0,0 +1,140 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
class_name RendererPersistence
|
||||
## An interface for per-renderer persistent data.
|
||||
##
|
||||
## Allows renderers to store key/value data, split into namespaces (usually the renderer name)
|
||||
## and channels (files on a filesystem). For every namespace, a folder is created,
|
||||
## and populated with JSON files named after the channel name.
|
||||
|
||||
|
||||
const _BASE_PATH := "user://renderer_persistence/"
|
||||
const _FILE_TEMPLATE := "{namespace}/{channel}.json"
|
||||
|
||||
# Dictionary[String -> namespace, Dictionary[String -> channel, Variant]]
|
||||
static var _data: Dictionary
|
||||
|
||||
#region API
|
||||
## Initializes a namespace. Returns [code]false[/code] if the folder for the namespace didn't exist
|
||||
## before, useful for initializing defaults.
|
||||
static func init_namespace(name_space: String) -> bool:
|
||||
var n := name_space.validate_filename()
|
||||
if _data.get(n) != null:
|
||||
return true
|
||||
|
||||
var err := _create_namespace_folder(n)
|
||||
if err != OK:
|
||||
DeckHolder.logger.log_system("Could not create namespace: error %s" % err, Logger.LogType.ERROR)
|
||||
return false
|
||||
_data[name_space] = {}
|
||||
return false
|
||||
|
||||
|
||||
## Returns the corresponding value for the given [param key] in the [param channel] storage.
|
||||
## If the key does not exist, returns [param default], or [code]null[/code] if the parameter is omitted.
|
||||
static func get_value(name_space: String, channel: String, key: String, default: Variant = null) -> Variant:
|
||||
_lazy_load_channel(name_space, channel)
|
||||
var validated_name := name_space.validate_filename()
|
||||
var validated_channel := channel.validate_filename()
|
||||
|
||||
return (_data[validated_name][validated_channel] as Dictionary).get(key, default)
|
||||
|
||||
|
||||
## Sets the value in the [param channel] storage at [param key] to [param value].
|
||||
static func set_value(name_space: String, channel: String, key: String, value: Variant) -> void:
|
||||
_lazy_load_channel(name_space, channel)
|
||||
var validated_name := name_space.validate_filename()
|
||||
var validated_channel := channel.validate_filename()
|
||||
|
||||
_data[validated_name][validated_channel][key] = value
|
||||
|
||||
|
||||
## Returns the corresponding value for the given [param key] in the [param channel] storage.
|
||||
## If the key does not exist, creates it in the storage, initializes it to [param default]
|
||||
## and returns it.
|
||||
static func get_or_create(name_space: String, channel: String, key: String, default: Variant) -> Variant:
|
||||
_lazy_load_channel(name_space, channel)
|
||||
var validated_name := name_space.validate_filename()
|
||||
var validated_channel := channel.validate_filename()
|
||||
|
||||
var channel_data: Dictionary = _data[validated_name][validated_channel]
|
||||
return _get_or_create(channel_data, key, default)
|
||||
|
||||
|
||||
## Erases an entry in the store by [param key] and returns its previous value,
|
||||
## or [code]null[/code] if it didn't exist.
|
||||
static func erase(name_space: String, channel: String, key: String) -> Variant:
|
||||
_lazy_load_channel(name_space, channel)
|
||||
var validated_name := name_space.validate_filename()
|
||||
var validated_channel := channel.validate_filename()
|
||||
|
||||
var channel_data: Dictionary = _data[validated_name][validated_channel]
|
||||
var ret = channel_data.get(key)
|
||||
channel_data.erase(key)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
## Commits the [param channel] to the filesystem. If [param channel] is empty,
|
||||
## commits all channels in the [param name_space].
|
||||
static func commit(name_space: String, channel: String = "") -> void:
|
||||
if !channel.is_empty():
|
||||
_commit_channel(name_space, channel)
|
||||
else:
|
||||
for c: String in _data[name_space]:
|
||||
_commit_channel(name_space, c)
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
static func _get_or_create(d: Dictionary, key: String, default: Variant) -> Variant:
|
||||
var r = d.get(key)
|
||||
if r == null:
|
||||
r = default
|
||||
d[key] = r
|
||||
|
||||
return r
|
||||
|
||||
|
||||
static func _create_namespace_folder(name_space: String) -> Error:
|
||||
return DirAccess.make_dir_recursive_absolute(_BASE_PATH.path_join(name_space))
|
||||
|
||||
|
||||
static func _lazy_load_channel(name_space: String, channel: String) -> void:
|
||||
var validated_name := name_space.validate_filename()
|
||||
var validated_channel := channel.validate_filename()
|
||||
if !_data.has(validated_name):
|
||||
DeckHolder.logger.log_system("Namespace %s is not initialized" % name_space, Logger.LogType.ERROR)
|
||||
return
|
||||
|
||||
if (_data[validated_name] as Dictionary).has(validated_channel):
|
||||
return
|
||||
|
||||
var path := _BASE_PATH.path_join(_FILE_TEMPLATE.format(
|
||||
{
|
||||
"namespace": validated_name,
|
||||
"channel": validated_channel
|
||||
}))
|
||||
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
if f == null:
|
||||
_data[validated_name][validated_channel] = {}
|
||||
return
|
||||
|
||||
var data = JSON.parse_string(f.get_as_text())
|
||||
_data[validated_name][validated_channel] = data
|
||||
|
||||
|
||||
static func _commit_channel(name_space: String, channel: String) -> void:
|
||||
_lazy_load_channel(name_space, channel)
|
||||
var validated_name := name_space.validate_filename()
|
||||
var validated_channel := channel.validate_filename()
|
||||
|
||||
var path := _BASE_PATH.path_join(_FILE_TEMPLATE.format(
|
||||
{
|
||||
"namespace": validated_name,
|
||||
"channel": validated_channel
|
||||
}))
|
||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
f.store_string(JSON.stringify(_data[validated_name][validated_channel], "\t", false))
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
class_name SearchProvider
|
||||
|
||||
## A class facilitating the searching of nodes.
|
||||
|
@ -30,7 +33,7 @@ static var filters: Array[Filter] = [
|
|||
func(element: NodeDB.NodeDescriptor, _search_string: String, pre_strip_string: String) -> bool:
|
||||
const p := r"#c\s[\w]+"
|
||||
var r := RegEx.create_from_string(p)
|
||||
print("pre: ", pre_strip_string)
|
||||
#print("pre: ", pre_strip_string)
|
||||
var c := r.search(pre_strip_string).get_string().split("#c ", false)[0]
|
||||
|
||||
return c.is_subsequence_ofn(element.category),
|
||||
|
@ -39,7 +42,7 @@ static var filters: Array[Filter] = [
|
|||
const p := r"#c\s[\w]+"
|
||||
var r := RegEx.create_from_string(p)
|
||||
var c := r.search(search_string).get_string()
|
||||
prints("c:", c, "r:", search_string.replace(c, ""))
|
||||
#prints("c:", c, "r:", search_string.replace(c, ""))
|
||||
return search_string.replace(c, "")
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
class_name DeckType
|
||||
|
||||
enum Types{
|
||||
|
|
38
dist/logo-flattened.svg
vendored
Normal file
38
dist/logo-flattened.svg
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg id="svg1" width="300" height="300" version="1.1" viewBox="0 0 79.375 79.375" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs id="defs1">
|
||||
<linearGradient id="linearGradient6" x1="96.801" x2="140.52" y1="-6.4058" y2="18.837" gradientUnits="userSpaceOnUse">
|
||||
<stop id="stop6" stop-color="#f73a3a" offset="0"/>
|
||||
<stop id="stop5" stop-color="#f61f1f" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient47" x1="39.688" x2="39.688" y1="13.213" y2="79.375" gradientUnits="userSpaceOnUse">
|
||||
<stop id="stop46" stop-color="#414141" offset="0"/>
|
||||
<stop id="stop48" stop-color="#4b4b4b" offset=".14512"/>
|
||||
<stop id="stop49" stop-color="#4b4b4b" offset=".85488"/>
|
||||
<stop id="stop47" stop-color="#414141" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient51" x1="39.688" x2="39.688" y1="13.213" y2="-1.9855e-7" gradientUnits="userSpaceOnUse">
|
||||
<stop id="stop50" stop-color="#acacac" offset="0"/>
|
||||
<stop id="stop51" stop-color="#949494" offset="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="layer1">
|
||||
<path id="rect1" d="m0 13.213h79.375v60.162a6 6 135 0 1-6 6h-67.375a6 6 45 0 1-6-6v-60.162z" fill="url(#linearGradient47)"/>
|
||||
<g id="g6" transform="translate(-.52917 -.52917)" opacity=".070064">
|
||||
<path id="path1" transform="matrix(.80178 0 0 .80178 -51.672 33.308)" d="m140.52 18.837-43.722 25.243v-50.485z" opacity="1"/>
|
||||
<g id="g5" transform="translate(2.1167 2.1167)" opacity="1">
|
||||
<circle id="circle3" cx="79.375" cy="46.294" r="5.1498"/>
|
||||
<circle id="circle4" cx="-1.1102e-16" cy="26.055" r="5.1498"/>
|
||||
<circle id="circle5" cx="-1.1102e-16" cy="66.533" r="5.1498"/>
|
||||
</g>
|
||||
</g>
|
||||
<path id="path5" transform="matrix(.80178 0 0 .80178 -53.789 31.191)" d="m140.52 18.837-43.722 25.243v-50.485z" fill="url(#linearGradient6)"/>
|
||||
<g id="g3" fill="#fff">
|
||||
<circle id="circle10" cx="79.375" cy="46.294" r="5.1498"/>
|
||||
<circle id="circle1" cx="-1.1102e-16" cy="26.055" r="5.1498"/>
|
||||
<circle id="circle2" cx="-1.1102e-16" cy="66.533" r="5.1498"/>
|
||||
</g>
|
||||
<path id="rect45" transform="matrix(1 0 0 -1 0 13.213)" d="m0 0h79.375v7.2132a6 6 135 0 1-6 6h-67.375a6 6 45 0 1-6-6v-7.2132z" fill="url(#linearGradient51)"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
37
dist/logo-flattened.svg.import
vendored
Normal file
37
dist/logo-flattened.svg.import
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bsac4pnv4a3wv"
|
||||
path="res://.godot/imported/logo-flattened.svg-0ce0b3a9eae5db0ad39f3f1f09da3354.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://dist/logo-flattened.svg"
|
||||
dest_files=["res://.godot/imported/logo-flattened.svg-0ce0b3a9eae5db0ad39f3f1f09da3354.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
276
dist/logo.svg
vendored
Normal file
276
dist/logo.svg
vendored
Normal file
|
@ -0,0 +1,276 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="300"
|
||||
height="300"
|
||||
viewBox="0 0 79.374998 79.375"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="logo-flattened.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showguides="true"
|
||||
inkscape:zoom="2.0000002"
|
||||
inkscape:cx="125.74999"
|
||||
inkscape:cy="160.99998"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
inkscape:window-x="1440"
|
||||
inkscape:window-y="36"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false">
|
||||
<sodipodi:guide
|
||||
position="9.0983681,66.161791"
|
||||
orientation="0,-1"
|
||||
id="guide5"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="79.374998,33.080896"
|
||||
orientation="0,-1"
|
||||
id="guide6"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="23.82456,53.319914"
|
||||
orientation="0,-1"
|
||||
id="guide7"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="23.82456,12.84188"
|
||||
orientation="0,-1"
|
||||
id="guide8"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="0,15.213542"
|
||||
orientation="1,0"
|
||||
id="guide10"
|
||||
inkscape:locked="false" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs1">
|
||||
<linearGradient
|
||||
id="linearGradient49"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
style="stop-color:#acacac;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop50" />
|
||||
<stop
|
||||
style="stop-color:#949494;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop51" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient46"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
style="stop-color:#414141;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop46" />
|
||||
<stop
|
||||
style="stop-color:#4b4b4b;stop-opacity:1;"
|
||||
offset="0.1451201"
|
||||
id="stop48" />
|
||||
<stop
|
||||
style="stop-color:#4b4b4b;stop-opacity:1;"
|
||||
offset="0.85487986"
|
||||
id="stop49" />
|
||||
<stop
|
||||
style="stop-color:#414141;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop47" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
style="stop-color:#f73a3a;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop6" />
|
||||
<stop
|
||||
style="stop-color:#f61f1f;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3"
|
||||
id="linearGradient6"
|
||||
x1="96.801147"
|
||||
y1="-6.4058304"
|
||||
x2="140.52271"
|
||||
y2="18.836823"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient46"
|
||||
id="linearGradient47"
|
||||
x1="39.6875"
|
||||
y1="13.213208"
|
||||
x2="39.6875"
|
||||
y2="79.375"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient49"
|
||||
id="linearGradient51"
|
||||
x1="39.6875"
|
||||
y1="13.213208"
|
||||
x2="39.6875"
|
||||
y2="-1.9854737e-07"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB;"
|
||||
inkscape:label="Drop Shadow"
|
||||
id="filter4"
|
||||
x="-0.10978566"
|
||||
y="-0.095077169"
|
||||
width="1.2515921"
|
||||
height="1.2295275">
|
||||
<feFlood
|
||||
result="flood"
|
||||
in="SourceGraphic"
|
||||
flood-opacity="0.270588"
|
||||
flood-color="rgb(0,0,0)"
|
||||
id="feFlood3" />
|
||||
<feGaussianBlur
|
||||
result="blur"
|
||||
in="SourceGraphic"
|
||||
stdDeviation="2.000000"
|
||||
id="feGaussianBlur3" />
|
||||
<feOffset
|
||||
result="offset"
|
||||
in="blur"
|
||||
dx="1.400000"
|
||||
dy="1.987768"
|
||||
id="feOffset3" />
|
||||
<feComposite
|
||||
result="comp1"
|
||||
operator="in"
|
||||
in="flood"
|
||||
in2="offset"
|
||||
id="feComposite3" />
|
||||
<feComposite
|
||||
result="comp2"
|
||||
operator="over"
|
||||
in="SourceGraphic"
|
||||
in2="comp1"
|
||||
id="feComposite4" />
|
||||
</filter>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="rect1"
|
||||
d="m 0,13.213208 79.375,0 0,60.161789 a 6,6 135 0 1 -6,6 H 5.9999971 A 5.9999971,5.9999971 45 0 1 0,73.375 V 13.213208 Z"
|
||||
style="fill:url(#linearGradient47);fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.988688" />
|
||||
<g
|
||||
id="g6"
|
||||
style="opacity:0.0700637;fill:#000000;fill-opacity:1"
|
||||
transform="translate(-0.52916663,-0.52916663)">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.989989;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.988688"
|
||||
id="path1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="111.375"
|
||||
sodipodi:cy="18.836823"
|
||||
sodipodi:r1="29.147705"
|
||||
sodipodi:r2="14.573853"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 140.52271,18.836823 -43.721563,25.242653 0,-50.4853065 z"
|
||||
inkscape:transform-center-x="-5.8424986"
|
||||
transform="matrix(0.80177853,0,0,0.80177853,-51.671853,33.307811)" />
|
||||
<g
|
||||
id="g5"
|
||||
transform="translate(2.1166667,2.1166667)"
|
||||
style="opacity:1;fill:#000000;fill-opacity:1">
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.988688"
|
||||
id="circle3"
|
||||
cx="79.375"
|
||||
cy="46.294102"
|
||||
r="5.1497717" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.988688"
|
||||
id="circle4"
|
||||
cx="-1.110223e-16"
|
||||
cy="26.055086"
|
||||
r="5.1497717" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.988688"
|
||||
id="circle5"
|
||||
cx="-1.1102222e-16"
|
||||
cy="66.533119"
|
||||
r="5.1497717" />
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:url(#linearGradient6);fill-opacity:1;stroke:none;stroke-width:0.989989;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.988688"
|
||||
id="path5"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="111.375"
|
||||
sodipodi:cy="18.836823"
|
||||
sodipodi:r1="29.147705"
|
||||
sodipodi:r2="14.573853"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 140.52271,18.836823 -43.721563,25.242653 0,-50.4853065 z"
|
||||
inkscape:transform-center-x="-5.8424986"
|
||||
transform="matrix(0.80177853,0,0,0.80177853,-53.788521,31.191143)" />
|
||||
<g
|
||||
id="g3">
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.988688"
|
||||
id="circle10"
|
||||
cx="79.375"
|
||||
cy="46.294102"
|
||||
r="5.1497717" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.988688"
|
||||
id="circle1"
|
||||
cx="-1.110223e-16"
|
||||
cy="26.055086"
|
||||
r="5.1497717" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.988688"
|
||||
id="circle2"
|
||||
cx="-1.1102222e-16"
|
||||
cy="66.533119"
|
||||
r="5.1497717" />
|
||||
</g>
|
||||
<path
|
||||
id="rect45"
|
||||
transform="matrix(1,0,0,-1,0,13.213208)"
|
||||
d="m 0,0 h 79.375 l 0,7.2132082 a 6,6 135 0 1 -6,5.9999998 H 6.0000002 A 6.0000002,6.0000002 45 0 1 0,7.213208 V 0 Z"
|
||||
style="fill:url(#linearGradient51);fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.988688" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.8 KiB |
37
dist/logo.svg.import
vendored
Normal file
37
dist/logo.svg.import
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bt2gnexdatmuw"
|
||||
path="res://.godot/imported/logo.svg-3b682001f45a5b402a88618d2eaf767b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://dist/logo.svg"
|
||||
dest_files=["res://.godot/imported/logo.svg-3b682001f45a5b402a88618d2eaf767b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
102
export_presets.cfg
Normal file
102
export_presets.cfg
Normal file
|
@ -0,0 +1,102 @@
|
|||
[preset.0]
|
||||
|
||||
name="Linux/X11"
|
||||
platform="Linux/X11"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="dist/0.0.1/linux/StreamGraph.x86_64"
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
|
||||
[preset.0.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
binary_format/embed_pck=false
|
||||
texture_format/bptc=true
|
||||
texture_format/s3tc=true
|
||||
texture_format/etc=false
|
||||
texture_format/etc2=false
|
||||
binary_format/architecture="x86_64"
|
||||
ssh_remote_deploy/enabled=false
|
||||
ssh_remote_deploy/host="user@host_ip"
|
||||
ssh_remote_deploy/port="22"
|
||||
ssh_remote_deploy/extra_args_ssh=""
|
||||
ssh_remote_deploy/extra_args_scp=""
|
||||
ssh_remote_deploy/run_script="#!/usr/bin/env bash
|
||||
export DISPLAY=:0
|
||||
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
|
||||
\"{temp_dir}/{exe_name}\" {cmd_args}"
|
||||
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
|
||||
kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
|
||||
rm -rf \"{temp_dir}\""
|
||||
|
||||
[preset.1]
|
||||
|
||||
name="Windows Desktop"
|
||||
platform="Windows Desktop"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="dist/0.0.1/windows/StreamGraph.exe"
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
|
||||
[preset.1.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
binary_format/embed_pck=false
|
||||
texture_format/bptc=true
|
||||
texture_format/s3tc=true
|
||||
texture_format/etc=false
|
||||
texture_format/etc2=false
|
||||
binary_format/architecture="x86_64"
|
||||
codesign/enable=false
|
||||
codesign/timestamp=true
|
||||
codesign/timestamp_server_url=""
|
||||
codesign/digest_algorithm=1
|
||||
codesign/description=""
|
||||
codesign/custom_options=PackedStringArray()
|
||||
application/modify_resources=true
|
||||
application/icon=""
|
||||
application/console_wrapper_icon=""
|
||||
application/icon_interpolation=4
|
||||
application/file_version=""
|
||||
application/product_version=""
|
||||
application/company_name="Eroax & Yagich"
|
||||
application/product_name="StreamGraph"
|
||||
application/file_description=""
|
||||
application/copyright=""
|
||||
application/trademarks=""
|
||||
application/export_angle=0
|
||||
ssh_remote_deploy/enabled=false
|
||||
ssh_remote_deploy/host="user@host_ip"
|
||||
ssh_remote_deploy/port="22"
|
||||
ssh_remote_deploy/extra_args_ssh=""
|
||||
ssh_remote_deploy/extra_args_scp=""
|
||||
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
|
||||
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
|
||||
$settings = New-ScheduledTaskSettingsSet
|
||||
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
|
||||
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
|
||||
Start-ScheduledTask -TaskName godot_remote_debug
|
||||
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
|
||||
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
|
||||
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
|
||||
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
|
||||
Remove-Item -Recurse -Force '{temp_dir}'"
|
11
graph_node_renderer/about_dialog.gd
Normal file
11
graph_node_renderer/about_dialog.gd
Normal file
|
@ -0,0 +1,11 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends AcceptDialog
|
||||
class_name AboutDialog
|
||||
|
||||
@onready var version_label: Label = %VersionLabel
|
||||
|
||||
func _ready() -> void:
|
||||
var version: String = ProjectSettings.get_setting("application/config/version")
|
||||
version_label.text = "v%s" % version
|
766
graph_node_renderer/about_dialog.tscn
Normal file
766
graph_node_renderer/about_dialog.tscn
Normal file
|
@ -0,0 +1,766 @@
|
|||
[gd_scene load_steps=5 format=3 uid="uid://bu466w2w3q08c"]
|
||||
|
||||
[ext_resource type="Script" path="res://graph_node_renderer/about_dialog.gd" id="1_xn0s3"]
|
||||
[ext_resource type="Texture2D" uid="uid://bsac4pnv4a3wv" path="res://dist/logo-flattened.svg" id="2_e1chc"]
|
||||
|
||||
[sub_resource type="SystemFont" id="SystemFont_ueb25"]
|
||||
font_names = PackedStringArray("Monospace")
|
||||
subpixel_positioning = 0
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_qamh4"]
|
||||
font = SubResource("SystemFont_ueb25")
|
||||
font_size = 12
|
||||
|
||||
[node name="AboutDialog" type="AcceptDialog"]
|
||||
title = "About StreamGraph"
|
||||
initial_position = 2
|
||||
size = Vector2i(714, 466)
|
||||
script = ExtResource("1_xn0s3")
|
||||
|
||||
[node name="HBoxContainer" type="VBoxContainer" parent="."]
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = 706.0
|
||||
offset_bottom = 417.0
|
||||
theme_override_constants/separation = 15
|
||||
|
||||
[node name="VBoxContainer" type="HBoxContainer" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 40
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TextureRect" type="TextureRect" parent="HBoxContainer/VBoxContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
texture = ExtResource("2_e1chc")
|
||||
expand_mode = 3
|
||||
|
||||
[node name="Label" type="Label" parent="HBoxContainer/VBoxContainer/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(400, 0)
|
||||
layout_mode = 2
|
||||
text = "StreamGraph is a node graph-based virtual stream deck and livestream automation tool."
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="VersionLabel" type="Label" parent="HBoxContainer/VBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "v"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="CopyrightLabel" type="Label" parent="HBoxContainer/VBoxContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "ⓒ 2023-present Eroax
|
||||
ⓒ 2023-present Yagich"
|
||||
|
||||
[node name="Label" type="Label" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "For third-party licenses, see the THIRDPARTY.md file."
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Label2" type="Label" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "StreamGraph license"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="HBoxContainer/PanelContainer"]
|
||||
custom_minimum_size = Vector2(0, 200)
|
||||
layout_mode = 2
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="HBoxContainer/PanelContainer/ScrollContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/margin_left = 4
|
||||
theme_override_constants/margin_top = 4
|
||||
theme_override_constants/margin_right = 4
|
||||
theme_override_constants/margin_bottom = 4
|
||||
|
||||
[node name="LicenseLabel" type="Label" parent="HBoxContainer/PanelContainer/ScrollContainer/MarginContainer"]
|
||||
custom_minimum_size = Vector2(600, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = " GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
\"This License\" refers to version 3 of the GNU General Public License.
|
||||
|
||||
\"Copyright\" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
\"The Program\" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as \"you\". \"Licensees\" and
|
||||
\"recipients\" may be individuals or organizations.
|
||||
|
||||
To \"modify\" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a \"modified version\" of the
|
||||
earlier work or a work \"based on\" the earlier work.
|
||||
|
||||
A \"covered work\" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To \"propagate\" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To \"convey\" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays \"Appropriate Legal Notices\"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The \"source code\" for a work means the preferred form of the work
|
||||
for making modifications to it. \"Object code\" means any non-source
|
||||
form of a work.
|
||||
|
||||
A \"Standard Interface\" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The \"System Libraries\" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
\"Major Component\", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The \"Corresponding Source\" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
\"keep intact all notices\".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
\"aggregate\" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A \"User Product\" is either (1) a \"consumer product\", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, \"normally used\" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
\"Installation Information\" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
\"Additional permissions\" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered \"further
|
||||
restrictions\" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An \"entity transaction\" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A \"contributor\" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's \"contributor version\".
|
||||
|
||||
A contributor's \"essential patent claims\" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, \"control\" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a \"patent license\" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To \"grant\" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. \"Knowingly relying\" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is \"discriminatory\" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License \"or any later version\" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the \"copyright\" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an \"about box\".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a \"copyright disclaimer\" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>."
|
||||
label_settings = SubResource("LabelSettings_qamh4")
|
||||
autowrap_mode = 3
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends MarginContainer
|
||||
class_name AddNodeMenu
|
||||
|
||||
|
@ -83,6 +86,7 @@ func search(text: String) -> void:
|
|||
## Callback for [member search_line_edit]'s input events. Handles highlighting items when navigating with up/down arrow keys.
|
||||
func _on_search_line_edit_gui_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_down"):
|
||||
search_line_edit.accept_event()
|
||||
var category: Category
|
||||
for i: String in categories:
|
||||
var c: Category = categories[i]
|
||||
|
@ -103,6 +107,7 @@ func _on_search_line_edit_gui_input(event: InputEvent) -> void:
|
|||
scroll_container.ensure_control_visible(category.get_child(item + 1))
|
||||
|
||||
if event.is_action_pressed("ui_up"):
|
||||
search_line_edit.accept_event()
|
||||
var category: Category
|
||||
for i: String in categories:
|
||||
var c: Category = categories[i]
|
||||
|
@ -178,7 +183,6 @@ func _on_category_collapse_toggled(collapsed: bool, category: String) -> void:
|
|||
class Category extends VBoxContainer:
|
||||
const COLLAPSE_ICON := preload("res://graph_node_renderer/textures/collapse-icon.svg")
|
||||
const COLLAPSE_ICON_COLLAPSED := preload("res://graph_node_renderer/textures/collapse-icon-collapsed.svg")
|
||||
|
||||
var collapse_button: Button
|
||||
|
||||
## Emitted when a child item has been pressed.
|
||||
|
|
29
graph_node_renderer/debug_decks_list.gd
Normal file
29
graph_node_renderer/debug_decks_list.gd
Normal file
|
@ -0,0 +1,29 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends VBoxContainer
|
||||
class_name DebugDecksList
|
||||
|
||||
signal item_pressed(deck_id: String, instance_id: String)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for i in get_children():
|
||||
i.queue_free()
|
||||
|
||||
for deck_id: String in DeckHolder.decks:
|
||||
if DeckHolder.decks[deck_id] is Deck:
|
||||
var b := Button.new()
|
||||
b.text = deck_id
|
||||
b.pressed.connect(func():
|
||||
item_pressed.emit(deck_id, "")
|
||||
)
|
||||
add_child(b)
|
||||
else:
|
||||
for instance_id: String in DeckHolder.decks[deck_id]:
|
||||
var b := Button.new()
|
||||
b.text = "%s::%s" % [deck_id, instance_id]
|
||||
b.pressed.connect(func():
|
||||
item_pressed.emit(deck_id, instance_id)
|
||||
)
|
||||
add_child(b)
|
6
graph_node_renderer/debug_decks_list.tscn
Normal file
6
graph_node_renderer/debug_decks_list.tscn
Normal file
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://dm7sc6364j84i"]
|
||||
|
||||
[ext_resource type="Script" path="res://graph_node_renderer/debug_decks_list.gd" id="1_etp6v"]
|
||||
|
||||
[node name="DebugDecksList" type="VBoxContainer"]
|
||||
script = ExtResource("1_etp6v")
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends Control
|
||||
class_name DeckHolderRenderer
|
||||
|
||||
|
@ -5,14 +8,23 @@ class_name DeckHolderRenderer
|
|||
##
|
||||
## Entry point for the [GraphEdit] based Renderer
|
||||
|
||||
## Reference to the base scene for [DeckRendererGraphEdit]
|
||||
## Reference to the base scene for [DeckRessssssndererGraphEdit]
|
||||
const DECK_SCENE := preload("res://graph_node_renderer/deck_renderer_graph_edit.tscn")
|
||||
const DEBUG_DECKS_LIST := preload("res://graph_node_renderer/debug_decks_list.tscn")
|
||||
|
||||
const PERSISTENCE_NAMESPACE := "default"
|
||||
|
||||
## Reference to the main windows [TabContainerCustom]
|
||||
@onready var tab_container: TabContainerCustom = %TabContainerCustom as TabContainerCustom
|
||||
@onready var tab_container := %TabContainerCustom as TabContainerCustom
|
||||
## Reference to the [FileDialog] used for File operations through the program.
|
||||
@onready var file_dialog: FileDialog = $FileDialog
|
||||
|
||||
@export var new_deck_shortcut: Shortcut
|
||||
@export var open_deck_shortcut: Shortcut
|
||||
@export var save_deck_shortcut: Shortcut
|
||||
@export var save_deck_as_shortcut: Shortcut
|
||||
@export var close_deck_shortcut: Shortcut
|
||||
|
||||
## Enum for storing the Options in the "File" PopupMenu.
|
||||
enum FileMenuId {
|
||||
NEW,
|
||||
|
@ -20,22 +32,94 @@ enum FileMenuId {
|
|||
SAVE = 3,
|
||||
SAVE_AS,
|
||||
CLOSE = 6,
|
||||
RECENTS,
|
||||
}
|
||||
@onready var file_popup_menu: PopupMenu = %File as PopupMenu
|
||||
var max_recents := 4
|
||||
var recent_files := []
|
||||
var recent_path: String
|
||||
@onready var unsaved_changes_dialog_single_deck := $UnsavedChangesDialogSingleDeck as UnsavedChangesDialogSingleDeck
|
||||
@onready var unsaved_changes_dialog: ConfirmationDialog = $UnsavedChangesDialog
|
||||
|
||||
enum ConnectionsMenuId {
|
||||
OBS,
|
||||
TWITCH,
|
||||
}
|
||||
|
||||
enum DebugMenuId {
|
||||
DECKS,
|
||||
EMBED_SUBWINDOWS,
|
||||
}
|
||||
@onready var debug_popup_menu: PopupMenu = %Debug
|
||||
|
||||
enum HelpMenuId {
|
||||
DOCS,
|
||||
ABOUT,
|
||||
}
|
||||
@onready var about_dialog: AcceptDialog = %AboutDialog
|
||||
|
||||
## Weak Reference to the Deck that is currently going to be saved.
|
||||
var _deck_to_save: WeakRef
|
||||
|
||||
@onready var no_obsws := %NoOBSWS as NoOBSWS
|
||||
|
||||
@onready var obs_setup_dialog := $OBSWebsocketSetupDialog as OBSWebsocketSetupDialog
|
||||
@onready var twitch_setup_dialog := $Twitch_Setup_Dialog as TwitchSetupDialog
|
||||
@onready var logger_renderer: LoggerRenderer = %LoggerRenderer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
get_tree().auto_accept_quit = false
|
||||
tab_container.add_button_pressed.connect(add_empty_deck)
|
||||
tab_container.tab_changed.connect(_on_tab_container_tab_changed)
|
||||
RendererPersistence.init_namespace(PERSISTENCE_NAMESPACE)
|
||||
|
||||
var embed_subwindows: bool = RendererPersistence.get_or_create(PERSISTENCE_NAMESPACE, "config", "embed_subwindows", true)
|
||||
debug_popup_menu.set_item_checked(
|
||||
DebugMenuId.EMBED_SUBWINDOWS,
|
||||
embed_subwindows
|
||||
)
|
||||
|
||||
get_tree().get_root().gui_embed_subwindows = embed_subwindows
|
||||
file_dialog.use_native_dialog = !embed_subwindows
|
||||
|
||||
recent_files = RendererPersistence.get_or_create(
|
||||
PERSISTENCE_NAMESPACE, "config",
|
||||
"recent_files", []
|
||||
)
|
||||
recent_path = RendererPersistence.get_or_create(
|
||||
PERSISTENCE_NAMESPACE, "config",
|
||||
"recent_path", OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
|
||||
)
|
||||
add_recents_to_menu()
|
||||
|
||||
tab_container.tab_close_requested.connect(
|
||||
func(tab: int):
|
||||
DeckHolder.close_deck(tab_container.get_tab_metadata(tab))
|
||||
tab_container.close_tab(tab)
|
||||
if tab_container.get_tab_metadata(tab, "dirty") && !tab_container.get_tab_metadata(tab, "group"):
|
||||
unsaved_changes_dialog_single_deck.set_meta("tab", tab)
|
||||
unsaved_changes_dialog_single_deck.show()
|
||||
return
|
||||
close_tab(tab)
|
||||
)
|
||||
|
||||
file_dialog.canceled.connect(disconnect_file_dialog_signals)
|
||||
Connections.obs_websocket = no_obsws
|
||||
Connections.twitch = %Twitch_Connection
|
||||
|
||||
Connections.twitch.chat_socket.chat_received_rich.connect(Connections._twitch_chat_received)
|
||||
|
||||
file_popup_menu.set_item_shortcut(FileMenuId.NEW, new_deck_shortcut)
|
||||
file_popup_menu.set_item_shortcut(FileMenuId.OPEN, open_deck_shortcut)
|
||||
file_popup_menu.set_item_shortcut(FileMenuId.SAVE, save_deck_shortcut)
|
||||
file_popup_menu.set_item_shortcut(FileMenuId.SAVE_AS, save_deck_as_shortcut)
|
||||
file_popup_menu.set_item_shortcut(FileMenuId.CLOSE, close_deck_shortcut)
|
||||
|
||||
|
||||
func _on_tab_container_tab_changed(tab: int) -> void:
|
||||
var is_group = tab_container.get_tab_metadata(tab, "group", false)
|
||||
file_popup_menu.set_item_disabled(FileMenuId.SAVE, is_group)
|
||||
file_popup_menu.set_item_disabled(FileMenuId.SAVE_AS, is_group)
|
||||
|
||||
|
||||
## Called when the File button in the [MenuBar] is pressed with the [param id]
|
||||
## of the button within it that was pressed.
|
||||
|
@ -44,27 +128,46 @@ func _on_file_id_pressed(id: int) -> void:
|
|||
FileMenuId.NEW:
|
||||
add_empty_deck()
|
||||
FileMenuId.OPEN:
|
||||
open_open_dialog("res://")
|
||||
FileMenuId.SAVE:
|
||||
open_open_dialog(recent_path)
|
||||
FileMenuId.SAVE when tab_container.get_tab_count() > 0:
|
||||
save_active_deck()
|
||||
FileMenuId.SAVE_AS:
|
||||
open_save_dialog("res://")
|
||||
FileMenuId.SAVE_AS when tab_container.get_tab_count() > 0:
|
||||
open_save_dialog(tab_container.get_tab_metadata(tab_container.get_current_tab(), "path"))
|
||||
FileMenuId.CLOSE:
|
||||
close_current_tab()
|
||||
_ when id in range(FileMenuId.RECENTS, FileMenuId.RECENTS + max_recents + 1):
|
||||
open_deck_at_path(recent_files[id - FileMenuId.RECENTS - 1])
|
||||
|
||||
## Adds an empty [DeckRendererGraphEdit] with a corresponding [Deck] for it's data.
|
||||
func add_empty_deck() -> void:
|
||||
var deck := DeckHolder.add_empty_deck()
|
||||
var inst: DeckRendererGraphEdit = DECK_SCENE.instantiate()
|
||||
inst.deck = deck
|
||||
tab_container.add_content(inst, "Deck %s" % (tab_container.get_tab_count() + 1))
|
||||
tab_container.set_tab_metadata(tab_container.get_current_tab(), deck)
|
||||
inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested.bind(deck))
|
||||
var tab := tab_container.add_content(inst, "<unsaved deck>")
|
||||
tab_container.set_tab_metadata(tab, "id", deck.id)
|
||||
tab_container.set_tab_metadata(tab, "group", false)
|
||||
tab_container.set_tab_metadata(tab, "path", recent_path.path_join(""))
|
||||
inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested)
|
||||
inst.dirty_state_changed.connect(_on_deck_renderer_dirty_state_changed.bind(inst))
|
||||
tab_container.set_current_tab(tab)
|
||||
|
||||
## Closes the current tab in [member tab_container]
|
||||
func close_current_tab() -> void:
|
||||
tab_container.close_tab(tab_container.get_current_tab())
|
||||
|
||||
|
||||
func close_tab(tab: int) -> void:
|
||||
if !tab_container.get_tab_metadata(tab, "group"):
|
||||
var groups := DeckHolder.close_deck(tab_container.get_tab_metadata(tab, "id"))
|
||||
# close tabs associated with this deck's groups
|
||||
for group in groups:
|
||||
for c_tab in range(tab_container.get_tab_count() - 1, 0, -1):
|
||||
if tab_container.get_tab_metadata(c_tab, "id") == group:
|
||||
tab_container.close_tab(tab)
|
||||
await get_tree().process_frame
|
||||
|
||||
tab_container.close_tab(tab)
|
||||
|
||||
## Opens [member file_dialog] with the mode [member FileDialog.FILE_MODE_SAVE_FILE]
|
||||
## as well as getting a weakref to the active [Deck]
|
||||
func open_save_dialog(path: String) -> void:
|
||||
|
@ -80,7 +183,7 @@ func open_save_dialog(path: String) -> void:
|
|||
func open_open_dialog(path: String) -> void:
|
||||
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILES
|
||||
file_dialog.title = "Open Deck(s)"
|
||||
file_dialog.current_path = path
|
||||
file_dialog.current_path = path + "/"
|
||||
file_dialog.popup_centered()
|
||||
file_dialog.files_selected.connect(_on_file_dialog_open_files, CONNECT_ONE_SHOT)
|
||||
|
||||
|
@ -92,20 +195,46 @@ func _on_file_dialog_save_file(path: String) -> void:
|
|||
return
|
||||
|
||||
deck.save_path = path
|
||||
tab_container.set_tab_title(tab_container.get_current_tab(), path.get_file())
|
||||
var renderer: DeckRendererGraphEdit = tab_container.get_content(tab_container.get_current_tab()) as DeckRendererGraphEdit
|
||||
renderer.dirty = false
|
||||
# TODO: put this into DeckHolder instead
|
||||
var json := JSON.stringify(deck.to_dict(), "\t")
|
||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
f.store_string(json)
|
||||
add_recent_file(get_active_deck().save_path)
|
||||
recent_path = path.get_base_dir()
|
||||
RendererPersistence.set_value(PERSISTENCE_NAMESPACE, "config", "recent_path", recent_path)
|
||||
|
||||
|
||||
## Connected to [signal FileDialog.open_files] on [member file_dialog]. Opens
|
||||
## the selected paths, instantiating [DeckRenderGraphEdit]s and [Deck]s for each.
|
||||
func _on_file_dialog_open_files(paths: PackedStringArray) -> void:
|
||||
for path in paths:
|
||||
open_deck_at_path(path)
|
||||
|
||||
|
||||
func open_deck_at_path(path: String) -> void:
|
||||
for tab in tab_container.get_tab_count():
|
||||
if tab_container.get_tab_metadata(tab, "path") == path:
|
||||
tab_container.set_current_tab(tab)
|
||||
return
|
||||
|
||||
var deck := DeckHolder.open_deck_from_file(path)
|
||||
var inst: DeckRendererGraphEdit = DECK_SCENE.instantiate()
|
||||
inst.deck = deck
|
||||
tab_container.add_content(inst, "Deck %s" % (tab_container.get_tab_count() + 1))
|
||||
var tab := tab_container.add_content(inst, path.get_file())
|
||||
tab_container.set_tab_metadata(tab, "id", deck.id)
|
||||
tab_container.set_tab_metadata(tab, "group", false)
|
||||
tab_container.set_tab_metadata(tab, "path", path)
|
||||
inst.initialize_from_deck()
|
||||
inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested.bind(deck))
|
||||
inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested)
|
||||
inst.dirty_state_changed.connect(_on_deck_renderer_dirty_state_changed.bind(inst))
|
||||
add_recent_file(path)
|
||||
recent_path = path.get_base_dir()
|
||||
RendererPersistence.set_value(PERSISTENCE_NAMESPACE, "config", "recent_path", recent_path)
|
||||
tab_container.set_current_tab(tab)
|
||||
|
||||
|
||||
## Gets the currently active [Deck] from [member tab_container]
|
||||
func get_active_deck() -> Deck:
|
||||
|
@ -116,12 +245,24 @@ func get_active_deck() -> Deck:
|
|||
|
||||
## Saves the active [Deck] in [member tab_container]
|
||||
func save_active_deck() -> void:
|
||||
if get_active_deck().save_path.is_empty():
|
||||
save_tab(tab_container.get_current_tab())
|
||||
|
||||
|
||||
func save_tab(tab: int) -> void:
|
||||
if tab_container.get_tab_metadata(tab, "group"):
|
||||
return
|
||||
|
||||
var renderer := tab_container.get_content(tab) as DeckRendererGraphEdit
|
||||
var deck := renderer.deck
|
||||
if deck.save_path.is_empty():
|
||||
open_save_dialog("res://")
|
||||
else:
|
||||
var json := JSON.stringify(get_active_deck().to_dict(), "\t")
|
||||
var f := FileAccess.open(get_active_deck().save_path, FileAccess.WRITE)
|
||||
var json := JSON.stringify(deck.to_dict(), "\t")
|
||||
var f := FileAccess.open(deck.save_path, FileAccess.WRITE)
|
||||
f.store_string(json)
|
||||
add_recent_file(deck.save_path)
|
||||
renderer.dirty = false
|
||||
|
||||
|
||||
## Disconnects the [FileDialog] signals if they are already connected.
|
||||
func disconnect_file_dialog_signals() -> void:
|
||||
|
@ -134,11 +275,183 @@ func disconnect_file_dialog_signals() -> void:
|
|||
## Connected to [signal DeckRenderGraphEdit.group_entered_request] to allow entering
|
||||
## groups based off the given [param group_id] and [param deck]. As well as adding
|
||||
## a corresponding tab to [member tab_container]
|
||||
func _on_deck_renderer_group_enter_requested(group_id: String, deck: Deck) -> void:
|
||||
var group_deck := deck.get_group(group_id)
|
||||
func _on_deck_renderer_group_enter_requested(group_id: String) -> void:
|
||||
#var group_deck := deck.get_group(group_id)
|
||||
for tab in tab_container.get_tab_count():
|
||||
if tab_container.get_tab_metadata(tab, "id") == group_id:
|
||||
tab_container.set_current_tab(tab)
|
||||
return
|
||||
var group_deck := DeckHolder.get_deck(group_id)
|
||||
var deck_renderer: DeckRendererGraphEdit = DECK_SCENE.instantiate()
|
||||
deck_renderer.deck = group_deck
|
||||
deck_renderer.initialize_from_deck()
|
||||
tab_container.add_content(deck_renderer, "Group %s" % (tab_container.get_tab_count() + 1))
|
||||
tab_container.set_tab_metadata(tab_container.get_current_tab(), group_deck)
|
||||
deck_renderer.group_enter_requested.connect(_on_deck_renderer_group_enter_requested.bind(deck_renderer.deck))
|
||||
var tab := tab_container.add_content(deck_renderer, "(g) %s" % group_id.left(8))
|
||||
tab_container.set_tab_metadata(tab, "id", group_id)
|
||||
tab_container.set_tab_metadata(tab, "group", true)
|
||||
deck_renderer.group_enter_requested.connect(_on_deck_renderer_group_enter_requested)
|
||||
tab_container.set_current_tab(tab)
|
||||
|
||||
|
||||
func _on_connections_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
ConnectionsMenuId.OBS:
|
||||
obs_setup_dialog.popup_centered()
|
||||
ConnectionsMenuId.TWITCH:
|
||||
twitch_setup_dialog.popup_centered()
|
||||
|
||||
|
||||
func _on_obs_websocket_setup_dialog_connect_button_pressed(state: OBSWebsocketSetupDialog.ConnectionState) -> void:
|
||||
match state:
|
||||
OBSWebsocketSetupDialog.ConnectionState.DISCONNECTED:
|
||||
obs_setup_dialog.set_button_state(OBSWebsocketSetupDialog.ConnectionState.CONNECTING)
|
||||
no_obsws.connect_to_obsws(obs_setup_dialog.get_port(), obs_setup_dialog.get_password())
|
||||
await no_obsws.connection_ready
|
||||
obs_setup_dialog.set_button_state(OBSWebsocketSetupDialog.ConnectionState.CONNECTED)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
DeckHolder.send_event(&"process", {"delta": delta})
|
||||
|
||||
|
||||
func _on_debug_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
DebugMenuId.DECKS:
|
||||
var d := AcceptDialog.new()
|
||||
var debug_decks: DebugDecksList = DEBUG_DECKS_LIST.instantiate()
|
||||
d.add_child(debug_decks)
|
||||
d.canceled.connect(d.queue_free)
|
||||
d.confirmed.connect(d.queue_free)
|
||||
debug_decks.item_pressed.connect(_on_debug_decks_viewer_item_pressed)
|
||||
add_child(d)
|
||||
d.popup_centered()
|
||||
DebugMenuId.EMBED_SUBWINDOWS:
|
||||
var c := debug_popup_menu.is_item_checked(id)
|
||||
debug_popup_menu.set_item_checked(id, !c)
|
||||
get_tree().get_root().gui_embed_subwindows = !c
|
||||
RendererPersistence.set_value(PERSISTENCE_NAMESPACE, "config", "embed_subwindows", !c)
|
||||
file_dialog.use_native_dialog = c
|
||||
|
||||
|
||||
func _on_debug_decks_viewer_item_pressed(deck_id: String, instance_id: String) -> void:
|
||||
if instance_id == "":
|
||||
var deck := DeckHolder.get_deck(deck_id)
|
||||
var inst: DeckRendererGraphEdit = DECK_SCENE.instantiate()
|
||||
inst.deck = deck
|
||||
var tab := tab_container.add_content(inst, "<Deck %s>" % [deck_id.left(8)])
|
||||
tab_container.set_tab_metadata(tab, "id", deck.id)
|
||||
inst.initialize_from_deck()
|
||||
inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested)
|
||||
tab_container.set_current_tab(tab)
|
||||
else:
|
||||
var deck := DeckHolder.get_group_instance(deck_id, instance_id)
|
||||
var inst: DeckRendererGraphEdit = DECK_SCENE.instantiate()
|
||||
inst.deck = deck
|
||||
var tab := tab_container.add_content(inst, "<Group %s::%s>" % [deck_id.left(8), instance_id.left(8)])
|
||||
tab_container.set_tab_metadata(tab, "id", deck.id)
|
||||
inst.initialize_from_deck()
|
||||
inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested)
|
||||
tab_container.set_current_tab(tab)
|
||||
|
||||
|
||||
func _on_deck_renderer_dirty_state_changed(renderer: DeckRendererGraphEdit) -> void:
|
||||
var idx: int = range(tab_container.get_tab_count()).filter(
|
||||
func(x: int):
|
||||
return tab_container.get_content(x) == renderer
|
||||
)[0]
|
||||
var title := tab_container.get_tab_title(idx).trim_suffix("(*)")
|
||||
if renderer.dirty:
|
||||
tab_container.set_tab_title(idx, "%s(*)" % title)
|
||||
else:
|
||||
tab_container.set_tab_title(idx, title.trim_suffix("(*)"))
|
||||
tab_container.set_tab_metadata(idx, "dirty", renderer.dirty)
|
||||
|
||||
|
||||
func add_recent_file(path: String) -> void:
|
||||
var item := recent_files.find(path)
|
||||
if item == -1:
|
||||
recent_files.push_front(path)
|
||||
else:
|
||||
recent_files.push_front(recent_files.pop_at(item))
|
||||
recent_files = recent_files.slice(0, max_recents)
|
||||
|
||||
add_recents_to_menu()
|
||||
|
||||
|
||||
func add_recents_to_menu() -> void:
|
||||
if recent_files.is_empty():
|
||||
return
|
||||
|
||||
if file_popup_menu.get_item_count() > FileMenuId.RECENTS + 1:
|
||||
var end := file_popup_menu.get_item_count() - FileMenuId.RECENTS - 1
|
||||
for i in end:
|
||||
file_popup_menu.remove_item(file_popup_menu.get_item_count() - 1)
|
||||
|
||||
var reduce_length := recent_files.any(func(x: String): return x.length() > 35)
|
||||
|
||||
for i in recent_files.size():
|
||||
var file = recent_files[i] as String
|
||||
if reduce_length:
|
||||
# shorten the basepath to be the first letter of all folders
|
||||
var base: String = Array(
|
||||
file.get_base_dir().split("/", false)
|
||||
).reduce(
|
||||
func(a: String, s: String):
|
||||
return a + s[0] + "/",
|
||||
"/")
|
||||
var filename := file.get_file()
|
||||
file = base.path_join(filename)
|
||||
|
||||
file_popup_menu.add_item(file)
|
||||
var s := Shortcut.new()
|
||||
var k := InputEventKey.new()
|
||||
k.keycode = KEY_1 + i
|
||||
k.ctrl_pressed = true
|
||||
s.events.append(k)
|
||||
file_popup_menu.set_item_shortcut(file_popup_menu.get_item_count() - 1, s)
|
||||
|
||||
RendererPersistence.set_value(PERSISTENCE_NAMESPACE, "config", "recent_files", recent_files)
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||||
RendererPersistence.commit(PERSISTENCE_NAMESPACE)
|
||||
|
||||
if range(tab_container.get_tab_count()).any(func(x: int): return tab_container.get_content(x).dirty):
|
||||
unsaved_changes_dialog.show()
|
||||
else:
|
||||
for i in tab_container.get_tab_count():
|
||||
close_tab(i)
|
||||
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func _on_unsaved_changes_dialog_single_deck_confirmed() -> void:
|
||||
save_tab(unsaved_changes_dialog_single_deck.get_meta("tab"))
|
||||
close_tab(unsaved_changes_dialog_single_deck.get_meta("tab"))
|
||||
|
||||
|
||||
func _on_unsaved_changes_dialog_single_deck_custom_action(action: StringName) -> void:
|
||||
if action == &"force_close":
|
||||
close_tab(unsaved_changes_dialog_single_deck.get_meta("tab"))
|
||||
unsaved_changes_dialog_single_deck.hide()
|
||||
|
||||
|
||||
func _on_unsaved_changes_dialog_confirmed() -> void:
|
||||
for i in tab_container.get_tab_count():
|
||||
close_tab(i)
|
||||
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("toggle_console"):
|
||||
logger_renderer.visible = !logger_renderer.visible
|
||||
accept_event()
|
||||
|
||||
|
||||
func _on_help_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
HelpMenuId.ABOUT:
|
||||
about_dialog.show()
|
||||
HelpMenuId.DOCS:
|
||||
OS.shell_open("https://codeberg.org/Eroax/StreamGraph/wiki")
|
||||
|
|
|
@ -1,8 +1,62 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://duaah5x0jhkn6"]
|
||||
[gd_scene load_steps=22 format=3 uid="uid://duaah5x0jhkn6"]
|
||||
|
||||
[ext_resource type="Script" path="res://graph_node_renderer/deck_holder_renderer.gd" id="1_67g2g"]
|
||||
[ext_resource type="PackedScene" uid="uid://b84f2ngtcm5b8" path="res://graph_node_renderer/tab_container_custom.tscn" id="1_s3ug2"]
|
||||
[ext_resource type="Theme" uid="uid://dqqdqscid2iem" path="res://graph_node_renderer/default_theme.tres" id="1_tgul2"]
|
||||
[ext_resource type="Script" path="res://addons/no-obs-ws/NoOBSWS.gd" id="4_nu72u"]
|
||||
[ext_resource type="PackedScene" uid="uid://duvh3r740w2p5" path="res://graph_node_renderer/logger_renderer.tscn" id="4_pvexk"]
|
||||
[ext_resource type="Script" path="res://addons/no_twitch/twitch_connection.gd" id="5_3n36q"]
|
||||
[ext_resource type="PackedScene" uid="uid://eioso6jb42jy" path="res://graph_node_renderer/obs_websocket_setup_dialog.tscn" id="5_uo2gj"]
|
||||
[ext_resource type="PackedScene" uid="uid://bq2lxmbnic4lc" path="res://graph_node_renderer/twitch_setup_dialog.tscn" id="7_7rhap"]
|
||||
[ext_resource type="PackedScene" uid="uid://cuwou2aa7qfc2" path="res://graph_node_renderer/unsaved_changes_dialog_single_deck.tscn" id="8_qf6ve"]
|
||||
[ext_resource type="PackedScene" uid="uid://cvvkj138fg8jg" path="res://graph_node_renderer/unsaved_changes_dialog.tscn" id="9_4n0q6"]
|
||||
[ext_resource type="PackedScene" uid="uid://bu466w2w3q08c" path="res://graph_node_renderer/about_dialog.tscn" id="11_6ln7n"]
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_giamc"]
|
||||
device = -1
|
||||
ctrl_pressed = true
|
||||
keycode = 78
|
||||
unicode = 110
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_30rq6"]
|
||||
events = [SubResource("InputEventKey_giamc")]
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_cyjf4"]
|
||||
device = -1
|
||||
ctrl_pressed = true
|
||||
keycode = 79
|
||||
unicode = 111
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_m48tj"]
|
||||
events = [SubResource("InputEventKey_cyjf4")]
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_jgr3p"]
|
||||
device = -1
|
||||
ctrl_pressed = true
|
||||
keycode = 83
|
||||
unicode = 115
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_xr6s4"]
|
||||
events = [SubResource("InputEventKey_jgr3p")]
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_762xj"]
|
||||
device = -1
|
||||
shift_pressed = true
|
||||
ctrl_pressed = true
|
||||
keycode = 83
|
||||
unicode = 83
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_myxuq"]
|
||||
events = [SubResource("InputEventKey_762xj")]
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_exx3o"]
|
||||
device = -1
|
||||
ctrl_pressed = true
|
||||
keycode = 87
|
||||
unicode = 119
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_46v8y"]
|
||||
events = [SubResource("InputEventKey_exx3o")]
|
||||
|
||||
[node name="DeckHolderRenderer" type="Control"]
|
||||
layout_mode = 3
|
||||
|
@ -13,6 +67,11 @@ grow_horizontal = 2
|
|||
grow_vertical = 2
|
||||
theme = ExtResource("1_tgul2")
|
||||
script = ExtResource("1_67g2g")
|
||||
new_deck_shortcut = SubResource("Shortcut_30rq6")
|
||||
open_deck_shortcut = SubResource("Shortcut_m48tj")
|
||||
save_deck_shortcut = SubResource("Shortcut_xr6s4")
|
||||
save_deck_as_shortcut = SubResource("Shortcut_myxuq")
|
||||
close_deck_shortcut = SubResource("Shortcut_46v8y")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
layout_mode = 1
|
||||
|
@ -28,7 +87,7 @@ theme_override_constants/margin_bottom = 2
|
|||
|
||||
[node name="VSplitContainer" type="VSplitContainer" parent="MarginContainer"]
|
||||
layout_mode = 2
|
||||
split_offset = 677
|
||||
split_offset = 460
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VSplitContainer"]
|
||||
layout_mode = 2
|
||||
|
@ -37,7 +96,8 @@ layout_mode = 2
|
|||
layout_mode = 2
|
||||
|
||||
[node name="File" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"]
|
||||
item_count = 7
|
||||
unique_name_in_owner = true
|
||||
item_count = 8
|
||||
item_0/text = "New Deck"
|
||||
item_0/id = 0
|
||||
item_1/text = "Open Deck"
|
||||
|
@ -54,20 +114,81 @@ item_5/id = 5
|
|||
item_5/separator = true
|
||||
item_6/text = "Close Deck"
|
||||
item_6/id = 6
|
||||
item_7/text = "Recent Decks"
|
||||
item_7/id = 7
|
||||
item_7/separator = true
|
||||
|
||||
[node name="Edit" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[node name="Connections" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"]
|
||||
unique_name_in_owner = true
|
||||
item_count = 2
|
||||
item_0/text = "OBS..."
|
||||
item_0/id = 0
|
||||
item_1/text = "Twitch.."
|
||||
item_1/id = 1
|
||||
|
||||
[node name="Debug" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"]
|
||||
unique_name_in_owner = true
|
||||
item_count = 2
|
||||
item_0/text = "Decks..."
|
||||
item_0/id = 0
|
||||
item_1/text = "Embed subwindows"
|
||||
item_1/checkable = 1
|
||||
item_1/checked = true
|
||||
item_1/id = 1
|
||||
|
||||
[node name="Help" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"]
|
||||
unique_name_in_owner = true
|
||||
item_count = 2
|
||||
item_0/text = "Online Documentation"
|
||||
item_0/id = 0
|
||||
item_1/text = "About..."
|
||||
item_1/id = 1
|
||||
|
||||
[node name="TabContainerCustom" parent="MarginContainer/VSplitContainer/VBoxContainer" instance=ExtResource("1_s3ug2")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ConsoleContainer" type="PanelContainer" parent="MarginContainer/VSplitContainer"]
|
||||
[node name="LoggerRenderer" parent="MarginContainer/VSplitContainer" instance=ExtResource("4_pvexk")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="FileDialog" type="FileDialog" parent="."]
|
||||
size = Vector2i(776, 447)
|
||||
mode_overrides_title = false
|
||||
access = 2
|
||||
filters = PackedStringArray("*.deck;StreamGraph Decks")
|
||||
use_native_dialog = true
|
||||
|
||||
[node name="Connections" type="Node" parent="."]
|
||||
|
||||
[node name="NoOBSWS" type="Node" parent="Connections"]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("4_nu72u")
|
||||
|
||||
[node name="Twitch_Connection" type="Node" parent="Connections"]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("5_3n36q")
|
||||
|
||||
[node name="OBSWebsocketSetupDialog" parent="." instance=ExtResource("5_uo2gj")]
|
||||
|
||||
[node name="Twitch_Setup_Dialog" parent="." instance=ExtResource("7_7rhap")]
|
||||
|
||||
[node name="UnsavedChangesDialogSingleDeck" parent="." instance=ExtResource("8_qf6ve")]
|
||||
|
||||
[node name="UnsavedChangesDialog" parent="." instance=ExtResource("9_4n0q6")]
|
||||
|
||||
[node name="AboutDialog" parent="." instance=ExtResource("11_6ln7n")]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/File" to="." method="_on_file_id_pressed"]
|
||||
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/Connections" to="." method="_on_connections_id_pressed"]
|
||||
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/Debug" to="." method="_on_debug_id_pressed"]
|
||||
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/Help" to="." method="_on_help_id_pressed"]
|
||||
[connection signal="connect_button_pressed" from="OBSWebsocketSetupDialog" to="." method="_on_obs_websocket_setup_dialog_connect_button_pressed"]
|
||||
[connection signal="confirmed" from="UnsavedChangesDialogSingleDeck" to="." method="_on_unsaved_changes_dialog_single_deck_confirmed"]
|
||||
[connection signal="custom_action" from="UnsavedChangesDialogSingleDeck" to="." method="_on_unsaved_changes_dialog_single_deck_custom_action"]
|
||||
[connection signal="confirmed" from="UnsavedChangesDialog" to="." method="_on_unsaved_changes_dialog_confirmed"]
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends GraphNode
|
||||
class_name DeckNodeRendererGraphNode
|
||||
|
||||
|
@ -17,18 +20,28 @@ func _ready() -> void:
|
|||
node.ports_updated.connect(_on_node_ports_updated)
|
||||
for port in node.get_all_ports():
|
||||
update_port(port)
|
||||
position_offset_changed.connect(_on_position_offset_changed)
|
||||
node.renamed.connect(_on_node_renamed)
|
||||
if node.node_type == "group_node":
|
||||
get_titlebar_hbox().tooltip_text = "Group %s" % node.group_id.left(8)
|
||||
|
||||
## Connected to [signal GraphElement.position_offset_updated] and updates the
|
||||
## [member node]s properties
|
||||
func _on_position_offset_changed() -> void:
|
||||
node.position.x = position_offset.x
|
||||
node.position.y = position_offset.y
|
||||
node.position_updated.emit(node.position)
|
||||
(get_parent() as DeckRendererGraphEdit).dirty = true
|
||||
|
||||
## Connected to [member node]s [signal position_updated] to keep parity with the
|
||||
## data position.
|
||||
func _on_node_position_updated(new_position: Dictionary) -> void:
|
||||
position_offset_changed.disconnect(_on_position_offset_changed)
|
||||
position_offset.x = new_position.x
|
||||
position_offset.y = new_position.y
|
||||
position_offset_changed.connect(_on_position_offset_changed)
|
||||
(get_parent() as DeckRendererGraphEdit).dirty = true
|
||||
|
||||
|
||||
## Connected to [member node]s [signal port_added] handles setting up the specified
|
||||
## [member Port.descriptor] with it's required nodes/signals etc. + adding the port
|
||||
|
@ -64,6 +77,10 @@ func _on_node_ports_updated() -> void:
|
|||
update_port(port)
|
||||
|
||||
|
||||
func _on_node_renamed(new_name: String) -> void:
|
||||
title = new_name
|
||||
|
||||
|
||||
func update_port(port: Port) -> void:
|
||||
var descriptor_split := port.descriptor.split(":")
|
||||
match descriptor_split[0]:
|
||||
|
@ -92,6 +109,12 @@ func update_port(port: Port) -> void:
|
|||
line_edit.text_changed.connect(port.set_value)
|
||||
"singlechoice":
|
||||
var box := OptionButton.new()
|
||||
if descriptor_split.slice(1).is_empty():
|
||||
if port.value:
|
||||
box.add_item(port.value)
|
||||
else:
|
||||
box.add_item(port.label)
|
||||
else:
|
||||
for item in descriptor_split.slice(1):
|
||||
box.add_item(item)
|
||||
add_child(box)
|
||||
|
@ -111,10 +134,22 @@ func update_port(port: Port) -> void:
|
|||
code_edit.text_changed.connect(port.set_value.bind(code_edit.get_text))
|
||||
code_edit.custom_minimum_size = Vector2(200, 100)
|
||||
code_edit.size_flags_vertical = SIZE_EXPAND_FILL
|
||||
"checkbox":
|
||||
var cb := CheckBox.new()
|
||||
add_child(cb)
|
||||
if descriptor_split.size() > 1:
|
||||
cb.button_pressed = true
|
||||
if port.value is bool:
|
||||
cb.button_pressed = port.value
|
||||
cb.text = port.label
|
||||
port.value_callback = cb.is_pressed
|
||||
cb.toggled.connect(port.set_value)
|
||||
_:
|
||||
var label := Label.new()
|
||||
add_child(label)
|
||||
label.text = port.label
|
||||
if port.port_type == DeckNode.PortType.OUTPUT:
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
|
||||
set_slot(
|
||||
port.index,
|
||||
|
|
|
@ -9,5 +9,3 @@ offset_bottom = 55.0
|
|||
resizable = true
|
||||
title = "Deck Node"
|
||||
script = ExtResource("1_pos0w")
|
||||
|
||||
[connection signal="position_offset_changed" from="." to="." method="_on_position_offset_changed"]
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends GraphEdit
|
||||
class_name DeckRendererGraphEdit
|
||||
|
||||
|
@ -17,6 +20,9 @@ var add_node_menu: AddNodeMenu
|
|||
## nodes in [method _on_add_node_menu_node_selected]
|
||||
var popup_position: Vector2
|
||||
|
||||
var rename_popup := RenamePopup.new()
|
||||
@export var rename_popup_size: Vector2i = Vector2i(200, 0)
|
||||
|
||||
## References the [Deck] that holds all the functional properties of this [DeckRendererGraphEdit]
|
||||
var deck: Deck:
|
||||
set(v):
|
||||
|
@ -27,6 +33,17 @@ var deck: Deck:
|
|||
## Emits when Group creation is requested. Ex. Hitting the "group_nodes" Hotkey.
|
||||
signal group_enter_requested(group_id: String)
|
||||
|
||||
var dirty: bool = false:
|
||||
set(v):
|
||||
if change_dirty:
|
||||
dirty = v
|
||||
dirty_state_changed.emit()
|
||||
var is_group: bool = false
|
||||
|
||||
var change_dirty: bool = true
|
||||
|
||||
signal dirty_state_changed
|
||||
|
||||
## Sets up the [member search_popup_panel] with an instance of [member ADD_NODE_SCENE]
|
||||
## stored in [member add_node_menu]. And sets its size of [member search_popup_panel] to
|
||||
## [member add_node_popup_size]
|
||||
|
@ -37,6 +54,11 @@ func _ready() -> void:
|
|||
search_popup_panel.size = search_popup_size
|
||||
add_child(search_popup_panel, false, Node.INTERNAL_MODE_BACK)
|
||||
|
||||
rename_popup = RenamePopup.new()
|
||||
rename_popup.rename_confirmed.connect(_on_rename_popup_rename_confirmed)
|
||||
rename_popup.close_requested.connect(_on_rename_popup_closed)
|
||||
add_child(rename_popup, false, Node.INTERNAL_MODE_FRONT)
|
||||
|
||||
for t: DeckType.Types in DeckType.CONVERSION_MAP:
|
||||
for out_type: DeckType.Types in DeckType.CONVERSION_MAP[t]:
|
||||
add_valid_connection_type(t, out_type)
|
||||
|
@ -55,13 +77,14 @@ func attempt_connection(from_node_name: StringName, from_port: int, to_node_name
|
|||
#var from_output := from_node_renderer.node.get_global_port_idx_from_output(from_port)
|
||||
#var to_input := to_node_renderer.node.get_global_port_idx_from_input(to_port)
|
||||
|
||||
if deck.connect_nodes(from_node_renderer.node, to_node_renderer.node, from_port, to_port):
|
||||
if deck.connect_nodes(from_node_renderer.node._id, to_node_renderer.node._id, from_port, to_port):
|
||||
connect_node(
|
||||
from_node_renderer.name,
|
||||
from_port,
|
||||
to_node_renderer.name,
|
||||
to_port
|
||||
)
|
||||
dirty = true
|
||||
|
||||
## Receives [signal GraphEdit.disconnection_request] and attempts to disconnect the two [DeckNode]s
|
||||
## involved, utilizes [NodeDB] for accessing them.
|
||||
|
@ -72,7 +95,7 @@ func attempt_disconnect(from_node_name: StringName, from_port: int, to_node_name
|
|||
#var from_output := from_node_renderer.node.get_global_port_idx_from_output(from_port)
|
||||
#var to_input := to_node_renderer.node.get_global_port_idx_from_input(to_port)
|
||||
|
||||
deck.disconnect_nodes(from_node_renderer.node, to_node_renderer.node, from_port, to_port)
|
||||
deck.disconnect_nodes(from_node_renderer.node._id, to_node_renderer.node._id, from_port, to_port)
|
||||
|
||||
disconnect_node(
|
||||
from_node_renderer.name,
|
||||
|
@ -80,6 +103,7 @@ func attempt_disconnect(from_node_name: StringName, from_port: int, to_node_name
|
|||
to_node_renderer.name,
|
||||
to_port
|
||||
)
|
||||
dirty = true
|
||||
|
||||
## Returns the associated [DeckNodeRendererGraphNode] for the supplied [DeckNode].
|
||||
## Or [code]null[/code] if none is found.
|
||||
|
@ -96,23 +120,20 @@ func _on_scroll_offset_changed(offset: Vector2) -> void:
|
|||
|
||||
## Setups all the data from the set [member deck] in this [DeckRendererGraphEdit]
|
||||
func initialize_from_deck() -> void:
|
||||
change_dirty = false
|
||||
for i in get_children():
|
||||
i.queue_free()
|
||||
|
||||
scroll_offset = deck.get_meta("offset", Vector2())
|
||||
|
||||
is_group = deck.is_group
|
||||
for node_id in deck.nodes:
|
||||
var node_renderer: DeckNodeRendererGraphNode = NODE_SCENE.instantiate()
|
||||
node_renderer.node = deck.nodes[node_id]
|
||||
add_child(node_renderer)
|
||||
node_renderer.position_offset = node_renderer.node.position_as_vector2()
|
||||
|
||||
for node_id in deck.nodes:
|
||||
var node: DeckNode = deck.nodes[node_id]
|
||||
var from_node = get_children().filter(
|
||||
func(c: DeckNodeRendererGraphNode):
|
||||
return c.node._id == node_id
|
||||
)[0]
|
||||
change_dirty = true
|
||||
dirty = false
|
||||
|
||||
refresh_connections()
|
||||
|
||||
|
@ -127,20 +148,16 @@ func refresh_connections() -> void:
|
|||
)[0]
|
||||
|
||||
for from_port in node.outgoing_connections:
|
||||
for connection in node.outgoing_connections[from_port]:
|
||||
var to_node_id = connection.keys()[0]
|
||||
var to_node_port = connection.values()[0]
|
||||
var to_node: DeckNodeRendererGraphNode = get_children().filter(
|
||||
for to_node_id: String in node.outgoing_connections[from_port]:
|
||||
var to_node_ports = node.outgoing_connections[from_port][to_node_id]
|
||||
var renderer: Array = get_children().filter(
|
||||
func(c: DeckNodeRendererGraphNode):
|
||||
return c.node._id == to_node_id
|
||||
)[0]
|
||||
#print("***")
|
||||
#print("calling connect_node with:")
|
||||
#print(from_node.node.name)
|
||||
#print(from_node.node.get_port_type_idx_from_global(from_port))
|
||||
#print(to_node.node.name)
|
||||
#print(to_node.node.get_port_type_idx_from_global(to_node_port))
|
||||
#print("***")
|
||||
)
|
||||
if renderer.is_empty():
|
||||
break
|
||||
var to_node: DeckNodeRendererGraphNode = renderer[0]
|
||||
for to_node_port: int in to_node_ports:
|
||||
connect_node(
|
||||
from_node.name,
|
||||
from_port,
|
||||
|
@ -155,6 +172,8 @@ func _on_deck_node_added(node: DeckNode) -> void:
|
|||
inst.node = node
|
||||
add_child(inst)
|
||||
inst.position_offset = inst.node.position_as_vector2()
|
||||
dirty = true
|
||||
|
||||
|
||||
## Connected to [signal Deck.node_added], used to remove the specified
|
||||
## [DeckNodeRendererGraphNode] and queue_free it.
|
||||
|
@ -165,6 +184,7 @@ func _on_deck_node_removed(node: DeckNode) -> void:
|
|||
|
||||
renderer.queue_free()
|
||||
break
|
||||
dirty = true
|
||||
|
||||
## Utility function that gets all [DeckNodeRenderGraphNode]s that are selected
|
||||
## See [member GraphNode.selected]
|
||||
|
@ -178,7 +198,6 @@ func get_selected_nodes() -> Array:
|
|||
## based off the action "group_nodes".
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("group_nodes") && get_selected_nodes().size() > 0:
|
||||
print("?")
|
||||
clear_connections()
|
||||
var nodes = get_selected_nodes().map(
|
||||
func(x: DeckNodeRendererGraphNode):
|
||||
|
@ -188,6 +207,14 @@ func _gui_input(event: InputEvent) -> void:
|
|||
refresh_connections()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
if event.is_action_pressed("rename_node") && get_selected_nodes().size() == 1:
|
||||
var node: DeckNodeRendererGraphNode = get_selected_nodes()[0]
|
||||
var pos := get_viewport_rect().position + get_global_mouse_position()
|
||||
rename_popup.popup_on_parent(Rect2i(pos, rename_popup_size))
|
||||
rename_popup.le.size.x = rename_popup_size.x
|
||||
rename_popup.set_text(node.title)
|
||||
|
||||
|
||||
## Handles entering groups with action "enter_group". Done here to bypass neighbor
|
||||
## functionality.
|
||||
func _input(event: InputEvent) -> void:
|
||||
|
@ -197,10 +224,21 @@ func _input(event: InputEvent) -> void:
|
|||
if event.is_action_pressed("enter_group") && get_selected_nodes().size() == 1:
|
||||
if !((get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.node_type == "group_node"):
|
||||
return
|
||||
print("tried to enter group")
|
||||
group_enter_requested.emit((get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.group_id)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _on_rename_popup_rename_confirmed(new_name: String) -> void:
|
||||
var node: DeckNodeRendererGraphNode = get_selected_nodes()[0]
|
||||
node.title = new_name
|
||||
node.node.rename(new_name)
|
||||
dirty = true
|
||||
|
||||
|
||||
func _on_rename_popup_closed() -> void:
|
||||
pass
|
||||
|
||||
|
||||
## Opens [member search_popup_panel] at the mouse position. Connected to [signal GraphEdit.popup_request]
|
||||
func _on_popup_request(p_popup_position: Vector2) -> void:
|
||||
var p := get_viewport_rect().position + get_global_mouse_position()
|
||||
|
@ -223,3 +261,93 @@ func _on_add_node_menu_node_selected(type: String) -> void:
|
|||
get_node_renderer(node).position_offset = node_pos
|
||||
|
||||
search_popup_panel.hide()
|
||||
|
||||
|
||||
func _on_delete_nodes_request(nodes: Array[StringName]) -> void:
|
||||
var node_ids := nodes.map(
|
||||
func(n: StringName):
|
||||
return (get_node(NodePath(n)) as DeckNodeRendererGraphNode).node._id
|
||||
)
|
||||
|
||||
clear_connections()
|
||||
|
||||
if node_ids.is_empty():
|
||||
return
|
||||
|
||||
for node_id in node_ids:
|
||||
deck.remove_node(node_id, true)
|
||||
|
||||
refresh_connections()
|
||||
dirty = true
|
||||
|
||||
|
||||
func _on_copy_nodes_request() -> void:
|
||||
var selected := get_selected_nodes()
|
||||
if selected.is_empty():
|
||||
return
|
||||
|
||||
var selected_ids: Array[String] = []
|
||||
selected_ids.assign(selected.map(
|
||||
func(x: DeckNodeRendererGraphNode):
|
||||
return x.node._id
|
||||
))
|
||||
|
||||
DisplayServer.clipboard_set(deck.copy_nodes_json(selected_ids))
|
||||
|
||||
|
||||
func _on_paste_nodes_request() -> void:
|
||||
var clip := DisplayServer.clipboard_get()
|
||||
var node_pos := (get_local_mouse_position() + scroll_offset - (Vector2(75.0, 0.0) * zoom)) / zoom
|
||||
|
||||
if snapping_enabled:
|
||||
node_pos = node_pos.snapped(Vector2(snapping_distance, snapping_distance))
|
||||
|
||||
clear_connections()
|
||||
|
||||
deck.paste_nodes_from_json(clip, node_pos)
|
||||
|
||||
refresh_connections()
|
||||
dirty = true
|
||||
|
||||
|
||||
func _on_duplicate_nodes_request() -> void:
|
||||
var selected := get_selected_nodes()
|
||||
if selected.is_empty():
|
||||
return
|
||||
|
||||
var selected_ids: Array[String] = []
|
||||
selected_ids.assign(selected.map(
|
||||
func(x: DeckNodeRendererGraphNode):
|
||||
return x.node._id
|
||||
))
|
||||
|
||||
clear_connections()
|
||||
|
||||
deck.duplicate_nodes(selected_ids)
|
||||
|
||||
refresh_connections()
|
||||
dirty = true
|
||||
|
||||
|
||||
class RenamePopup extends Popup:
|
||||
var le := LineEdit.new()
|
||||
|
||||
signal rename_confirmed(new_text: String)
|
||||
|
||||
func _ready() -> void:
|
||||
add_child(le)
|
||||
le.placeholder_text = "New Name"
|
||||
le.text_submitted.connect(
|
||||
func(new_text: String):
|
||||
if new_text.is_empty():
|
||||
return
|
||||
|
||||
rename_confirmed.emit(new_text)
|
||||
hide()
|
||||
)
|
||||
|
||||
|
||||
func set_text(text: String) -> void:
|
||||
le.text = text
|
||||
le.select_all()
|
||||
le.grab_focus()
|
||||
|
|
|
@ -12,5 +12,9 @@ right_disconnects = true
|
|||
show_arrange_button = false
|
||||
script = ExtResource("1_pojfs")
|
||||
|
||||
[connection signal="copy_nodes_request" from="." to="." method="_on_copy_nodes_request"]
|
||||
[connection signal="delete_nodes_request" from="." to="." method="_on_delete_nodes_request"]
|
||||
[connection signal="duplicate_nodes_request" from="." to="." method="_on_duplicate_nodes_request"]
|
||||
[connection signal="paste_nodes_request" from="." to="." method="_on_paste_nodes_request"]
|
||||
[connection signal="popup_request" from="." to="." method="_on_popup_request"]
|
||||
[connection signal="scroll_offset_changed" from="." to="." method="_on_scroll_offset_changed"]
|
||||
|
|
46
graph_node_renderer/logger_renderer.gd
Normal file
46
graph_node_renderer/logger_renderer.gd
Normal file
|
@ -0,0 +1,46 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends PanelContainer
|
||||
class_name LoggerRenderer
|
||||
|
||||
@onready var output_label: RichTextLabel = %OutputLabel
|
||||
@onready var copy_button: Button = %CopyButton
|
||||
@onready var clear_button: Button = %ClearButton
|
||||
|
||||
const COLORS := {
|
||||
Logger.LogType.INFO: Color.WHITE,
|
||||
Logger.LogType.WARN: Color("f4f486"),
|
||||
Logger.LogType.ERROR: Color("f47c7c"),
|
||||
}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
clear_button.pressed.connect(
|
||||
func():
|
||||
output_label.clear()
|
||||
output_label.text = ""
|
||||
output_label.push_context()
|
||||
)
|
||||
DeckHolder.logger.log_message.connect(_on_logger_log_message)
|
||||
output_label.push_context()
|
||||
|
||||
# the copy button is disabled for now because it doesnt work
|
||||
copy_button.pressed.connect(
|
||||
func():
|
||||
var c = output_label.get_text()
|
||||
DisplayServer.clipboard_set(output_label.get_text())
|
||||
)
|
||||
|
||||
|
||||
func _on_logger_log_message(text: String, type: Logger.LogType, category: Logger.LogCategory) -> void:
|
||||
output_label.pop_context()
|
||||
output_label.push_context()
|
||||
var category_text: String = Logger.LogCategory.keys()[category].capitalize()
|
||||
category_text = "(%s)" % category_text
|
||||
output_label.push_bold()
|
||||
output_label.add_text("%s: " % category_text)
|
||||
output_label.pop()
|
||||
output_label.push_color(COLORS[type])
|
||||
output_label.add_text(text)
|
||||
output_label.newline()
|
79
graph_node_renderer/logger_renderer.tscn
Normal file
79
graph_node_renderer/logger_renderer.tscn
Normal file
|
@ -0,0 +1,79 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://duvh3r740w2p5"]
|
||||
|
||||
[ext_resource type="Script" path="res://graph_node_renderer/logger_renderer.gd" id="1_82rlk"]
|
||||
|
||||
[sub_resource type="SystemFont" id="SystemFont_1dh4a"]
|
||||
font_names = PackedStringArray("Monospace")
|
||||
|
||||
[sub_resource type="SystemFont" id="SystemFont_0uj5d"]
|
||||
font_names = PackedStringArray("Monospace")
|
||||
font_weight = 700
|
||||
|
||||
[node name="LoggerRenderer" type="PanelContainer"]
|
||||
anchors_preset = -1
|
||||
anchor_right = 0.42016
|
||||
anchor_bottom = 0.181704
|
||||
offset_right = -0.0240021
|
||||
offset_bottom = 0.255997
|
||||
script = ExtResource("1_82rlk")
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 3
|
||||
theme_override_constants/margin_top = 3
|
||||
theme_override_constants/margin_right = 3
|
||||
theme_override_constants/margin_bottom = 3
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Debug Console"
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/HBoxContainer/PanelContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 3
|
||||
theme_override_constants/margin_top = 3
|
||||
theme_override_constants/margin_right = 3
|
||||
theme_override_constants/margin_bottom = 3
|
||||
|
||||
[node name="OutputLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/HBoxContainer/PanelContainer/MarginContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
focus_mode = 2
|
||||
theme_override_fonts/normal_font = SubResource("SystemFont_1dh4a")
|
||||
theme_override_fonts/bold_font = SubResource("SystemFont_0uj5d")
|
||||
theme_override_font_sizes/normal_font_size = 12
|
||||
theme_override_font_sizes/bold_font_size = 13
|
||||
scroll_following = true
|
||||
context_menu_enabled = true
|
||||
threaded = true
|
||||
selection_enabled = true
|
||||
deselect_on_focus_loss_enabled = false
|
||||
drag_and_drop_selection_enabled = false
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="CopyButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
text = "Copy"
|
||||
|
||||
[node name="ClearButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Clear"
|
63
graph_node_renderer/obs_websocket_setup_dialog.gd
Normal file
63
graph_node_renderer/obs_websocket_setup_dialog.gd
Normal file
|
@ -0,0 +1,63 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends ConfirmationDialog
|
||||
class_name OBSWebsocketSetupDialog
|
||||
|
||||
@onready var port_spin_box: SpinBox = %PortSpinBox
|
||||
@onready var password_line_edit: LineEdit = %PasswordLineEdit
|
||||
@onready var connect_button: Button = %ConnectButton
|
||||
|
||||
signal connect_button_pressed(state: ConnectionState)
|
||||
|
||||
enum ConnectionState {
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
}
|
||||
var state: ConnectionState
|
||||
|
||||
@onready var _old_port: int = port_spin_box.value
|
||||
@onready var _old_password: String = password_line_edit.text
|
||||
|
||||
|
||||
func get_port() -> int:
|
||||
return int(port_spin_box.value)
|
||||
|
||||
|
||||
func get_password() -> String:
|
||||
return password_line_edit.text
|
||||
|
||||
|
||||
func set_button_state(p_state: ConnectionState) -> void:
|
||||
connect_button.disabled = p_state == ConnectionState.CONNECTING
|
||||
state = p_state
|
||||
|
||||
match p_state:
|
||||
ConnectionState.DISCONNECTED:
|
||||
connect_button.text = "Connect"
|
||||
ConnectionState.CONNECTING:
|
||||
connect_button.text = "Connecting..."
|
||||
ConnectionState.CONNECTED:
|
||||
connect_button.text = "Disconnect"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
connect_button.pressed.connect(
|
||||
func():
|
||||
connect_button_pressed.emit(state)
|
||||
)
|
||||
|
||||
canceled.connect(
|
||||
func():
|
||||
print("canceled")
|
||||
port_spin_box.value = float(_old_port)
|
||||
password_line_edit.text = _old_password
|
||||
)
|
||||
|
||||
confirmed.connect(
|
||||
func():
|
||||
print("confirmed")
|
||||
_old_port = int(port_spin_box.value)
|
||||
_old_password = password_line_edit.text
|
||||
)
|
51
graph_node_renderer/obs_websocket_setup_dialog.tscn
Normal file
51
graph_node_renderer/obs_websocket_setup_dialog.tscn
Normal file
|
@ -0,0 +1,51 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://eioso6jb42jy"]
|
||||
|
||||
[ext_resource type="Script" path="res://graph_node_renderer/obs_websocket_setup_dialog.gd" id="1_6ggla"]
|
||||
|
||||
[node name="OBSWebsocketSetupDialog" type="ConfirmationDialog"]
|
||||
title = "OBS Websocket Setup"
|
||||
initial_position = 4
|
||||
size = Vector2i(500, 300)
|
||||
script = ExtResource("1_6ggla")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = 492.0
|
||||
offset_bottom = 251.0
|
||||
|
||||
[node name="GridContainer" type="GridContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
columns = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/GridContainer"]
|
||||
layout_mode = 2
|
||||
text = "Port"
|
||||
|
||||
[node name="PortSpinBox" type="SpinBox" parent="VBoxContainer/GridContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
max_value = 25565.0
|
||||
value = 4455.0
|
||||
|
||||
[node name="Label2" type="Label" parent="VBoxContainer/GridContainer"]
|
||||
layout_mode = 2
|
||||
text = "Password"
|
||||
|
||||
[node name="PasswordLineEdit" type="LineEdit" parent="VBoxContainer/GridContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
secret = true
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_bottom = 20
|
||||
|
||||
[node name="CenterContainer" type="CenterContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ConnectButton" type="Button" parent="VBoxContainer/CenterContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Connect"
|
|
@ -1,34 +1,39 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends VBoxContainer
|
||||
class_name TabContainerCustom
|
||||
|
||||
## Custom Recreation of [TabContainer] for Flexibility
|
||||
##
|
||||
## Allows for more customizability within the [TabBar] thats used mainly. Extra buttons etc.
|
||||
## Alternative to [TabContainer]. Instead of using the tree hierarchy to add tabs directly,
|
||||
## tabs must be created by script.
|
||||
|
||||
## Reference to the [TabBar] at the top of the Container.
|
||||
## Reference to the [TabBar] at the top of the container.
|
||||
@onready var tab_bar: TabBar = %TabBar
|
||||
## Reference to the [Button] at the end of the [TabBar] that's
|
||||
## used for adding new Tabs
|
||||
## used for adding new tabs.
|
||||
@onready var add_tab_button: Button = %Button
|
||||
## Reference to the [MarginContainer] around the Tabs Contents.
|
||||
## Reference to the [MarginContainer] around the tab's contents.
|
||||
@onready var content_container: MarginContainer = %ContentContainer
|
||||
|
||||
## Emitted when the add [Button] within [member tab_bar] is pressed
|
||||
## Emitted when the add [Button] within [member tab_bar] is pressed.
|
||||
signal add_button_pressed
|
||||
## Emitted when the current tab in [member tab_bar] is changed.
|
||||
signal tab_changed(tab: int)
|
||||
## Emitted when a tab in [member tab_bar] has been closed.
|
||||
## See [signal TabBar.tab_close_requested]
|
||||
signal tab_closed(tab: int)
|
||||
## Emitted when a request to close a tab in the [member tab_bar] has been
|
||||
## requested using [signal TabBar.tab_close_pressed]
|
||||
## Emitted when a request to close a tab in the [member tab_bar] has been requested.
|
||||
signal tab_close_requested(tab: int)
|
||||
## Emitted when the order of the tabs in [member tab_bar] has been changed.
|
||||
signal tab_rearranged(old: int, new: int)
|
||||
|
||||
## Holds the previously active tab in [member tab_bar]
|
||||
# Holds the previously active tab in the internal tab_bar
|
||||
var _previous_active_tab: int = -1
|
||||
|
||||
var _tab_metadata: Array[Dictionary] #Array[Dictionary[String -> key, Variant]]
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
tab_bar.tab_selected.connect(
|
||||
|
@ -53,49 +58,84 @@ func _ready() -> void:
|
|||
|
||||
tab_bar.active_tab_rearranged.connect(
|
||||
func(idx_to: int):
|
||||
var old := _tab_metadata[_previous_active_tab]
|
||||
var new := _tab_metadata[idx_to]
|
||||
_tab_metadata[idx_to] = old
|
||||
_tab_metadata[_previous_active_tab] = new
|
||||
tab_rearranged.emit(_previous_active_tab, idx_to)
|
||||
content_container.move_child(content_container.get_child(_previous_active_tab), idx_to)
|
||||
_previous_active_tab = idx_to
|
||||
)
|
||||
|
||||
## Adds the given [Node] as "content" for the given tabs name as a [String]
|
||||
func add_content(c: Node, tab_title: String) -> void:
|
||||
## Adds the given [Node] as the displayed content for a tab.
|
||||
func add_content(c: Node, tab_title: String) -> int:
|
||||
tab_bar.add_tab(tab_title)
|
||||
content_container.add_child(c)
|
||||
tab_bar.set_current_tab(tab_bar.tab_count - 1)
|
||||
#tab_bar.set_current_tab(tab_bar.tab_count - 1)
|
||||
_tab_metadata.append({})
|
||||
return tab_bar.tab_count - 1
|
||||
|
||||
## Returns the count of tabs in [member tab_bar]
|
||||
|
||||
## Updates the tab at index [param tab_idx]'s title to [param title].
|
||||
func set_tab_title(tab_idx: int, title: String) -> void:
|
||||
tab_bar.set_tab_title(tab_idx, title)
|
||||
|
||||
|
||||
func get_tab_title(tab_idx: int) -> String:
|
||||
return tab_bar.get_tab_title(tab_idx)
|
||||
|
||||
|
||||
## Returns the number of tabs.
|
||||
func get_tab_count() -> int:
|
||||
return tab_bar.tab_count
|
||||
|
||||
## Returns [code]true[/code] if [method get_tab_count] returns 0.
|
||||
|
||||
## Returns [code]true[/code] if the tab bar has no tabs.
|
||||
func is_empty() -> bool:
|
||||
return get_tab_count() == 0
|
||||
|
||||
## Closes the tab that is at the given [param tab] in [member tab_bar]
|
||||
|
||||
## Closes a tab at the index [param tab].
|
||||
func close_tab(tab: int) -> void:
|
||||
content_container.get_child(tab).queue_free()
|
||||
if !tab_bar.select_previous_available():
|
||||
tab_bar.select_next_available()
|
||||
tab_bar.remove_tab(tab)
|
||||
_tab_metadata.remove_at(tab)
|
||||
tab_closed.emit(tab)
|
||||
if tab_bar.tab_count == 0:
|
||||
_previous_active_tab = -1
|
||||
|
||||
## Returns the currently selected tab in [member tab_bar]
|
||||
|
||||
## Returns the currently selected tab.
|
||||
func get_current_tab() -> int:
|
||||
return tab_bar.current_tab
|
||||
|
||||
## Returns the child of [member content_container] at the given [param idx]
|
||||
|
||||
## Sets the current tab to the tab at [param idx].
|
||||
func set_current_tab(idx: int) -> void:
|
||||
tab_bar.current_tab = idx
|
||||
|
||||
|
||||
## Returns the child of [member content_container] at the [param idx].
|
||||
func get_content(idx: int) -> Control:
|
||||
return content_container.get_child(idx)
|
||||
|
||||
## Sets the metadata value for the tab at index [param tab_idx], which can be
|
||||
## retrieved later using [method TabBar.get_tab_metadata()]
|
||||
func set_tab_metadata(tab: int, metadata: Variant) -> void:
|
||||
tab_bar.set_tab_metadata(tab, metadata)
|
||||
|
||||
## Returns the metadata value set to the tab at index [param tab_idx] using set_tab_metadata().
|
||||
## If no metadata was previously set, returns null by default.
|
||||
func get_tab_metadata(tab: int) -> Variant:
|
||||
return tab_bar.get_tab_metadata(tab)
|
||||
## Sets the metadata value for the tab at index [param tab_idx] at [param key], which can be
|
||||
## retrieved later using [method get_tab_metadata].
|
||||
func set_tab_metadata(tab: int, key: String, value: Variant) -> void:
|
||||
#var m = _tab_metadata.get(tab, {})
|
||||
#m[key] = value
|
||||
#_tab_metadata[tab] = m
|
||||
var m := _tab_metadata[tab]
|
||||
m[key] = value
|
||||
|
||||
|
||||
## Returns the metadata value set to the tab at index [param tab_idx] using [method set_tab_metadata].
|
||||
## If no metadata was previously set, returns [code]null[/code] by default.
|
||||
func get_tab_metadata(tab: int, key: String, default: Variant = null) -> Variant:
|
||||
if _tab_metadata.size() - 1 < tab:
|
||||
return default
|
||||
var m = _tab_metadata[tab]
|
||||
return m.get(key, default)
|
||||
|
|
21
graph_node_renderer/twitch_setup_dialog.gd
Normal file
21
graph_node_renderer/twitch_setup_dialog.gd
Normal file
|
@ -0,0 +1,21 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends ConfirmationDialog
|
||||
class_name TwitchSetupDialog
|
||||
|
||||
func _ready():
|
||||
|
||||
%Authenticate.pressed.connect(authenticate_twitch)
|
||||
confirmed.connect(connect_to_chat)
|
||||
|
||||
|
||||
func authenticate_twitch():
|
||||
|
||||
Connections.twitch.authenticate_with_twitch(%Client_ID.text)
|
||||
|
||||
|
||||
func connect_to_chat():
|
||||
|
||||
Connections.twitch.setup_chat_connection(%Default_Chat.text)
|
||||
|
49
graph_node_renderer/twitch_setup_dialog.tscn
Normal file
49
graph_node_renderer/twitch_setup_dialog.tscn
Normal file
|
@ -0,0 +1,49 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://bq2lxmbnic4lc"]
|
||||
|
||||
[ext_resource type="Script" path="res://graph_node_renderer/twitch_setup_dialog.gd" id="1_xx7my"]
|
||||
|
||||
[node name="twitch_setup_dialog" type="ConfirmationDialog"]
|
||||
title = "Twitch Connection Setup"
|
||||
position = Vector2i(0, 36)
|
||||
size = Vector2i(375, 158)
|
||||
min_size = Vector2i(375, 150)
|
||||
script = ExtResource("1_xx7my")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -179.5
|
||||
offset_top = 8.0
|
||||
offset_right = 179.5
|
||||
offset_bottom = 109.0
|
||||
grow_horizontal = 2
|
||||
size_flags_horizontal = 4
|
||||
|
||||
[node name="Authentication" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Client_ID" type="LineEdit" parent="VBoxContainer/Authentication"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
tooltip_text = "The Twitch Client ID used by this Connection for it's Authentication. Defaults to the built in Client ID that the program ships with."
|
||||
placeholder_text = "Custom Client ID (Optional)"
|
||||
|
||||
[node name="Authenticate" type="Button" parent="VBoxContainer/Authentication"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 0.25
|
||||
text = "Authenticate"
|
||||
|
||||
[node name="CheckBox" type="CheckBox" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
disabled = true
|
||||
button_pressed = true
|
||||
text = "Extra Chat Info"
|
||||
|
||||
[node name="Default_Chat" type="LineEdit" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
placeholder_text = "Default Chat Channel"
|
16
graph_node_renderer/unsaved_changes_dialog.tscn
Normal file
16
graph_node_renderer/unsaved_changes_dialog.tscn
Normal file
|
@ -0,0 +1,16 @@
|
|||
[gd_scene format=3 uid="uid://cvvkj138fg8jg"]
|
||||
|
||||
[node name="UnsavedChangesDialog" type="ConfirmationDialog"]
|
||||
initial_position = 2
|
||||
size = Vector2i(391, 206)
|
||||
ok_button_text = "Quit without saving"
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = 383.0
|
||||
offset_bottom = 157.0
|
||||
text = "You have unsaved changes. Quitting now will discard them. Are you sure you want to quit?"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 3
|
|
@ -0,0 +1,9 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends ConfirmationDialog
|
||||
class_name UnsavedChangesDialogSingleDeck
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
add_button("Don't Save", true, "force_close")
|
20
graph_node_renderer/unsaved_changes_dialog_single_deck.tscn
Normal file
20
graph_node_renderer/unsaved_changes_dialog_single_deck.tscn
Normal file
|
@ -0,0 +1,20 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://cuwou2aa7qfc2"]
|
||||
|
||||
[ext_resource type="Script" path="res://graph_node_renderer/unsaved_changes_dialog_single_deck.gd" id="1_qpf7a"]
|
||||
|
||||
[node name="UnsavedChangesDialogSingleDeck" type="ConfirmationDialog"]
|
||||
initial_position = 2
|
||||
size = Vector2i(356, 206)
|
||||
ok_button_text = "Save"
|
||||
script = ExtResource("1_qpf7a")
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = 348.0
|
||||
offset_bottom = 157.0
|
||||
text = "This deck has unsaved changes. Closing it will discard any changes.
|
||||
Save now?"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 3
|
0
img/.gdignore
Normal file
0
img/.gdignore
Normal file
BIN
img/example1.png
Normal file
BIN
img/example1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
img/example2.png
Normal file
BIN
img/example2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
|
@ -1,54 +0,0 @@
|
|||
extends HBoxContainer
|
||||
|
||||
@onready var left_slot: ColorRect = $LeftSlot
|
||||
@onready var right_slot: ColorRect = $RightSlot
|
||||
|
||||
var text: String
|
||||
|
||||
signal button_pressed
|
||||
|
||||
|
||||
func set_input_enabled(enabled: bool) -> void:
|
||||
left_slot.visible = enabled
|
||||
|
||||
|
||||
func set_output_enabled(enabled: bool) -> void:
|
||||
right_slot.visible = enabled
|
||||
|
||||
|
||||
func add_label(text: String) -> void:
|
||||
var l := Label.new()
|
||||
add_child(l)
|
||||
l.text = text
|
||||
move_child(l, 1)
|
||||
l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
|
||||
func add_field(placeholder: String = "") -> void:
|
||||
var le := LineEdit.new()
|
||||
add_child(le)
|
||||
move_child(le, 1)
|
||||
le.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
le.placeholder_text = placeholder
|
||||
|
||||
le.text_changed.connect(
|
||||
func(new_text: String):
|
||||
text = new_text
|
||||
)
|
||||
|
||||
|
||||
func get_text() -> String:
|
||||
return text
|
||||
|
||||
|
||||
func add_button(text: String) -> void:
|
||||
var b := Button.new()
|
||||
b.text = text
|
||||
add_child(b)
|
||||
move_child(b, 1)
|
||||
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
b.pressed.connect(
|
||||
func():
|
||||
button_pressed.emit()
|
||||
)
|
|
@ -1,16 +0,0 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://ddqtmahfxel26"]
|
||||
|
||||
[ext_resource type="Script" path="res://port_drawer.gd" id="1_wot5w"]
|
||||
|
||||
[node name="PortDrawer" type="HBoxContainer"]
|
||||
script = ExtResource("1_wot5w")
|
||||
|
||||
[node name="LeftSlot" type="ColorRect" parent="."]
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(12, 12)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="RightSlot" type="ColorRect" parent="."]
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(12, 12)
|
||||
layout_mode = 2
|
|
@ -10,22 +10,23 @@ config_version=5
|
|||
|
||||
[application]
|
||||
|
||||
config/name="Re-DotDeck"
|
||||
config/name="StreamGraph"
|
||||
config/version="0.0.1"
|
||||
config/tags=PackedStringArray("dot_deck")
|
||||
run/main_scene="res://graph_node_renderer/deck_holder_renderer.tscn"
|
||||
config/use_custom_user_dir=true
|
||||
config/custom_user_dir_name="dotdeck"
|
||||
config/custom_user_dir_name="streamgraph"
|
||||
config/auto_accept_quit=false
|
||||
config/features=PackedStringArray("4.2", "Forward Plus")
|
||||
run/low_processor_mode=true
|
||||
config/icon="res://icon.svg"
|
||||
config/icon="res://dist/logo-flattened.svg"
|
||||
|
||||
[autoload]
|
||||
|
||||
NodeDB="*res://classes/deck/node_db.gd"
|
||||
|
||||
[display]
|
||||
[editor_plugins]
|
||||
|
||||
window/subwindows/embed_subwindows=false
|
||||
enabled=PackedStringArray("res://addons/no-obs-ws/plugin.cfg", "res://addons/no_twitch/plugin.cfg")
|
||||
|
||||
[input]
|
||||
|
||||
|
@ -39,6 +40,16 @@ enter_group={
|
|||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
rename_node={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194333,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
toggle_console={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":78,"key_label":0,"unicode":110,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[rendering]
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# (c) 2023-present Eroax
|
||||
# (c) 2023-present Yagich
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
extends DeckNode
|
||||
|
||||
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
extends PanelContainer
|
||||
|
||||
@onready var name_label: Label = %NameLabel
|
||||
@onready var elements_container: VBoxContainer = %ElementsContainer
|
||||
|
||||
var node: DeckNode
|
||||
const PortDrawer := preload("res://port_drawer.gd")
|
||||
var port_drawer_scene := preload("res://port_drawer.tscn")
|
||||
|
||||
# THIS IS SUPER JANK AND A HACK FOR DEMONSTRATION PURPOSES
|
||||
# PLEASE DO NOT ACTUALLY DO ANYTHING THIS CLASS DOES
|
||||
# IN THE REAL PROJECT !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
func _ready() -> void:
|
||||
name_label.text = node.name
|
||||
for i in node.input_ports.size():
|
||||
var input_port := node.input_ports[i]
|
||||
var port_drawer: PortDrawer = port_drawer_scene.instantiate()
|
||||
elements_container.add_child(port_drawer)
|
||||
port_drawer.set_input_enabled(true)
|
||||
match input_port.descriptor:
|
||||
"field":
|
||||
port_drawer.add_field(input_port.label)
|
||||
input_port.value_callback = port_drawer.get_text
|
||||
"button":
|
||||
port_drawer.add_button(input_port.label)
|
||||
port_drawer.button_pressed.connect(func():
|
||||
node._receive(i, DeckType.DeckTypeString.new("memes"))
|
||||
)
|
||||
_:
|
||||
port_drawer.add_label(input_port.label)
|
||||
|
||||
for i in node.output_ports.size():
|
||||
if elements_container.get_child_count() - 1 < i:
|
||||
var pd: PortDrawer = port_drawer_scene.instantiate()
|
||||
elements_container.add_child(pd)
|
||||
var port_drawer: PortDrawer = elements_container.get_child(i)
|
||||
port_drawer.set_output_enabled(true)
|
||||
var output_port := node.output_ports[i]
|
||||
match output_port.descriptor:
|
||||
"field":
|
||||
port_drawer.add_field(output_port.label)
|
||||
output_port.value_callback = port_drawer.get_text
|
||||
"button":
|
||||
port_drawer.add_button(output_port.label)
|
||||
port_drawer.button_pressed.connect(func():
|
||||
node.send(i, DeckType.DeckTypeBool.new(true))
|
||||
)
|
||||
_:
|
||||
port_drawer.add_label(output_port.label)
|
||||
|
||||
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://ch8s1d7vobhi4"]
|
||||
|
||||
[ext_resource type="Script" path="res://test_node_renderer.gd" id="1_85wy1"]
|
||||
|
||||
[node name="TestNodeRenderer" type="PanelContainer"]
|
||||
custom_minimum_size = Vector2(300, 0)
|
||||
anchors_preset = -1
|
||||
anchor_right = 0.26
|
||||
anchor_bottom = 0.34
|
||||
offset_right = 0.47998
|
||||
offset_bottom = -0.320007
|
||||
script = ExtResource("1_85wy1")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ColorRect" type="ColorRect" parent="VBoxContainer/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(4, 0)
|
||||
layout_mode = 2
|
||||
color = Color(1, 1, 0.34902, 1)
|
||||
|
||||
[node name="NameLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Name"
|
||||
|
||||
[node name="HSeparator2" type="HSeparator" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ElementsContainer" type="VBoxContainer" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
Loading…
Reference in a new issue