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:
Lera Elvoé 2023-12-15 21:44:25 +00:00 committed by yagich
parent dc7d53e3d2
commit b55a462945
98 changed files with 12010 additions and 461 deletions

3
.gitignore vendored
View file

@ -1,2 +1,5 @@
# Godot 4+ specific ignores # Godot 4+ specific ignores
.godot/ .godot/
# distribution folder
dist/*/

674
COPYING Normal file
View 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
View 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
View 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.
```

View 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
View 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]

View 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]

View 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,
}

File diff suppressed because it is too large Load diff

View 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"

View 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

View 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)

View 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)

View 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

View 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")

View file

@ -0,0 +1,9 @@
extends Button
@onready var twitch_connection : Twitch_Connection = $"../../Twitch_Connection"
func _pressed():
twitch_connection.authenticate_with_twitch()

View file

@ -0,0 +1,4 @@
extends Resource
class_name TokenSaver
@export var token : String

View file

@ -0,0 +1,7 @@
[plugin]
name="No Twitch"
description=""
author="Eroax"
version="0.0.1"
script="plugin.gd"

View 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

View 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)

View 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())

View 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)

View file

@ -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 class_name Deck
## A deck/graph with nodes. ## A deck/graph with nodes.
## ##
@ -16,11 +19,13 @@ var save_path: String = ""
var is_group: bool = false var is_group: bool = false
## List of groups belonging to this deck, in the format of[br] ## List of groups belonging to this deck, in the format of[br]
## [code]Dictionary[String -> Deck.id, Deck][/code] ## [code]Dictionary[String -> Deck.id, Deck][/code]
var groups: Dictionary = {} #var groups: Dictionary = {}
## A unique identifier for this deck. ## A unique identifier for this deck, or an ID for the group this deck represents.
var id: String = "" 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. ## 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]. ## The ID of this group's input node. Used only if [member is_group] is [code]true[/code].
var group_input_node: String var group_input_node: String
## The ID of this group's input node. Used only if [member is_group] is [code]true[/code]. ## 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. ## 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]. ## Used only if [member is_group] is [code]true[/code].
## @experimental ## @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. ## Emitted when a node has been added to this deck.
signal node_added(node: DeckNode) signal node_added(node: DeckNode)
## Emitted when a node has been removed from this deck. ## Emitted when a node has been removed from this deck.
signal node_removed(node: DeckNode) 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] ## Instantiate a node by its' [member DeckNode.node_type] and add it to this deck.[br]
## See [method add_node_inst] for parameter descriptions. ## See [method add_node_inst] for parameter descriptions.
@ -59,8 +76,31 @@ func add_node_inst(node: DeckNode, assign_id: String = "", assign_to_self: bool
node._id = uuid node._id = uuid
else: else:
nodes[assign_id] = node nodes[assign_id] = node
node._id = assign_id
node_added.emit(node) 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 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. ## 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 # check that we can do the type conversion
var type_a: DeckType.Types = from_node.get_output_ports()[from_output_port].type 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 var type_b: DeckType.Types = to_node.get_input_ports()[to_input_port].type
if !DeckType.can_convert(type_a, type_b): 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 return false
# TODO: prevent duplicate connections # 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) from_node.add_outgoing_connection(from_output_port, to_node._id, to_input_port)
return true return true
## Remove a connection from two nodes. ## Remove a connection from two nodes.
func disconnect_nodes(from_node: DeckNode, to_node: DeckNode, from_output_port: int, to_input_port: int) -> void: func disconnect_nodes(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> void:
var hash = {to_node._id: to_input_port}.hash() var from_node := get_node(from_node_id)
var to_node := get_node(to_node_id)
from_node.remove_outgoing_connection(from_output_port, hash) from_node.remove_outgoing_connection(from_output_port, to_node_id, to_input_port)
to_node.remove_incoming_connection(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. ## 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. ## Remove a node from this deck.
func remove_node(uuid: String) -> void: func remove_node(uuid: String, remove_connections: bool = false, force: bool = false, keep_group_instances: bool = false) -> void:
var node = nodes.get(uuid) 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) nodes.erase(uuid)
node_removed.emit(node) 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. ## Group the [param nodes_to_group] into a new deck and return it.
## Returns [code]null[/code] on failure.[br] ## 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(): if nodes_to_group.is_empty():
return null 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( var node_ids_to_keep := nodes_to_group.map(
func(x: DeckNode): func(x: DeckNode):
return x._id return x._id
) )
var group := Deck.new() var group := DeckHolder.add_empty_group()
group.is_group = true
group._belonging_to = self
var group_id := UUID.v4()
group.id = group_id
var midpoint := Vector2() var midpoint := Vector2()
var rightmost := -INF
var leftmost := INF
for node: DeckNode in nodes_to_group: for node: DeckNode in nodes_to_group:
if node.node_type == "group_node": # for recursive grouping #if node.node_type == "group_node":
var _group_id: String = node.group_id #var _group_id: String = node.group_id
var _group: Deck = groups[_group_id] #var _group: Deck = groups[_group_id]
groups.erase(_group) #groups.erase(_group)
group.groups[_group_id] = _group #group.groups[_group_id] = _group
_group._belonging_to = group #_group._belonging_to = group
for from_port: int in node.outgoing_connections: if node.position.x > rightmost:
for connection: Dictionary in node.outgoing_connections[from_port]: rightmost = node.position.x
if !(connection.keys()[0] in node_ids_to_keep): if node.position.x < leftmost:
disconnect_nodes(node, get_node(connection.keys()[0]), from_port, connection.values()[0]) leftmost = node.position.x
for to_port: int in node.incoming_connections: var outgoing_connections := node.outgoing_connections.duplicate(true)
for from_node: String in node.incoming_connections[to_port]:
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): 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() midpoint += node.position_as_vector2()
remove_node(node._id) remove_node(node._id, false, true)
group.add_node_inst(node, node._id) group.add_node_inst(node, node._id)
midpoint /= nodes_to_group.size() midpoint /= nodes_to_group.size()
emit_node_added_signal = false
var _group_node := add_node_type("group_node") 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.x = midpoint.x
_group_node.position.y = midpoint.y _group_node.position.y = midpoint.y
_group_node.position_updated.emit(_group_node.position) _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 input_node := group.add_node_type("group_input")
var output_node := group.add_node_type("group_output") var output_node := group.add_node_type("group_output")
group.group_input_node = input_node._id group.group_input_node = input_node._id
group.group_output_node = output_node._id group.group_output_node = output_node._id
_group_node.input_node = input_node input_node.position.x = leftmost - 350
_group_node.output_node = output_node output_node.position.x = rightmost + 350
_group_node.setup_connections() 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 return group
## Get a group belonging to this deck by its ID. ## Get a group belonging to this deck by its ID.
func get_group(uuid: String) -> Deck: #func get_group(uuid: String) -> Deck:
return groups.get(uuid) #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. ## 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 := { var inner := {
"nodes": {}, "nodes": {},
"variable_stack": variable_stack, "variable_stack": variable_stack,
@ -184,14 +406,18 @@ func to_dict(with_meta: bool = true) -> Dictionary:
"groups": {} "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) 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(): #for group_id in groups.keys():
inner["groups"][group_id] = groups[group_id].to_dict(with_meta) #inner["groups"][group_id] = groups[group_id].to_dict(with_meta)
if is_group: if is_group:
inner["group_node"] = group_node inner["instance_id"] = instance_id
inner["group_input_node"] = group_input_node inner["group_input_node"] = group_input_node
inner["group_output_node"] = group_output_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 var nodes_data: Dictionary = data.deck.nodes as Dictionary
for node_id in nodes_data: for node_id in nodes_data:
var node := deck.add_node_type(nodes_data[node_id].node_type, node_id, false) var node := DeckNode.from_dict(nodes_data[node_id])
node._id = node_id deck.add_node_inst(node, 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 groups_data: Dictionary = data.deck.groups as Dictionary var groups_data: Dictionary = data.deck.groups as Dictionary
for group_id: String in groups_data: for node_id: String in deck.nodes:
var group := Deck.from_dict(groups_data[group_id]) var node := deck.get_node(node_id)
group._belonging_to = deck if node.node_type != "group_node":
group.is_group = true continue
deck.groups[group_id] = group var group_id: String = node.group_id
group.group_node = groups_data[group_id]["deck"]["group_node"] var group_instance_id: String = node.group_instance_id
group.group_input_node = groups_data[group_id]["deck"]["group_input_node"] var group_data: Dictionary = groups_data[group_id]
group.group_output_node = groups_data[group_id]["deck"]["group_output_node"] var group := DeckHolder.add_group_from_dict(group_data, group_id, group_instance_id)
deck.get_node(group.group_node).init_io() 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 return deck

View file

@ -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 class_name DeckHolder
## @experimental ## @experimental
## A static class holding references to all decks opened in the current session. ## A static class holding references to all decks opened in the current session.
## List of decks opened this 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. ## Returns a new empty deck and assigns a new random ID to it.
static func add_empty_deck() -> Deck: static func add_empty_deck() -> Deck:
var deck := Deck.new() var deck := Deck.new()
DeckHolder.decks.append(deck)
var uuid := UUID.v4() var uuid := UUID.v4()
decks[uuid] = deck
deck.id = uuid deck.id = uuid
return deck return deck
@ -21,12 +27,200 @@ static func open_deck_from_file(path: String) -> Deck:
if f.get_error() != OK: if f.get_error() != OK:
return null return null
var deck := Deck.from_dict(JSON.parse_string(f.get_as_text()), path) var deck := open_deck_from_dict(JSON.parse_string(f.get_as_text()), path)
DeckHolder.decks.append(deck)
return deck return deck
## Unloads a deck. static func open_deck_from_dict(data: Dictionary, path := "") -> Deck:
static func close_deck(deck: Deck) -> void: var deck := Deck.from_dict(data, path)
DeckHolder.decks.erase(deck) 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

View file

@ -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 class_name DeckNode
## A node in a [Deck]. ## A node in a [Deck].
## ##
@ -8,7 +11,7 @@ class_name DeckNode
var name: String var name: String
## A map of outgoing connections from this node, in the format[br] ## 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 var outgoing_connections: Dictionary
## A map of incoming connections to this node, in the format[br] ## 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] ## [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. ## Only used by renderers.
var position: Dictionary = {"x": 0.0, "y": 0.0} 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{ enum PortType{
INPUT, ## Input port type (slot on the left). INPUT, ## Input port type (slot on the left).
OUTPUT, ## Output port type (slot on the right). 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. ## Emitted when a connection to this node has been removed.
signal incoming_connection_removed(from_port: int) 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. ## Add an input port to this node. Usually only used at initialization.
func add_input_port(type: DeckType.Types, label: String, descriptor: String = "") -> void: 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) var port := Port.new(type, label, ports.size(), port_type, index_of_type, descriptor)
ports.append(port) ports.append(port)
port_added.emit(ports.size() - 1) 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() ports_updated.emit()
## Remove a port from this node. ## Remove a port from this node.
## @deprecated
func remove_port(port_idx: int) -> void: func remove_port(port_idx: int) -> void:
outgoing_connections.erase(port_idx) outgoing_connections.erase(port_idx)
incoming_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: if outgoing_connections.get(from_output_port) == null:
return return
for connection in outgoing_connections[from_output_port]: for node: String in outgoing_connections[from_output_port]:
connection = connection as Dictionary for input_port: int in outgoing_connections[from_output_port][node]:
# key is node uuid get_node(node)._receive(input_port, data, extra_data)
# value is input port on destination node
for node in connection:
get_node(node)._receive(connection[node], data, extra_data)
## Virtual function that's called when this node receives data from another node's [method send] call. ## 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: func _receive(to_input_port: int, data: Variant, extra_data: Array = []) -> void:
pass 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 ## Add a connection from the output port at [param from_port] to [param to_node]'s input port
## at [param to_port]. ## at [param to_port].
func add_outgoing_connection(from_port: int, to_node: String, to_port: int) -> void: func add_outgoing_connection(from_port: int, to_node: String, to_port: int) -> void:
var port_connections: Array = outgoing_connections.get(from_port, []) var inner: Dictionary = outgoing_connections.get(from_port, {}) as Dictionary
port_connections.append({to_node: to_port}) var inner_ports: Array = inner.get(to_node, []) as Array
outgoing_connections[from_port] = port_connections 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) get_node(to_node).add_incoming_connection(to_port, _id, from_port)
outgoing_connection_added.emit(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]) 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 ## Virtual function that's called when this node has been requested a value from the output port
## at [param from_port]. ## at [param from_port].
@warning_ignore("unused_parameter")
func _value_request(from_port: int) -> Variant: func _value_request(from_port: int) -> Variant:
return null return null
## Remove an outgoing connection from this node. ## Remove an outgoing connection from this node.
## Does [b]not[/b] remove the other node's incoming connection equivalent. ## Does [b]not[/b] remove the other node's incoming connection equivalent.
func remove_outgoing_connection(from_port: int, connection_hash: int) -> void: func remove_outgoing_connection(from_port: int, to_node: String, to_port: int) -> void:
var port_connections: Array = (outgoing_connections.get(from_port, []) as Array).duplicate(true) var connections: Dictionary = outgoing_connections.get(from_port, {}) as Dictionary
if port_connections.is_empty(): if connections.is_empty():
return return
var incoming_connection := {} var inner_ports: Array = connections.get(to_node, []) as Array
inner_ports.erase(to_port)
var to_remove: int = -1 if inner_ports.is_empty():
for i in port_connections.size(): (outgoing_connections[from_port] as Dictionary).erase(to_node)
if port_connections[i].hash() == connection_hash: 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) outgoing_connection_removed.emit(from_port)
@ -181,6 +202,16 @@ func remove_incoming_connection(to_port: int) -> void:
incoming_connection_removed.emit(to_port) 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. ## Returns a list of all input ports.
func get_input_ports() -> Array[Port]: func get_input_ports() -> Array[Port]:
return ports.filter( return ports.filter(
@ -254,17 +285,34 @@ func _post_load() -> void:
pass 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. ## Returns a [Dictionary] representation of this node.
func to_dict(with_meta: bool = true) -> Dictionary: func to_dict(with_meta: bool = true) -> Dictionary:
var d := { var d := {
"_id": _id, "_id": _id,
"name": name, "name": name,
"outgoing_connections": outgoing_connections, "outgoing_connections": outgoing_connections.duplicate(true),
"incoming_connections": incoming_connections, "incoming_connections": incoming_connections.duplicate(true),
"props": {}, "props": {},
"node_type": node_type, "node_type": node_type,
"port_values": [], "port_values": [],
"position": position, "position": position.duplicate(),
} }
for prop in props_to_serialize: for prop in props_to_serialize:
@ -282,6 +330,48 @@ func to_dict(with_meta: bool = true) -> Dictionary:
return d 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]. ## Returns the node's [member position] as a [Vector2].
func position_as_vector2() -> Vector2: func position_as_vector2() -> Vector2:
return Vector2(position.x, position.y) return Vector2(position.x, position.y)

42
classes/deck/logger.gd Normal file
View 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)

View file

@ -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 extends Node
class_name NodeDB_ class_name NodeDB_
@ -29,7 +32,7 @@ func _init() -> void:
dir.list_dir_begin() dir.list_dir_begin()
var current_file := dir.get_next() var current_file := dir.get_next()
while current_file != "": while current_file != "":
print(current_file) #print(current_file)
if current_file.ends_with(".gd"): if current_file.ends_with(".gd"):
var script_path := BASE_NODE_PATH.path_join(current_file) var script_path := BASE_NODE_PATH.path_join(current_file)
var node: DeckNode = load(script_path).new() as DeckNode var node: DeckNode = load(script_path).new() as DeckNode
@ -74,12 +77,12 @@ func save_node_index() -> void:
func load_node_index() -> bool: func load_node_index() -> bool:
var f := FileAccess.open(NODE_INDEX_CACHE_PATH, FileAccess.READ) var f := FileAccess.open(NODE_INDEX_CACHE_PATH, FileAccess.READ)
if f == null: 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 return false
var data: Dictionary = JSON.parse_string(f.get_as_text()) as Dictionary var data: Dictionary = JSON.parse_string(f.get_as_text()) as Dictionary
if data.is_empty(): 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 return false
for node_type in data: for node_type in data:
@ -87,7 +90,7 @@ func load_node_index() -> bool:
var nd := NodeDescriptor.from_dictionary(nd_dict) var nd := NodeDescriptor.from_dictionary(nd_dict)
nodes[node_type] = nd nodes[node_type] = nd
print("node index file exists, loaded") DeckHolder.logger.log_system("node index file exists, loaded")
return true return true
## Sets a specific [member DeckNode.node_type] to be a "favorite" for use in ## Sets a specific [member DeckNode.node_type] to be a "favorite" for use in

View 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

View file

@ -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 extends DeckNode
func _init() -> void: func _init() -> void:
name = "Button" name = "Button"
node_type = "button" node_type = "button"
description = "a button" description = "A button to trigger certain nodes that have a trigger input."
category = "general" category = "general"
add_output_port( add_output_port(

View file

@ -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 extends DeckNode
var thread : Thread var thread : Thread
@ -5,17 +8,17 @@ var thread : Thread
func _init(): func _init():
name = "Delay" name = "Delay"
node_type = name.to_snake_case() 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" 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, "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 = Thread.new()
thread.start(handle_delay.bind(data)) thread.start(handle_delay.bind(data))
@ -30,7 +33,7 @@ func handle_delay(data):
pass pass
print("Delay over") #print("Delay over")
send(0, data) send(0, data)

View 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)

View file

@ -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 extends DeckNode
var expr = Expression.new() var expr = Expression.new()
@ -5,30 +8,43 @@ var expr = Expression.new()
func _init(): func _init():
name = "Expression" name = "Expression"
node_type = name.to_snake_case() 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" category = "general"
props_to_serialize = [] 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 text = get_output_ports()[0].value_callback.call()
var err = expr.parse(text) var err = expr.parse(text, ["deck_var", "input"])
if err != OK: if err != OK:
DeckHolder.logger.log_node("Expression parse failed: %s" % err, Logger.LogType.ERROR)
printerr(err) printerr(err)
return null return null
var res = expr.execute() var res = expr.execute([_belonging_to.variable_stack, request_value(0)])
if expr.has_execute_failed(): if expr.has_execute_failed():
printerr("Expression Execution Failed: ", text) DeckHolder.logger.log_node("Expression Execution Failed: %s" % text, Logger.LogType.ERROR)
return null return null

View file

@ -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 extends DeckNode
func _init() -> void: func _init() -> void:
name = "Get Deck Var" name = "Get Deck Var"
node_type = name.to_snake_case() node_type = name.to_snake_case()
description = "retrieve a deck variable" description = "Retrieve a deck variable."
category = "general" category = "general"
add_output_port( add_output_port(
DeckType.Types.STRING, DeckType.Types.ANY,
"Variable", "Variable",
"field" "field"
) )
func _value_request(from_port: int) -> Variant: func _value_request(_from_port: int) -> Variant:
var key = ports[0].value_callback.call() var key = ports[0].value_callback.call()
return _belonging_to.variable_stack[key] return _belonging_to.variable_stack.get(key)

View file

@ -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 extends DeckNode
var output_count: int: var output_count: int:
get: get:
return get_all_ports().size() - 1 return get_all_ports().size() - 1
var group_node: DeckNode
func _init() -> void: func _init() -> void:
name = "Group input" name = "Group input"
node_type = "group_input" node_type = "group_input"
props_to_serialize = [&"output_count"] props_to_serialize = [&"output_count"]
appears_in_search = false appears_in_search = false
user_can_delete = false
add_output_port( add_output_port(
DeckType.Types.ANY, 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: func _on_outgoing_connection_removed(port_idx: int) -> void:
var last_connected_port := 0 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): 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 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: if port_idx < last_connected_port:
return return
@ -68,5 +77,4 @@ func _post_load() -> void:
func _value_request(from_port: int) -> Variant: 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) return group_node.request_value(group_node.get_input_ports()[from_port].index_of_type)

View file

@ -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 extends DeckNode
var group_id: String var group_id: String
var group_instance_id: String
var input_node: DeckNode var input_node: DeckNode
var output_node: DeckNode var output_node: DeckNode
var input_node_id: String
var output_node_id: String
var extra_ports: Array var extra_ports: Array
func _init() -> void: func _init() -> void:
name = "Group" name = "Group"
node_type = "group_node" 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 appears_in_search = false
func _pre_connection() -> void: func _pre_connection() -> void:
for port_type: PortType in extra_ports: for port_type: PortType in extra_ports:
var index_of_type: int
match port_type: match port_type:
PortType.OUTPUT: PortType.OUTPUT:
add_output_port(DeckType.Types.ANY, "Output %s" % get_output_ports().size()) add_output_port(DeckType.Types.ANY, "Output %s" % get_output_ports().size())
@ -25,10 +31,16 @@ func _pre_connection() -> void:
func init_io() -> 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: if !group:
return 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) input_node = group.get_node(group.group_input_node)
output_node = group.get_node(group.group_output_node) output_node = group.get_node(group.group_output_node)

View file

@ -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 extends DeckNode
var input_count: int: var input_count: int:
get: get:
return get_all_ports().size() - 1 return get_all_ports().size() - 1
var group_node: DeckNode
func _init() -> void: func _init() -> void:
name = "Group output" name = "Group output"
node_type = "group_output" node_type = "group_output"
props_to_serialize = [&"input_count"] props_to_serialize = [&"input_count"]
appears_in_search = false appears_in_search = false
user_can_delete = false
add_input_port( add_input_port(
DeckType.Types.ANY, DeckType.Types.ANY,
@ -36,7 +42,7 @@ func _on_incoming_connection_removed(port_idx: int) -> void:
if !(incoming_connections[port] as Dictionary).is_empty(): if !(incoming_connections[port] as Dictionary).is_empty():
last_connected_port = port 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: if port_idx < last_connected_port:
return return
@ -69,5 +75,4 @@ func _post_load() -> 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:
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) group_node.send(group_node.get_output_ports()[to_input_port].index_of_type, data, extra_data)

View 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)

View 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

View 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

View 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

View 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()

View 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}
)

View 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}

View 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)

View file

@ -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 extends DeckNode
var times_activated := 0 var times_activated := 0
@ -6,14 +9,14 @@ var times_activated := 0
func _init() -> void: func _init() -> void:
name = "Print" name = "Print"
node_type = name.to_snake_case() node_type = name.to_snake_case()
description = "print a value" description = "Print a value to the console."
props_to_serialize = [&"times_activated"] props_to_serialize = [&"times_activated"]
category = "general" category = "general"
add_input_port( add_input_port(
DeckType.Types.STRING, DeckType.Types.ANY,
"Text to print", "Data to print",
"field" "field"
) )
@ -34,17 +37,18 @@ func _receive(to_input_port: int, data: Variant, extra_data: Array = []) -> void
if to_input_port != 1: if to_input_port != 1:
return return
var data_to_print var data_to_print = str(resolve_input_port_value(0))
if request_value(0) != null: if data_to_print == null:
data_to_print = request_value(0) data_to_print = str(data)
elif get_input_ports()[0].value_callback.get_object() && get_input_ports()[0].value_callback.call() != "": if (data_to_print as String).is_empty():
data_to_print = get_input_ports()[0].value_callback.call() data_to_print = "<nothing>"
else:
data_to_print = data
times_activated += 1 times_activated += 1
# var data_to_print = input_ports[0].value_callback.call() # var data_to_print = input_ports[0].value_callback.call()
print(data_to_print) #print(data_to_print)
print("extra data: ", extra_data) #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) send(0, true)

View 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

View file

@ -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 extends DeckNode
func _init() -> void: func _init() -> void:
name = "Set Deck Var" name = "Set Deck Var"
node_type = name.to_snake_case() node_type = name.to_snake_case()
description = "set deck variable" description = "Set a deck variable on trigger."
category = "general" category = "general"
add_input_port( 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: if to_input_port != 2:
return return
var var_name: String = get_value_for_port(0, data) var var_name: String = resolve_input_port_value(0)
var var_value: Variant = get_value_for_port(1, data) var var_value: Variant = resolve_input_port_value(1)
_belonging_to.variable_stack[var_name] = var_value _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 # this can probably go into DeckNode with a different name that makes it clear
# that it prioritizes call-time resolution # 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: if request_value(port) != null:
return request_value(port) return request_value(port)
elif ports[port].value_callback.call() != "": elif get_input_ports()[port].value_callback.get_object() && get_input_ports()[port].value_callback.call() != null:
return ports[port].value_callback.call() return get_input_ports()[port].value_callback.call()
else: else:
return data return null

View file

@ -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 extends DeckNode
@ -5,6 +8,7 @@ func _init() -> void:
node_type = "string_constant" node_type = "string_constant"
name = "String Constant" name = "String Constant"
category = "general" category = "general"
description = "A String field."
add_output_port( add_output_port(
DeckType.Types.STRING, DeckType.Types.STRING,
@ -12,7 +16,7 @@ func _init() -> void:
"field" "field"
) )
func _value_request(from_port: int) -> Variant: func _value_request(_from_port: int) -> Variant:
if ports[0].value_callback.get_object(): if ports[0].value_callback.get_object():
return ports[0].value_callback.call() return ports[0].value_callback.call()
else: else:

View file

@ -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 extends DeckNode

View file

@ -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 extends DeckNode
@ -26,7 +29,7 @@ func _init() -> void:
add_input_port(DeckType.Types.BOOL, "Send", "button") 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: if to_input_port == 6:
send(0, false) send(0, false)
send(1, 1.0) send(1, 1.0)

View 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

View 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)

View 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

View 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}

View 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

View 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

View 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}

View 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

View 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

View file

@ -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 class_name Port
## A data type representing a port of a [DeckNode]. ## A data type representing a port of a [DeckNode].
## ##
@ -25,7 +28,9 @@ var index_of_type: int
var index: int var index: int
## The value of this port. ## The value of this port.
var value: Variant: set = set_value var value: Variant
signal value_updated(new_value: Variant)
func _init( func _init(
@ -48,7 +53,13 @@ func _init(
func set_value(v: Variant) -> void: 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: if v is Callable:
value = v.call() value = v.call()
return return
value = v value = v

View 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))

View file

@ -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 class_name SearchProvider
## A class facilitating the searching of nodes. ## 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: func(element: NodeDB.NodeDescriptor, _search_string: String, pre_strip_string: String) -> bool:
const p := r"#c\s[\w]+" const p := r"#c\s[\w]+"
var r := RegEx.create_from_string(p) 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] var c := r.search(pre_strip_string).get_string().split("#c ", false)[0]
return c.is_subsequence_ofn(element.category), return c.is_subsequence_ofn(element.category),
@ -39,7 +42,7 @@ static var filters: Array[Filter] = [
const p := r"#c\s[\w]+" const p := r"#c\s[\w]+"
var r := RegEx.create_from_string(p) var r := RegEx.create_from_string(p)
var c := r.search(search_string).get_string() 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, "") return search_string.replace(c, "")
), ),
] ]

View file

@ -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 class_name DeckType
enum Types{ enum Types{

38
dist/logo-flattened.svg vendored Normal file
View 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
View 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
View 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
View 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
View 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}'"

View 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

View 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

View file

@ -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 extends MarginContainer
class_name AddNodeMenu 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. ## 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: func _on_search_line_edit_gui_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_down"): if event.is_action_pressed("ui_down"):
search_line_edit.accept_event()
var category: Category var category: Category
for i: String in categories: for i: String in categories:
var c: Category = categories[i] 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)) scroll_container.ensure_control_visible(category.get_child(item + 1))
if event.is_action_pressed("ui_up"): if event.is_action_pressed("ui_up"):
search_line_edit.accept_event()
var category: Category var category: Category
for i: String in categories: for i: String in categories:
var c: Category = categories[i] var c: Category = categories[i]
@ -178,7 +183,6 @@ func _on_category_collapse_toggled(collapsed: bool, category: String) -> void:
class Category extends VBoxContainer: class Category extends VBoxContainer:
const COLLAPSE_ICON := preload("res://graph_node_renderer/textures/collapse-icon.svg") 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") const COLLAPSE_ICON_COLLAPSED := preload("res://graph_node_renderer/textures/collapse-icon-collapsed.svg")
var collapse_button: Button var collapse_button: Button
## Emitted when a child item has been pressed. ## Emitted when a child item has been pressed.

View 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)

View 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")

View file

@ -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 extends Control
class_name DeckHolderRenderer class_name DeckHolderRenderer
@ -5,14 +8,23 @@ class_name DeckHolderRenderer
## ##
## Entry point for the [GraphEdit] based Renderer ## 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 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] ## 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. ## Reference to the [FileDialog] used for File operations through the program.
@onready var file_dialog: FileDialog = $FileDialog @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 for storing the Options in the "File" PopupMenu.
enum FileMenuId { enum FileMenuId {
NEW, NEW,
@ -20,22 +32,94 @@ enum FileMenuId {
SAVE = 3, SAVE = 3,
SAVE_AS, SAVE_AS,
CLOSE = 6, 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. ## Weak Reference to the Deck that is currently going to be saved.
var _deck_to_save: WeakRef 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: func _ready() -> void:
get_tree().auto_accept_quit = false
tab_container.add_button_pressed.connect(add_empty_deck) 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( tab_container.tab_close_requested.connect(
func(tab: int): func(tab: int):
DeckHolder.close_deck(tab_container.get_tab_metadata(tab)) if tab_container.get_tab_metadata(tab, "dirty") && !tab_container.get_tab_metadata(tab, "group"):
tab_container.close_tab(tab) 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) 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] ## Called when the File button in the [MenuBar] is pressed with the [param id]
## of the button within it that was pressed. ## of the button within it that was pressed.
@ -44,27 +128,46 @@ func _on_file_id_pressed(id: int) -> void:
FileMenuId.NEW: FileMenuId.NEW:
add_empty_deck() add_empty_deck()
FileMenuId.OPEN: FileMenuId.OPEN:
open_open_dialog("res://") open_open_dialog(recent_path)
FileMenuId.SAVE: FileMenuId.SAVE when tab_container.get_tab_count() > 0:
save_active_deck() save_active_deck()
FileMenuId.SAVE_AS: FileMenuId.SAVE_AS when tab_container.get_tab_count() > 0:
open_save_dialog("res://") open_save_dialog(tab_container.get_tab_metadata(tab_container.get_current_tab(), "path"))
FileMenuId.CLOSE: FileMenuId.CLOSE:
close_current_tab() 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. ## Adds an empty [DeckRendererGraphEdit] with a corresponding [Deck] for it's data.
func add_empty_deck() -> void: func add_empty_deck() -> void:
var deck := DeckHolder.add_empty_deck() var deck := DeckHolder.add_empty_deck()
var inst: DeckRendererGraphEdit = DECK_SCENE.instantiate() var inst: DeckRendererGraphEdit = DECK_SCENE.instantiate()
inst.deck = deck inst.deck = deck
tab_container.add_content(inst, "Deck %s" % (tab_container.get_tab_count() + 1)) var tab := tab_container.add_content(inst, "<unsaved deck>")
tab_container.set_tab_metadata(tab_container.get_current_tab(), deck) tab_container.set_tab_metadata(tab, "id", deck.id)
inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested.bind(deck)) 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] ## Closes the current tab in [member tab_container]
func close_current_tab() -> void: func close_current_tab() -> void:
tab_container.close_tab(tab_container.get_current_tab()) 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] ## Opens [member file_dialog] with the mode [member FileDialog.FILE_MODE_SAVE_FILE]
## as well as getting a weakref to the active [Deck] ## as well as getting a weakref to the active [Deck]
func open_save_dialog(path: String) -> void: 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: func open_open_dialog(path: String) -> void:
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILES file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILES
file_dialog.title = "Open Deck(s)" file_dialog.title = "Open Deck(s)"
file_dialog.current_path = path file_dialog.current_path = path + "/"
file_dialog.popup_centered() file_dialog.popup_centered()
file_dialog.files_selected.connect(_on_file_dialog_open_files, CONNECT_ONE_SHOT) 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 return
deck.save_path = path 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 json := JSON.stringify(deck.to_dict(), "\t")
var f := FileAccess.open(path, FileAccess.WRITE) var f := FileAccess.open(path, FileAccess.WRITE)
f.store_string(json) 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 ## Connected to [signal FileDialog.open_files] on [member file_dialog]. Opens
## the selected paths, instantiating [DeckRenderGraphEdit]s and [Deck]s for each. ## the selected paths, instantiating [DeckRenderGraphEdit]s and [Deck]s for each.
func _on_file_dialog_open_files(paths: PackedStringArray) -> void: func _on_file_dialog_open_files(paths: PackedStringArray) -> void:
for path in paths: for path in paths:
var deck := DeckHolder.open_deck_from_file(path) open_deck_at_path(path)
var inst: DeckRendererGraphEdit = DECK_SCENE.instantiate()
inst.deck = deck
tab_container.add_content(inst, "Deck %s" % (tab_container.get_tab_count() + 1)) func open_deck_at_path(path: String) -> void:
inst.initialize_from_deck() for tab in tab_container.get_tab_count():
inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested.bind(deck)) 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
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)
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] ## Gets the currently active [Deck] from [member tab_container]
func get_active_deck() -> Deck: func get_active_deck() -> Deck:
@ -116,12 +245,24 @@ func get_active_deck() -> Deck:
## Saves the active [Deck] in [member tab_container] ## Saves the active [Deck] in [member tab_container]
func save_active_deck() -> void: 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://") open_save_dialog("res://")
else: else:
var json := JSON.stringify(get_active_deck().to_dict(), "\t") var json := JSON.stringify(deck.to_dict(), "\t")
var f := FileAccess.open(get_active_deck().save_path, FileAccess.WRITE) var f := FileAccess.open(deck.save_path, FileAccess.WRITE)
f.store_string(json) f.store_string(json)
add_recent_file(deck.save_path)
renderer.dirty = false
## Disconnects the [FileDialog] signals if they are already connected. ## Disconnects the [FileDialog] signals if they are already connected.
func disconnect_file_dialog_signals() -> void: 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 ## 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 ## groups based off the given [param group_id] and [param deck]. As well as adding
## a corresponding tab to [member tab_container] ## a corresponding tab to [member tab_container]
func _on_deck_renderer_group_enter_requested(group_id: String, deck: Deck) -> void: func _on_deck_renderer_group_enter_requested(group_id: String) -> void:
var group_deck := deck.get_group(group_id) #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() var deck_renderer: DeckRendererGraphEdit = DECK_SCENE.instantiate()
deck_renderer.deck = group_deck deck_renderer.deck = group_deck
deck_renderer.initialize_from_deck() deck_renderer.initialize_from_deck()
tab_container.add_content(deck_renderer, "Group %s" % (tab_container.get_tab_count() + 1)) var tab := tab_container.add_content(deck_renderer, "(g) %s" % group_id.left(8))
tab_container.set_tab_metadata(tab_container.get_current_tab(), group_deck) tab_container.set_tab_metadata(tab, "id", group_id)
deck_renderer.group_enter_requested.connect(_on_deck_renderer_group_enter_requested.bind(deck_renderer.deck)) 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")

View file

@ -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="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="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="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"] [node name="DeckHolderRenderer" type="Control"]
layout_mode = 3 layout_mode = 3
@ -13,6 +67,11 @@ grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
theme = ExtResource("1_tgul2") theme = ExtResource("1_tgul2")
script = ExtResource("1_67g2g") 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="."] [node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 1 layout_mode = 1
@ -28,7 +87,7 @@ theme_override_constants/margin_bottom = 2
[node name="VSplitContainer" type="VSplitContainer" parent="MarginContainer"] [node name="VSplitContainer" type="VSplitContainer" parent="MarginContainer"]
layout_mode = 2 layout_mode = 2
split_offset = 677 split_offset = 460
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VSplitContainer"] [node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VSplitContainer"]
layout_mode = 2 layout_mode = 2
@ -37,7 +96,8 @@ layout_mode = 2
layout_mode = 2 layout_mode = 2
[node name="File" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"] [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/text = "New Deck"
item_0/id = 0 item_0/id = 0
item_1/text = "Open Deck" item_1/text = "Open Deck"
@ -54,20 +114,81 @@ item_5/id = 5
item_5/separator = true item_5/separator = true
item_6/text = "Close Deck" item_6/text = "Close Deck"
item_6/id = 6 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"] [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")] [node name="TabContainerCustom" parent="MarginContainer/VSplitContainer/VBoxContainer" instance=ExtResource("1_s3ug2")]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 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 layout_mode = 2
[node name="FileDialog" type="FileDialog" parent="."] [node name="FileDialog" type="FileDialog" parent="."]
size = Vector2i(776, 447) size = Vector2i(776, 447)
mode_overrides_title = false mode_overrides_title = false
access = 2 access = 2
filters = PackedStringArray("*.deck;StreamGraph Decks")
use_native_dialog = true 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/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"]

View file

@ -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 extends GraphNode
class_name DeckNodeRendererGraphNode class_name DeckNodeRendererGraphNode
@ -17,18 +20,28 @@ func _ready() -> void:
node.ports_updated.connect(_on_node_ports_updated) node.ports_updated.connect(_on_node_ports_updated)
for port in node.get_all_ports(): for port in node.get_all_ports():
update_port(port) 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 ## Connected to [signal GraphElement.position_offset_updated] and updates the
## [member node]s properties ## [member node]s properties
func _on_position_offset_changed() -> void: func _on_position_offset_changed() -> void:
node.position.x = position_offset.x node.position.x = position_offset.x
node.position.y = position_offset.y 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 ## Connected to [member node]s [signal position_updated] to keep parity with the
## data position. ## data position.
func _on_node_position_updated(new_position: Dictionary) -> void: 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.x = new_position.x
position_offset.y = new_position.y 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 ## 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 ## [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) update_port(port)
func _on_node_renamed(new_name: String) -> void:
title = new_name
func update_port(port: Port) -> void: func update_port(port: Port) -> void:
var descriptor_split := port.descriptor.split(":") var descriptor_split := port.descriptor.split(":")
match descriptor_split[0]: match descriptor_split[0]:
@ -92,8 +109,14 @@ func update_port(port: Port) -> void:
line_edit.text_changed.connect(port.set_value) line_edit.text_changed.connect(port.set_value)
"singlechoice": "singlechoice":
var box := OptionButton.new() var box := OptionButton.new()
for item in descriptor_split.slice(1): if descriptor_split.slice(1).is_empty():
box.add_item(item) 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) add_child(box)
port.value_callback = func(): return box.get_item_text(box.get_selected_id()) port.value_callback = func(): return box.get_item_text(box.get_selected_id())
if port.type == DeckType.Types.STRING: if port.type == DeckType.Types.STRING:
@ -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.text_changed.connect(port.set_value.bind(code_edit.get_text))
code_edit.custom_minimum_size = Vector2(200, 100) code_edit.custom_minimum_size = Vector2(200, 100)
code_edit.size_flags_vertical = SIZE_EXPAND_FILL 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() var label := Label.new()
add_child(label) add_child(label)
label.text = port.label label.text = port.label
if port.port_type == DeckNode.PortType.OUTPUT:
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
set_slot( set_slot(
port.index, port.index,

View file

@ -9,5 +9,3 @@ offset_bottom = 55.0
resizable = true resizable = true
title = "Deck Node" title = "Deck Node"
script = ExtResource("1_pos0w") script = ExtResource("1_pos0w")
[connection signal="position_offset_changed" from="." to="." method="_on_position_offset_changed"]

View file

@ -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 extends GraphEdit
class_name DeckRendererGraphEdit class_name DeckRendererGraphEdit
@ -17,6 +20,9 @@ var add_node_menu: AddNodeMenu
## nodes in [method _on_add_node_menu_node_selected] ## nodes in [method _on_add_node_menu_node_selected]
var popup_position: Vector2 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] ## References the [Deck] that holds all the functional properties of this [DeckRendererGraphEdit]
var deck: Deck: var deck: Deck:
set(v): set(v):
@ -27,6 +33,17 @@ var deck: Deck:
## Emits when Group creation is requested. Ex. Hitting the "group_nodes" Hotkey. ## Emits when Group creation is requested. Ex. Hitting the "group_nodes" Hotkey.
signal group_enter_requested(group_id: String) 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] ## 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 ## stored in [member add_node_menu]. And sets its size of [member search_popup_panel] to
## [member add_node_popup_size] ## [member add_node_popup_size]
@ -37,6 +54,11 @@ func _ready() -> void:
search_popup_panel.size = search_popup_size search_popup_panel.size = search_popup_size
add_child(search_popup_panel, false, Node.INTERNAL_MODE_BACK) 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 t: DeckType.Types in DeckType.CONVERSION_MAP:
for out_type: DeckType.Types in DeckType.CONVERSION_MAP[t]: for out_type: DeckType.Types in DeckType.CONVERSION_MAP[t]:
add_valid_connection_type(t, out_type) 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 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) #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( connect_node(
from_node_renderer.name, from_node_renderer.name,
from_port, from_port,
to_node_renderer.name, to_node_renderer.name,
to_port to_port
) )
dirty = true
## Receives [signal GraphEdit.disconnection_request] and attempts to disconnect the two [DeckNode]s ## Receives [signal GraphEdit.disconnection_request] and attempts to disconnect the two [DeckNode]s
## involved, utilizes [NodeDB] for accessing them. ## 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 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) #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( disconnect_node(
from_node_renderer.name, 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_node_renderer.name,
to_port to_port
) )
dirty = true
## Returns the associated [DeckNodeRendererGraphNode] for the supplied [DeckNode]. ## Returns the associated [DeckNodeRendererGraphNode] for the supplied [DeckNode].
## Or [code]null[/code] if none is found. ## 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] ## Setups all the data from the set [member deck] in this [DeckRendererGraphEdit]
func initialize_from_deck() -> void: func initialize_from_deck() -> void:
change_dirty = false
for i in get_children(): for i in get_children():
i.queue_free() i.queue_free()
scroll_offset = deck.get_meta("offset", Vector2()) scroll_offset = deck.get_meta("offset", Vector2())
is_group = deck.is_group
for node_id in deck.nodes: for node_id in deck.nodes:
var node_renderer: DeckNodeRendererGraphNode = NODE_SCENE.instantiate() var node_renderer: DeckNodeRendererGraphNode = NODE_SCENE.instantiate()
node_renderer.node = deck.nodes[node_id] node_renderer.node = deck.nodes[node_id]
add_child(node_renderer) add_child(node_renderer)
node_renderer.position_offset = node_renderer.node.position_as_vector2() node_renderer.position_offset = node_renderer.node.position_as_vector2()
change_dirty = true
for node_id in deck.nodes: dirty = false
var node: DeckNode = deck.nodes[node_id]
var from_node = get_children().filter(
func(c: DeckNodeRendererGraphNode):
return c.node._id == node_id
)[0]
refresh_connections() refresh_connections()
@ -127,26 +148,22 @@ func refresh_connections() -> void:
)[0] )[0]
for from_port in node.outgoing_connections: for from_port in node.outgoing_connections:
for connection in node.outgoing_connections[from_port]: for to_node_id: String in node.outgoing_connections[from_port]:
var to_node_id = connection.keys()[0] var to_node_ports = node.outgoing_connections[from_port][to_node_id]
var to_node_port = connection.values()[0] var renderer: Array = get_children().filter(
var to_node: DeckNodeRendererGraphNode = get_children().filter(
func(c: DeckNodeRendererGraphNode): func(c: DeckNodeRendererGraphNode):
return c.node._id == to_node_id 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("***")
connect_node(
from_node.name,
from_port,
to_node.name,
to_node_port
) )
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,
to_node.name,
to_node_port
)
## Connected to [signal Deck.node_added], used to instance the required ## Connected to [signal Deck.node_added], used to instance the required
## [DeckNodeRendererGraphNode] and set it's [member DeckNodeRenderGraphNode.position_offset] ## [DeckNodeRendererGraphNode] and set it's [member DeckNodeRenderGraphNode.position_offset]
@ -155,6 +172,8 @@ func _on_deck_node_added(node: DeckNode) -> void:
inst.node = node inst.node = node
add_child(inst) add_child(inst)
inst.position_offset = inst.node.position_as_vector2() inst.position_offset = inst.node.position_as_vector2()
dirty = true
## Connected to [signal Deck.node_added], used to remove the specified ## Connected to [signal Deck.node_added], used to remove the specified
## [DeckNodeRendererGraphNode] and queue_free it. ## [DeckNodeRendererGraphNode] and queue_free it.
@ -165,6 +184,7 @@ func _on_deck_node_removed(node: DeckNode) -> void:
renderer.queue_free() renderer.queue_free()
break break
dirty = true
## Utility function that gets all [DeckNodeRenderGraphNode]s that are selected ## Utility function that gets all [DeckNodeRenderGraphNode]s that are selected
## See [member GraphNode.selected] ## See [member GraphNode.selected]
@ -178,7 +198,6 @@ func get_selected_nodes() -> Array:
## based off the action "group_nodes". ## based off the action "group_nodes".
func _gui_input(event: InputEvent) -> void: func _gui_input(event: InputEvent) -> void:
if event.is_action_pressed("group_nodes") && get_selected_nodes().size() > 0: if event.is_action_pressed("group_nodes") && get_selected_nodes().size() > 0:
print("?")
clear_connections() clear_connections()
var nodes = get_selected_nodes().map( var nodes = get_selected_nodes().map(
func(x: DeckNodeRendererGraphNode): func(x: DeckNodeRendererGraphNode):
@ -188,6 +207,14 @@ func _gui_input(event: InputEvent) -> void:
refresh_connections() refresh_connections()
get_viewport().set_input_as_handled() 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 ## Handles entering groups with action "enter_group". Done here to bypass neighbor
## functionality. ## functionality.
func _input(event: InputEvent) -> void: 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 event.is_action_pressed("enter_group") && get_selected_nodes().size() == 1:
if !((get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.node_type == "group_node"): if !((get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.node_type == "group_node"):
return return
print("tried to enter group")
group_enter_requested.emit((get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.group_id) group_enter_requested.emit((get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.group_id)
get_viewport().set_input_as_handled() 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] ## Opens [member search_popup_panel] at the mouse position. Connected to [signal GraphEdit.popup_request]
func _on_popup_request(p_popup_position: Vector2) -> void: func _on_popup_request(p_popup_position: Vector2) -> void:
var p := get_viewport_rect().position + get_global_mouse_position() 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 get_node_renderer(node).position_offset = node_pos
search_popup_panel.hide() 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()

View file

@ -12,5 +12,9 @@ right_disconnects = true
show_arrange_button = false show_arrange_button = false
script = ExtResource("1_pojfs") 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="popup_request" from="." to="." method="_on_popup_request"]
[connection signal="scroll_offset_changed" from="." to="." method="_on_scroll_offset_changed"] [connection signal="scroll_offset_changed" from="." to="." method="_on_scroll_offset_changed"]

View 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()

View 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"

View 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
)

View 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"

View file

@ -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 extends VBoxContainer
class_name TabContainerCustom class_name TabContainerCustom
## Custom Recreation of [TabContainer] for Flexibility ## 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 @onready var tab_bar: TabBar = %TabBar
## Reference to the [Button] at the end of the [TabBar] that's ## 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 @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 @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 signal add_button_pressed
## Emitted when the current tab in [member tab_bar] is changed. ## Emitted when the current tab in [member tab_bar] is changed.
signal tab_changed(tab: int) signal tab_changed(tab: int)
## Emitted when a tab in [member tab_bar] has been closed. ## Emitted when a tab in [member tab_bar] has been closed.
## See [signal TabBar.tab_close_requested] ## See [signal TabBar.tab_close_requested]
signal tab_closed(tab: int) signal tab_closed(tab: int)
## Emitted when a request to close a tab in the [member tab_bar] has been ## Emitted when a request to close a tab in the [member tab_bar] has been requested.
## requested using [signal TabBar.tab_close_pressed]
signal tab_close_requested(tab: int) signal tab_close_requested(tab: int)
## Emitted when the order of the tabs in [member tab_bar] has been changed. ## Emitted when the order of the tabs in [member tab_bar] has been changed.
signal tab_rearranged(old: int, new: int) 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 _previous_active_tab: int = -1
var _tab_metadata: Array[Dictionary] #Array[Dictionary[String -> key, Variant]]
func _ready() -> void: func _ready() -> void:
tab_bar.tab_selected.connect( tab_bar.tab_selected.connect(
@ -53,49 +58,84 @@ func _ready() -> void:
tab_bar.active_tab_rearranged.connect( tab_bar.active_tab_rearranged.connect(
func(idx_to: int): 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) tab_rearranged.emit(_previous_active_tab, idx_to)
content_container.move_child(content_container.get_child(_previous_active_tab), idx_to) content_container.move_child(content_container.get_child(_previous_active_tab), idx_to)
_previous_active_tab = idx_to _previous_active_tab = idx_to
) )
## Adds the given [Node] as "content" for the given tabs name as a [String] ## Adds the given [Node] as the displayed content for a tab.
func add_content(c: Node, tab_title: String) -> void: func add_content(c: Node, tab_title: String) -> int:
tab_bar.add_tab(tab_title) tab_bar.add_tab(tab_title)
content_container.add_child(c) 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: func get_tab_count() -> int:
return tab_bar.tab_count 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: func is_empty() -> bool:
return get_tab_count() == 0 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: func close_tab(tab: int) -> void:
content_container.get_child(tab).queue_free() content_container.get_child(tab).queue_free()
if !tab_bar.select_previous_available(): if !tab_bar.select_previous_available():
tab_bar.select_next_available() tab_bar.select_next_available()
tab_bar.remove_tab(tab) tab_bar.remove_tab(tab)
_tab_metadata.remove_at(tab)
tab_closed.emit(tab) tab_closed.emit(tab)
if tab_bar.tab_count == 0: if tab_bar.tab_count == 0:
_previous_active_tab = -1 _previous_active_tab = -1
## Returns the currently selected tab in [member tab_bar]
## Returns the currently selected tab.
func get_current_tab() -> int: func get_current_tab() -> int:
return tab_bar.current_tab 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: func get_content(idx: int) -> Control:
return content_container.get_child(idx) 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(). ## Sets the metadata value for the tab at index [param tab_idx] at [param key], which can be
## If no metadata was previously set, returns null by default. ## retrieved later using [method get_tab_metadata].
func get_tab_metadata(tab: int) -> Variant: func set_tab_metadata(tab: int, key: String, value: Variant) -> void:
return tab_bar.get_tab_metadata(tab) #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)

View 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)

View 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"

View 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

View file

@ -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")

View 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
View file

BIN
img/example1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
img/example2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View file

@ -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()
)

View file

@ -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

View file

@ -10,22 +10,23 @@ config_version=5
[application] [application]
config/name="Re-DotDeck" config/name="StreamGraph"
config/version="0.0.1"
config/tags=PackedStringArray("dot_deck") config/tags=PackedStringArray("dot_deck")
run/main_scene="res://graph_node_renderer/deck_holder_renderer.tscn" run/main_scene="res://graph_node_renderer/deck_holder_renderer.tscn"
config/use_custom_user_dir=true 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") config/features=PackedStringArray("4.2", "Forward Plus")
run/low_processor_mode=true config/icon="res://dist/logo-flattened.svg"
config/icon="res://icon.svg"
[autoload] [autoload]
NodeDB="*res://classes/deck/node_db.gd" 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] [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) "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] [rendering]

View file

@ -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 extends DeckNode

View file

@ -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)

View file

@ -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