mirror of
https://github.com/Sovvie/Chemical.git
synced 2025-06-20 03:29:17 +00:00
Compare commits
No commits in common. "v0.1.0b" and "main" have entirely different histories.
75 changed files with 10829 additions and 1456 deletions
22
.gitignore
vendored
22
.gitignore
vendored
|
@ -1,14 +1,8 @@
|
|||
# A fast, small, safe, gradually typed embeddable scripting language derived from Lua
|
||||
#
|
||||
# https://github.com/luau-lang/luau
|
||||
# https://luau.org/
|
||||
|
||||
# Code coverage
|
||||
coverage.out
|
||||
|
||||
# Profiling
|
||||
profile.out
|
||||
profile.svg
|
||||
|
||||
# Time trace
|
||||
trace.json
|
||||
# Project place file
|
||||
/*.rbxl
|
||||
sourcemap.json*
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
/.vscode
|
||||
/.png
|
BIN
Chemical.rbxm
BIN
Chemical.rbxm
Binary file not shown.
674
LICENSE
674
LICENSE
|
@ -1,674 +0,0 @@
|
|||
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>.
|
771
README.md
771
README.md
|
@ -1,769 +1,4 @@
|
|||
# Chemical - Reactive State & UI Framework for Roblox Luau
|
||||

|
||||
|
||||
**Version:** 0.1.0b ALPHA (as of 6/3/2025, per comments)
|
||||
**Author:** Sovereignty (Discord: sov_dev)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Core Philosophy](#2-core-philosophy)
|
||||
3. [Installation & Setup](#3-installation--setup)
|
||||
4. [Core Reactive Primitives](#4-core-reactive-primitives)
|
||||
* [Value`<T>`](#valuet)
|
||||
* [Computed`<T>`](#computedt)
|
||||
* [Observer](#observer)
|
||||
* [Element](#element)
|
||||
* [Watch](#watch)
|
||||
5. [UI Creation & Management](#5-ui-creation--management)
|
||||
* [Create: `Chemical.Create()`](#chemicalcreate)
|
||||
* [Give: `Chemical.Give()`](#chemicalgive)
|
||||
* [UI Traits: `Ref`, `onEvent`, `onChange`](#ui-traits)
|
||||
6. [State Replication (`Chemical.Reaction`)](#6-state-replication)
|
||||
* [Server-Side API](#reaction-server-side-api)
|
||||
* [Client-Side API](#reaction-client-side-api)
|
||||
* [Example Usage](#reaction-example-usage)
|
||||
7. [Client-Side Routing (`Chemical.Router`)](#7-client-side-routing)
|
||||
8. [Utility Functions](#8-utility-functions)
|
||||
* [Await: `Chemical.Await()`](#chemical-await)
|
||||
* [Destroy: `Chemical.Destroy()`](#chemical-destroy)
|
||||
* [Nothing: `Chemical.Nothing()`](#chemical-nothing)
|
||||
9. [Under The Hood (Advanced)](#9-under-the-hood-advanced)
|
||||
* [Networking (`Suphi Packet`)](#networking)
|
||||
10. [Type System](#10-type-system)
|
||||
11. [Examples](#11-examples)
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Chemical is a comprehensive Luau framework for Roblox game development, emphasizing a reactive programming paradigm. It aims to simplify state management, UI development, and server-client data synchronization by providing a suite of interconnected tools. With Chemical, developers can build dynamic and responsive user interfaces and game systems that automatically react to changes in their underlying data.
|
||||
|
||||
Key features include observable values, derived (computed) values, an ECS-backed architecture, declarative UI construction, client-side routing, and a powerful state replication system.
|
||||
|
||||
## 2. Core Philosophy
|
||||
|
||||
* **Reactivity:** State changes should automatically propagate through the application, updating dependent values and UI elements without manual intervention.
|
||||
* **Declarative UI:** Describe *what* your UI should look like based on the current state, and let Chemical handle the updates.
|
||||
* **Centralized State (Optional):** While not strictly enforcing a single global store, `Chemical.Reaction` facilitates managing and syncing shared game state.
|
||||
* **Performance:** Leveraging an ECS backend and a custom packet system for efficient data handling and networking.
|
||||
* **Developer Experience:** Providing clear, typed APIs (using Luau type annotations) and utilities to streamline common tasks.
|
||||
|
||||
## 3. Installation & Setup
|
||||
|
||||
1. Place the `Chemical` root `ModuleScript` (and its descendant file structure from the `.rbxm`) into a suitable location, typically `ReplicatedStorage` to be accessible by both server and client.
|
||||
2. Ensure the `UseReactions` BoolValue in `Chemical/Configuration` is set to `true` if you intend to use the `Chemical.Reaction` state replication system. If `false`, attempting to use `Reaction` will result in a warning/error.
|
||||
|
||||
```lua
|
||||
-- Accessing the Chemical library
|
||||
local Chemical = require(game.ReplicatedStorage.Chemical)
|
||||
```
|
||||
|
||||
## 4. Core Reactive Primitives
|
||||
|
||||
These are the fundamental building blocks for creating reactive data flows.
|
||||
|
||||
### Value`<T>`
|
||||
|
||||
The `Value<T>` object is the most basic reactive unit. It encapsulates a single piece of data that can be read and written. When its data changes, any `Computed` values or `Observer`s depending on it are notified.
|
||||
|
||||
**API:**
|
||||
|
||||
* `Chemical.Value(initialValue: T): Value<T>`: Constructor.
|
||||
* `:get(): T`: Retrieves the current value. If called within a `Computed` function or `Watch` target getter, it registers this `Value` as a dependency.
|
||||
* `:set(newValue: T)`: Sets a new value. If the new value is different from the old, it triggers updates to dependents.
|
||||
* `:increment(amount: number?)`: For numeric `Value`s. Increments the value by `amount` (defaults to 1).
|
||||
* `:toggle()`: For boolean `Value`s. Flips the boolean state.
|
||||
* `:key(key: any, newValue: any)`: For table `Value`s. Sets `tbl[key] = newValue` and triggers updates.
|
||||
* `:insert(itemValue: any)`: For array-like table `Value`s. Equivalent to `table.insert(tbl, itemValue)`.
|
||||
* `:remove(itemValue: any)`: For array-like table `Value`s. Removes the first occurrence of `itemValue`.
|
||||
* `:destroy()`: Destroys the `Value` object, cleaning up its ECS entity and notifying dependent `Computed`s or `Observer`s (which may also destroy themselves).
|
||||
|
||||
**Example:**
|
||||
|
||||
```lua
|
||||
local Chemical = require(path.to.Chemical)
|
||||
|
||||
local playerScore = Chemical.Value(0)
|
||||
local isGameOver = Chemical.Value(false)
|
||||
|
||||
print(playerScore:get()) -- Output: 0
|
||||
|
||||
playerScore:increment(10)
|
||||
print(playerScore:get()) -- Output: 10
|
||||
|
||||
isGameOver:set(true)
|
||||
```
|
||||
|
||||
### Computed`<T>`
|
||||
|
||||
A `Computed<T>` object represents a value that is derived from one or more other reactive objects (`Value`s or other `Computed`s). It automatically re-evaluates its derivation function whenever any of its dependencies change, if the new value differs from the old it will appropriately cache the value and respond.
|
||||
|
||||
**API:**
|
||||
|
||||
* `Chemical.Computed(derivationFunction: () -> T, cleanupFunction?: (oldDerivedValue: T) -> ()): Computed<T>`: Constructor.
|
||||
* `derivationFunction`: A function that returns the computed value. Any `Value:get()` or `Computed:get()` calls inside this function establish dependencies.
|
||||
* `cleanupFunction` (optional): A function called with the *previous* computed value right before the `Computed` is re-cached due to a dependency change, or when the `Computed` object is destroyed. Useful for cleaning up side effects or resources tied to the old value.
|
||||
* `:get(): T`: Retrieves the current computed value. If called within another `Computed` or `Observer`, it registers this `Computed` as a dependency.
|
||||
* `:destroy()`: Destroys the `Computed` object, cleaning up its value if `cleanup` was provided as well as its ECS entity and notifying dependent `Computed`s or `Observer`s (which may also destroy themselves).
|
||||
|
||||
**Example:**
|
||||
|
||||
```lua
|
||||
local Chemical = require(path.to.Chemical)
|
||||
|
||||
local firstName = Chemical.Value("Jane")
|
||||
local lastName = Chemical.Value("Doe")
|
||||
local isGameOver = Chemical.Value(true)
|
||||
|
||||
local fullName = Chemical.Computed(function()
|
||||
return firstName:get() .. " " .. lastName:get()
|
||||
end)
|
||||
|
||||
print(fullName:get()) -- Output: Jane Doe
|
||||
|
||||
firstName:set("John")
|
||||
task.wait() --Computeds will run on the next frame after the change.
|
||||
print(fullName:get()) -- Output: John Doe (automatically updated)
|
||||
|
||||
local characterDescription = Chemical.Computed(function()
|
||||
local name = fullName:get() -- Dependency on another Computed
|
||||
local status = isGameOver:get()
|
||||
|
||||
return string.format("%s (Game Over: %s)", name, tostring(status))
|
||||
end, function(oldDescription) --Optional Cleanup method
|
||||
print("CharacterDescription cleanup. Old value:", oldDescription)
|
||||
end)
|
||||
|
||||
print(characterDescription:get()) -- Output: John Doe (Game Over: true)
|
||||
|
||||
isGameOver:set(false) -- Triggers re-computation of characterDescription
|
||||
task.wait()
|
||||
print(characterDescription:get()) -- Output: John Doe (Game Over: false)
|
||||
|
||||
isGameOver:set(false) -- Because isGameOver is already == false, it will not triggers re-computation of characterDescription
|
||||
-- Nor will it cause any observational changes/events to be triggered as the value of isGameOver did not change.
|
||||
-- This applies to computeds as well.
|
||||
```
|
||||
|
||||
### Observer
|
||||
|
||||
An `Observer` allows you to react to changes in a `Value` or `Computed` object by executing a callback function.
|
||||
|
||||
**API:**
|
||||
|
||||
* `Chemical.Observer(target: Value<any> | Computed<any>): Observer`: Constructor.
|
||||
* `:onChange(callback: (newValue: any?, oldValue: any?) -> ()): () -> ()`: Registers a callback function to be invoked when the observed `target`'s value changes.
|
||||
* Returns a `disconnectFunction` that, when called, unregisters this specific callback.
|
||||
* `:destroy()`: Destroys the `Observer` and disconnects all its listeners.
|
||||
|
||||
**Example:**
|
||||
|
||||
```lua
|
||||
local Chemical = require(path.to.Chemical)
|
||||
local health = Chemical.Value(100)
|
||||
|
||||
local healthObserver = Chemical.Observer(health)
|
||||
|
||||
local disconnectHealthListener = healthObserver:onChange(function(newHealth, oldHealth)
|
||||
print(string.format("Health changed from %s to %s", tostring(oldHealth), tostring(newHealth)))
|
||||
end)
|
||||
|
||||
health:set(80) -- Output: Health changed from 100 to 80
|
||||
health:set(80) -- No output (value didn't change)
|
||||
health:set(95) -- Output: Health changed from 80 to 95
|
||||
|
||||
disconnectHealthListener() -- Stop listening for this specific callback
|
||||
|
||||
health:set(100) -- No output from the disconnected listener
|
||||
|
||||
healthObserver:destroy() -- Destroy the observer entirely
|
||||
```
|
||||
|
||||
### Element
|
||||
|
||||
An `Element` is a specialized reactive state value, primarily designed for managing the visibility or active state of UI components, in conjunction with the `Chemical.Router`.
|
||||
The different between `Element` and `Value` is that `Element`s have reactive parameters which can be retrieved and are set by the `Router`.
|
||||
|
||||
**API:**
|
||||
|
||||
* `Chemical.Element(): Element`: Constructor. Initializes with a state of `false`.
|
||||
* `:get(): boolean`: Gets the current boolean state. Registers a dependency if used in a `Computed`.
|
||||
* `:set(newState: boolean)`: Sets the boolean state.
|
||||
* `:params(): { from?: string, [any]: any }` : This is the reactive parameter object, which can contain the reserved key `from`.
|
||||
* `:params(newParams: { from?: string, [any]: any })`: Sets or gets an associated parameters table. The `Router` uses this to pass information like the previous path (`from`) when an element's state changes due to a route transition.
|
||||
* `:onChange(callback: (newState: boolean, fromPath?: string) -> ()): () -> ()`: Listens for changes to the element's boolean state. The callback receives the new state and the `from` property of the current params. Returns a disconnect function.
|
||||
* `:destroy()`: Destroys the `Element`.
|
||||
* `.__persistent: boolean?`: (Internal property set by Router) If true, the Router will not automatically set this Element to `false` when navigating away from its associated path.
|
||||
|
||||
**Example (Conceptual, often used with Router):**
|
||||
|
||||
```lua
|
||||
local Chemical = require(path.to.Chemical)
|
||||
local router = Chemical.Router
|
||||
local settingsPageElement = Chemical.Element()
|
||||
|
||||
-- In UI Creation:
|
||||
-- Visible = settingsPageElement,
|
||||
|
||||
router:paths({
|
||||
{Path = "/settings", Element = settingsPageElement}
|
||||
})
|
||||
|
||||
-- Elsewhere (e.g., Router logic):
|
||||
-- router:to("/settings", { message = "hello" })
|
||||
```
|
||||
|
||||
### Watch
|
||||
|
||||
`Watch` allows you to observe a specific key within a table that is itself held by a `Value` or `Computed` object. The callback triggers only when the value associated with that *specific key* changes.
|
||||
|
||||
**API:**
|
||||
|
||||
* `Chemical.Watch(targetGetter: () -> ({ targetTableContainer: Value<{[any]:any}> | Computed<{[any]:any}>, key: any }), callback: (newValueForKey: any?, oldValueForKey: any?) -> ()): () -> ()`: Constructor.
|
||||
* `targetGetter`: A function that must return a table and a string:
|
||||
* The `Value` or `Computed` object that holds the table you want to watch.
|
||||
* The specific `key` within that table whose value changes you want to monitor.
|
||||
* Any reactive `:get()` calls within will establish dependencies for re-evaluating which table/key to watch if those dependencies change (though the primary use is for a single reactive table).
|
||||
* `callback`: A function invoked when the value of `targetTableContainer:get()[key]` changes. It receives the new and old values for that specific key.
|
||||
* Returns a `disconnectFunction`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```lua
|
||||
local Chemical = require(path.to.Chemical)
|
||||
|
||||
local userProfile = Chemical.Value({
|
||||
username = "User123",
|
||||
score = 100,
|
||||
inventory = {"sword", "shield"}
|
||||
})
|
||||
|
||||
local disconnectUsernameWatch = Chemical.Watch(
|
||||
function() return userProfile:get(), "username" end,
|
||||
function(newUsername, oldUsername)
|
||||
print(string.format("Username changed from '%s' to '%s'", oldUsername, newUsername))
|
||||
end
|
||||
)
|
||||
|
||||
local disconnectScoreWatch = Chemical.Watch(
|
||||
function() return userProfile:get(), "score" end,
|
||||
function(newScore, oldScore)
|
||||
print(string.format("Score changed from %s to %s", oldScore, newScore))
|
||||
end
|
||||
)
|
||||
|
||||
-- Update the whole profile table
|
||||
userProfile:set({
|
||||
username = "PlayerOne",
|
||||
score = 150,
|
||||
inventory = {"sword", "shield", "potion"}
|
||||
})
|
||||
-- Output:
|
||||
-- Username changed from 'User123' to 'PlayerOne'
|
||||
-- Score changed from 100 to 150
|
||||
|
||||
-- Update using :key (also triggers Watch if the specified key is watched)
|
||||
userProfile:key("score", 200)
|
||||
-- Output:
|
||||
-- Score changed from 150 to 200
|
||||
```
|
||||
|
||||
`Watch` has a very specific use case in regards to `Element`s.
|
||||
|
||||
```lua
|
||||
local Chemical = require(path.to.Chemical)
|
||||
|
||||
local router = Chemical.Router
|
||||
local someElement = Chemical.Element()
|
||||
|
||||
router:paths({
|
||||
{ Path = "/settings", Element = someElement }
|
||||
})
|
||||
|
||||
local disconnect = Chemical.Watch(
|
||||
function() return someElement:params(), "message" end,
|
||||
function(new, old) print("New message: ", new) end
|
||||
)
|
||||
|
||||
router:to("/settings", { message = "hello" }) --Prints "New message: hello"
|
||||
|
||||
--Element:params() can also be accessed directly, usually inside of an Observer after the Element's state has changed.
|
||||
```
|
||||
|
||||
## 5. UI Creation & Management
|
||||
|
||||
Chemical provides a declarative API for creating and managing Roblox GUI elements, making it easy to bind UI properties to reactive state.
|
||||
|
||||
### Chemical.Create()
|
||||
|
||||
`Chemical.Create(className: string): (propertyTable: dictionary): GuiObject`
|
||||
|
||||
Creates a new instance of the specified `className` (e.g., "Frame", "TextLabel") and applies properties defined in `propertyTable`.
|
||||
This supports intellisense for each className of GuiObjects!
|
||||
|
||||
**`propertyTable` Keys and Values:**
|
||||
|
||||
* **Standard Properties:** (e.g., `Size`, `Position`, `BackgroundColor3`, `Text`, `Visible`)
|
||||
* Can be static values: `Size = UDim2.fromScale(0.1, 0.1)`
|
||||
* Can be a `Value` or `Computed` object: `Visible = myVisibilityValue` (where `myVisibilityValue` is a `Value<boolean>`). The UI property will automatically update when the reactive object changes.
|
||||
* **`Parent: Instance | Value<Instance> | Computed<Instance>`**: Sets the parent of the created element. Can be static or reactive.
|
||||
* **`Children: {GuiObject}`**: An array of other `Chemical.Create()` calls or static GuiObject references. The created child elements will be parented to this element.
|
||||
* **UI Traits** (see below).
|
||||
|
||||
**Example (from `LocalScript Examples`):**
|
||||
|
||||
```lua
|
||||
local Chemical = require(path.to.Chemical)
|
||||
local Create = Chemical.Create
|
||||
local Value = Chemical.Value
|
||||
local PlayerGui = game.Players.LocalPlayer.PlayerGui
|
||||
|
||||
local frameColor = Value(Color3.fromRGB(200, 200, 200))
|
||||
local childFrameRef = Value() -- Will hold the reference to the child frame
|
||||
|
||||
local mainFrame = Create("Frame"){
|
||||
Size = UDim2.fromScale(0.5, 0.5),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
BackgroundColor3 = frameColor, -- Reactive property
|
||||
|
||||
Visible = true,
|
||||
|
||||
Children = {
|
||||
Create("Frame"){
|
||||
Name = "ChildInnerFrame",
|
||||
|
||||
Size = UDim2.fromScale(0.5, 0.5),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
BackgroundColor3 = Color3.fromRGB(100, 100, 100),
|
||||
|
||||
Visible = true,
|
||||
|
||||
[Chemical.Ref] = childFrameRef -- Assign instance to childFrameRef Value
|
||||
},
|
||||
|
||||
Create("TextButton"){
|
||||
Name = "ColorChangeButton",
|
||||
|
||||
Size = UDim2.fromOffset(100, 30),
|
||||
Position = UDim2.fromScale(0.5, 0.8),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
Text = "Change Color",
|
||||
|
||||
[Chemical.onEvent("MouseButton1Click")] = function()
|
||||
frameColor:set(Color3.fromRGB(math.random(0, 255), math.random(0, 255), math.random(0, 255)))
|
||||
end
|
||||
}
|
||||
},
|
||||
|
||||
Parent = Create("ScreenGui"){
|
||||
Name = "MyChemicalScreenGui",
|
||||
Parent = PlayerGui
|
||||
}
|
||||
}
|
||||
|
||||
task.wait(2)
|
||||
|
||||
|
||||
print("Child frame reference:", childFrameRef:get().Name) -- Output: ChildInnerFrame
|
||||
|
||||
```
|
||||
|
||||
### Chemical.Give()
|
||||
|
||||
`Chemical.Give(instance: GuiObject): (propertyTable: dictionary): GuiObject`
|
||||
|
||||
Applies reactive properties and traits to an *existing* `instance`. The `propertyTable` structure and capabilities are the same as for `Chemical.Create()`. This is useful for hydrating GUIs not created by Chemical or for applying reactive behavior incrementally.
|
||||
|
||||
```lua
|
||||
local existingFrame = script.Parent.SomeFrame -- Assuming a Frame exists
|
||||
local frameVisible = Chemical.Value(true)
|
||||
|
||||
Chemical.Give(existingFrame) {
|
||||
Visible = frameVisible, --Reactive object value
|
||||
|
||||
[Chemical.onChange("AbsoluteSize")] = function(newSize)
|
||||
print("Frame AbsoluteSize changed to:", newSize)
|
||||
end
|
||||
}
|
||||
|
||||
task.wait(3)
|
||||
|
||||
frameVisible:set(false) -- existingFrame will become invisible
|
||||
```
|
||||
|
||||
### UI Traits
|
||||
|
||||
Special keys used within the `propertyTable` of `Create` and `Give` to add specific behaviors:
|
||||
|
||||
* **`[Chemical.Ref] = refValue: Value<Instance>`**:
|
||||
When the UI element is created (by `Create`), or hydrated (by `Give`), `refValue:set(createdInstance)` is called. This allows you to get a reactive reference to the instance itself.
|
||||
* **`[Chemical.onEvent(eventName: string)] = callback: (() -> ()) | Value<boolean>`**:
|
||||
Connects to the specified `eventName` (e.g., "MouseButton1Click", "MouseEnter") of the UI element.
|
||||
* If `callback` is a function, it's called when the event fires. Arguements of the event *are* passed to the function.
|
||||
* If `callback` is a `Value<boolean>`, its value is set to `true` when the event fires. (The actual arguments of the event are not passed to the Value's set method).
|
||||
* **`[Chemical.onChange(propertyName: string | Value<any> | Computed<any>)] = callback: ((newValue: any) -> ()) | Value<any>`**:
|
||||
* If `propertyName` is a string: Listens to `Instance:GetPropertyChangedSignal(propertyName)`.
|
||||
* If `callback` is a function, it's called with the new property value.
|
||||
* If `callback` is a `Value`, its `set` method is called with the new property value.
|
||||
* You might use this for TextBox.Text properties, such that when the value of the TextBox changes so too does the reactive Value Object's value.
|
||||
* If `propertyName` is a `Value`, `Computed`, or `Element` (reactive object): Creates an `Observer` for this reactive object.
|
||||
* If `callback` is a function, it's called when the reactive object changes (receives `newValue, oldValue`).
|
||||
* (Using a Value as callback for a reactive propertyName is less common and might imply a two-way binding if not careful, however it is permitted).
|
||||
|
||||
|
||||
All connections made via these traits are automatically disconnected when the GuiObject they are attached to is destroyed (via `instance:Destroy()` or `Chemical.Destroy()`).
|
||||
|
||||
## 6. State Replication
|
||||
|
||||
`Chemical.Reaction` is a singleton service that automates the synchronization of state between the server and connected clients. It supports replicating both static values and reactive `Value`/`Computed` objects.
|
||||
|
||||
**Key Characteristics:**
|
||||
|
||||
* **Channel & Key Identification:** Reactions are identified by a `(channelName: string, reactionKey: string)` pair.
|
||||
* **One-Way Server-to-Client:** The primary flow of data is from server to client. Client-side changes to replicated state do not automatically propagate back to the server via this system.
|
||||
* **Nested Structure:** Supports state tables with up to one level of nesting where the nested values can be `Value` objects.
|
||||
|
||||
```lua
|
||||
-- Example of supported state structure for Reaction
|
||||
local state = {
|
||||
staticTopLevel = "hello",
|
||||
reactiveTopLevel = Chemical.Value(10),
|
||||
nestedObject = {
|
||||
staticNested = true,
|
||||
reactiveNested = Chemical.Value("world")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **Tokenization:** Internally, channel names, reaction keys, and field keys are tokenized (converted to numbers) for efficient network transmission.
|
||||
* **Initial Hydration:** When a client connects or is ready, it receives a full snapshot of all existing reactions it's concerned with.
|
||||
* **Delta Updates (for `Value` objects):** Only changes to `Value` objects are networked after initial hydration. Changes to static parts of the state after creation are not automatically replicated. If a `Value` object itself holds a table, the system is *designed to* (aims to in the future) send only the changed parts of that table.
|
||||
|
||||
### Reaction Server-Side API
|
||||
|
||||
Accessible via `local Reaction = Chemical.Reaction`.
|
||||
|
||||
* `Reaction:create(channelName: string, reactionKeyName: string, initialState: table): ServerReactionAPI`:
|
||||
* Creates a new reaction on the server and broadcasts its construction to all clients.
|
||||
* `initialState`: The table defining the reaction's state. Values within this table can be static Luau types or `Chemical.Value`/`Chemical.Computed` instances.
|
||||
* Returns a `ServerReactionAPI` object with two methods:
|
||||
* `destroy()`: Destroys the reaction on the server and notifies clients to deconstruct it. All reactive `Value` objects within its state are also destroyed.
|
||||
* `raw()`: Returns a deep, non-reactive snapshot of the current state of the reaction. `Value` objects are replaced with their current values.
|
||||
|
||||
### Reaction Client-Side API
|
||||
|
||||
Accessible via `local Reaction = Chemical.Reaction()`.
|
||||
|
||||
* `Reaction:await(channelName: string, reactionKeyName: string): Promise<ClientReaction>`:
|
||||
* Returns with the client-side reaction object once it has been constructed (either through initial hydration or a `ConstructReaction` packet).
|
||||
* The resolved `ClientReaction` object mirrors the structure of the server's `initialState`, where server-side `Chemical.Value`/`Chemical.Computed` instances become client-side `Chemical.Value` instances. Static values remain static.
|
||||
* `Reaction:onCreate(channelName: string, callback: (reactionKeyName: string, reactionObject: ClientReaction) -> ()): () -> ()`:
|
||||
* Subscribes to creations of new reactions within the specified `channelName`.
|
||||
* The `callback` is invoked immediately for any already existing reactions in that channel, and then for any new ones as they are constructed.
|
||||
* Returns a `disconnectFunction` to stop listening.
|
||||
|
||||
### Reaction Example Usage
|
||||
|
||||
*(See `Chemical/Examples` script for a practical implementation which creates a `PlayerData` reaction per player.)*
|
||||
|
||||
**Server (`ServerScriptService`):**
|
||||
|
||||
```lua
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local Players = game:GetService("Players")
|
||||
local Chemical = require(ReplicatedStorage.Chemical)
|
||||
|
||||
local Reaction = Chemical.Reaction -- Get the singleton instance
|
||||
|
||||
Players.PlayerAdded:Connect(function(player)
|
||||
-- Define initial state, some parts reactive, some static
|
||||
local healthValue = Chemical.Value(100)
|
||||
local manaValue = Chemical.Value(50)
|
||||
|
||||
local playerStatsState = {
|
||||
PlayerName = player.Name, -- Static
|
||||
UserId = player.UserId, -- Static
|
||||
Health = healthValue, -- Reactive
|
||||
Mana = manaValue, -- Reactive
|
||||
Inventory = {
|
||||
Gold = Chemical.Value(10), -- Nested reactive
|
||||
Items = {"Sword", "Shield"} -- Nested static
|
||||
}
|
||||
}
|
||||
|
||||
-- Create the reaction for this player
|
||||
-- The "PlayerData" channel could hold reactions for all players
|
||||
local myReaction = Reaction:create("PlayerData", tostring(player.UserId), playerStatsState)
|
||||
print("Created reaction for player:", player.Name)
|
||||
|
||||
-- Example of updating a reactive value after creation
|
||||
task.delay(5, function()
|
||||
if player and myReaction then -- Ensure player and reaction still exist
|
||||
print("Server: Setting health for", player.Name, "to 75")
|
||||
healthValue:set(75) -- This change will be replicated to clients
|
||||
end
|
||||
end)
|
||||
|
||||
-- When player leaves, destroy their reaction
|
||||
player.Removing:Connect(function()
|
||||
if myReaction then
|
||||
print("Server: Destroying reaction for player:", player.Name)
|
||||
myReaction:destroy()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
**Client (`LocalScript` in `StarterPlayerScripts`):**
|
||||
|
||||
```lua
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local Players = game:GetService("Players")
|
||||
local Chemical = require(ReplicatedStorage.Chemical)
|
||||
|
||||
local Reaction = Chemical.Reaction -- Get the singleton instance
|
||||
local Create = Chemical.Create
|
||||
local localPlayer = Players.LocalPlayer
|
||||
|
||||
local function setupPlayerUI(playerData)
|
||||
print("Client: Received PlayerData for", playerData.PlayerName, playerData)
|
||||
|
||||
local screenGui = Create("ScreenGui"){ Parent = localPlayer.PlayerGui }
|
||||
|
||||
Create("TextLabel"){
|
||||
Name = "HealthDisplay",
|
||||
Size = UDim2.fromOffset(200, 30),
|
||||
Position = UDim2.fromScale(0.5, 0.1),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
Text = Chemical.Computed(function() -- Text reactively updates
|
||||
return string.format("Name: %s | Health: %d", playerData.PlayerName, playerData.Health:get())
|
||||
end),
|
||||
|
||||
Parent = screenGui,
|
||||
}
|
||||
|
||||
Create("TextLabel"){
|
||||
Name = "ManaDisplay",
|
||||
Size = UDim2.fromOffset(200, 30),
|
||||
Position = UDim2.fromScale(0.5, 0.15),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
Text = Chemical.Computed(function()
|
||||
return "Mana: " .. playerData.Mana:get()
|
||||
end),
|
||||
|
||||
Parent = screenGui,
|
||||
}
|
||||
|
||||
Create("TextLabel"){
|
||||
Name = "GoldDisplay",
|
||||
Size = UDim2.fromOffset(200, 30),
|
||||
Position = UDim2.fromScale(0.5, 0.2),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
Text = Chemical.Computed(function()
|
||||
return "Gold: " .. playerData.Inventory.Gold:get()
|
||||
end),
|
||||
|
||||
Parent = screenGui,
|
||||
}
|
||||
|
||||
-- Observe changes locally if needed
|
||||
Chemical.Observer(playerData.Health):onChange(function(newHealth)
|
||||
print("Client: Health is now", newHealth)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Await this specific player's data
|
||||
Reaction:await("PlayerData", tostring(localPlayer.UserId))
|
||||
:andThen(setupPlayerUI)
|
||||
:catch(function(err)
|
||||
warn("Client: Failed to get PlayerData reaction:", err)
|
||||
end)
|
||||
|
||||
-- Alternatively, listen to all reactions in a channel
|
||||
-- local disconnectListener = Reaction:onCreate("PlayerData", function(reactionKey, reactionObject)
|
||||
-- if reactionKey == tostring(localPlayer.UserId) then
|
||||
-- print("Client: PlayerData (via onCreate) for me!", reactionObject)
|
||||
-- -- setupPlayerUI(reactionObject)
|
||||
-- -- if you use onCreate, you might want to manage disconnects or ensure UI is only set up once.
|
||||
-- else
|
||||
-- print("Client: PlayerData (via onCreate) for another player:", reactionKey)
|
||||
-- end
|
||||
-- end)
|
||||
```
|
||||
|
||||
## 7. Client-Side Routing
|
||||
|
||||
The `Chemical.Router` is a singleton service for managing client-side application flow by defining paths and associating them with reactive `Chemical.Element`s. When the route changes, corresponding `Element`s are activated or deactivated, typically controlling UI visibility.
|
||||
|
||||
**API:**
|
||||
|
||||
* `Chemical.Router: RouterInstance`: Gets the singleton router instance.
|
||||
* `router:paths(routes: {{ Path: string, Element: Chemical.Element, Persistent?: boolean }})`: Defines a set of routes.
|
||||
* `Path`: A string like "/shop/items" or "/profile". Leading/trailing slashes are handled.
|
||||
* `Element`: The `Chemical.Element` instance that will be set to `true` when this path is active.
|
||||
* `Persistent` (optional, boolean): If true, the `Element` will not be automatically set to `false` when navigating to a sibling or parent. It will still be set to `false` if explicitly exited.
|
||||
* `router:to(path: string, params?: table)`: Navigates to the specified `path`.
|
||||
* Deactivates elements associated with the previous path (respecting persistence and shared ancestry).
|
||||
* Activates the `Element` for the new `path`.
|
||||
* `params` is an optional table passed to the target `Element`'s `:params()` method. `params.from` is automatically set to the old path.
|
||||
* `router:is(path: string): boolean`: Returns `true` if the current path exactly matches the given `path`.
|
||||
* `router:exit(path: string, params?: table)`: Explicitly deactivates the `Element` (and its descendant elements in the route tree) associated with the `path`. Sets `CurrentPath` to `""`.
|
||||
* `router:onBeforeChange(callback: (newPath: string, oldPath: string) -> ())`: Registers a callback invoked before the current path changes and any `Element`s close.
|
||||
* `router:onChange(callback: (newPath: string, oldPath: string) -> ())`: Registers a callback invoked when `CurrentPath`'s value changes. This is syntactic sugar for `Chemical.Observer(router.CurrentPath):onChange(...)`.
|
||||
* `router:onAfterChange(callback: (newPath: string, oldPath: string) -> ())`: Registers a callback invoked after the current path has changed and target elements have been updated.
|
||||
* `router.CurrentPath: Value<string>`: A reactive `Value` object holding the current active path string.
|
||||
|
||||
**Example (from `LocalScript Examples`):**
|
||||
|
||||
```lua
|
||||
local Chemical = require(path.to.Chemical)
|
||||
local Create = Chemical.Create
|
||||
local Give = Chemical.Give
|
||||
local Value = Chemical.Value
|
||||
local Watch = Chemical.Watch
|
||||
local Ref = Chemical.Ref
|
||||
local PlayerGui = game.Players.LocalPlayer.PlayerGui
|
||||
|
||||
local Router = Chemical.Router -- Get the singleton instance
|
||||
|
||||
-- Define Elements for different pages/views
|
||||
local tutorialPageElement = Chemical.Element()
|
||||
local homePageElement = Chemical.Element()
|
||||
|
||||
local tutorialFrame = Value()
|
||||
|
||||
-- Define routes
|
||||
Router:paths({
|
||||
{ Path = "/tutorial", Element = tutorialPageElement },
|
||||
{ Path = "/home", Element = homePageElement, Persistent = true }
|
||||
})
|
||||
|
||||
-- Create UI that reacts to these elements
|
||||
Create("ScreenGui"){
|
||||
Parent = PlayerGui,
|
||||
Children = {
|
||||
Create("Frame"){ -- Tutorial Page
|
||||
Name = "TutorialFrame",
|
||||
Size = UDim2.fromScale(0.8, 0.8),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundColor3 = Color3.fromRGB(50, 50, 150),
|
||||
|
||||
Visible = tutorialPageElement, -- Reactive visibility
|
||||
|
||||
[Ref] = tutorialFrame, --Set the reference to this Frame
|
||||
|
||||
Children = {
|
||||
Create("TextLabel"){ Text = "Tutorial Page", Size = UDim2.fromScale(1,0.1)}
|
||||
}
|
||||
},
|
||||
Create("Frame"){ -- Home Page
|
||||
Name = "HomeFrame",
|
||||
Size = UDim2.fromScale(0.7, 0.7),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundColor3 = Color3.fromRGB(50, 150, 50),
|
||||
|
||||
Visible = homePageElement, -- Reactive visibility
|
||||
|
||||
Children = {
|
||||
Create("TextLabel"){ Text = "Home Page", Size = UDim2.fromScale(1,0.1)}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
--When the tutorialPageElement params are changed by the Router, we'll apply the specific key `customParam`'s value to the tutorialFrame.
|
||||
Watch(
|
||||
function() return tutorialPageElement:params(), "customParam" end,
|
||||
function(new) Give(tutorialFrame:get()) { Name = new } end
|
||||
)
|
||||
|
||||
-- Listen to route changes
|
||||
Router:onChange(function(newPath, oldPath)
|
||||
print(string.format("Route changed from '%s' to '%s'", oldPath, newPath))
|
||||
end)
|
||||
|
||||
-- Navigate
|
||||
task.wait(1)
|
||||
print("Navigating to /tutorial")
|
||||
Router:to("/tutorial", { customParam = "hello from tutorial start" })
|
||||
|
||||
task.wait(3)
|
||||
print("Navigating to /home")
|
||||
Router:to("/home") -- tutorialPageElement becomes false, homePageElement becomes true
|
||||
|
||||
task.wait(3)
|
||||
print("Navigating to /tutorial again")
|
||||
Router:to("/tutorial") -- homePageElement remains true (persistent), tutorialPageElement becomes true
|
||||
|
||||
task.wait(3)
|
||||
print("Exiting /tutorial (not /home because it's persistent from this level)")
|
||||
Router:exit("/tutorial") -- tutorialPageElement becomes false
|
||||
|
||||
task.wait(3)
|
||||
print("Exiting /home")
|
||||
Router:exit("/home") -- homePageElement becomes false
|
||||
```
|
||||
|
||||
## 8. Utility Functions
|
||||
|
||||
### Chemical Await
|
||||
|
||||
* **`Chemical.Await(chemicalObject: Value<any> | Computed<any>): T`**
|
||||
Yields the current thread until the provided `chemicalObject` (a `Value`, `Computed`, `Element`, or `Router`) changes its value at least once *after* `Await` is called. It resolves with no arguments once the change occurs. Essentially, it's a promise that resolves on the next change.
|
||||
* **Note:** This uses `Promise.new` internally and `observer:onChange`. The actual return value `T` here is effectively `void` from the promise's perspective (it resolves with no values). Its primary use is to pause execution until a specific reactive data point is known to have received an update.
|
||||
|
||||
### Chemical Destroy
|
||||
|
||||
* **`Chemical.Destroy(subject: Destroyable)`**
|
||||
A versatile cleanup function that attempts to properly destroy or disconnect various types of objects:
|
||||
* Objects with a `:destroy()` (or `:Destroy()`) method (like Chemical primitives).
|
||||
* Tables: Clears them and sets their metatable to nil. If elements within are `Destroyable`, recursively calls `Chemical.Destroy` on them.
|
||||
* Roblox `Instance`s: Calls `instance:Destroy()`.
|
||||
* `RBXScriptConnection`s: Calls `connection:Disconnect()`. - In the future, it will be possible to handle ConnectionLike objects.
|
||||
* Functions: Calls the function (intended for disconnect functions).
|
||||
* Threads: Calls `task.cancel(thread)`.
|
||||
* Tables: Recurscively calls `Chemical.Destroy(Table)`.
|
||||
|
||||
### Chemical Nothing
|
||||
* **`Chemical.Nothing()`**
|
||||
As the name implies, it does nothing. This can be useful when you want to specify that the cleanup of a Computed or Iterator should do nothing.
|
||||
|
||||
## 9. Under The Hood (Advanced)
|
||||
|
||||
### Networking
|
||||
|
||||
The `Chemical.Reaction` system leverages these `Packet` definitions for its `Construct`, `Deconstruct`, `UpdateRoot`, `UpdateNested`, `Ready`, and `Hydrate` operations.
|
||||
|
||||
## 10. Type System
|
||||
|
||||
Chemical heavily utilizes Luau's type annotation system for improved code clarity, maintainability, and editor intellisense.
|
||||
|
||||
* **Root `Types.lua`:** Defines the public interface types for core Chemical objects like `Value<T>`, `Computed<T>`, `Observer`, `Element`, and `Reaction`.
|
||||
* **`Types/Gui.lua` & `Types/Gui/Overrides.lua`:** Provide exhaustive type definitions for Roblox GUI object properties (`FrameProperties`, `TextLabelProperties`, etc.) and event names/signatures. These are crucial for the type-safe/intellisense usage of `Chemical.Create` and `Chemical.Give`.
|
||||
* **Inline Type Annotations:** Throughout the codebase, functions, variables, and table fields are typed.
|
||||
|
||||
## 11. Examples
|
||||
|
||||
The `Chemical/Examples` (Script) and `Chemical/Examples` (LocalScript) provide practical demonstrations:
|
||||
|
||||
* **Server-Side (`Examples` Script):**
|
||||
* Demonstrates creating a per-player `Reaction` with nested `Value` objects.
|
||||
* Shows how the server defines the reaction's state and structure.
|
||||
* The `export type Type` defines the expected structure of the client-side reaction object for better type safety on the client.
|
||||
* **Client-Side (`Examples` LocalScript):**
|
||||
* Uses `Chemical.Router` to define simple routes.
|
||||
* Uses `Chemical.Create` to build UI elements.
|
||||
* Demonstrates binding UI properties (like `Visible` and `BackgroundColor3`) to `Chemical.Element` and `Chemical.Value` objects, respectively.
|
||||
* Shows how to use `[Chemical.Ref]` to get a reference to a created UI element.
|
||||
* Illustrates connecting to UI events using `[Chemical.onEvent]`.
|
||||
* Shows basic router navigation with `:to()` and `:exit()`.
|
||||
### Reactive state management & replication and declarative UI composition library.
|
||||
#### Latest Release: https://github.com/Sovvie/Chemical/releases/tag/v0.2.5
|
||||
|
|
7
aftman.toml
Normal file
7
aftman.toml
Normal file
|
@ -0,0 +1,7 @@
|
|||
# This file lists tools managed by Aftman, a cross-platform toolchain manager.
|
||||
# For more information, see https://github.com/LPGhatguy/aftman
|
||||
|
||||
# To add a new tool, add an entry to this table.
|
||||
[tools]
|
||||
rojo = "rojo-rbx/rojo@7.5.1"
|
||||
# rojo = "rojo-rbx/rojo@6.2.0"
|
30
default.project.json
Normal file
30
default.project.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "project",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"ReplicatedStorage": {
|
||||
"$className": "ReplicatedStorage",
|
||||
"$ignoreUnknownInstances": true,
|
||||
"$path": "src/ReplicatedStorage"
|
||||
},
|
||||
"ServerScriptService": {
|
||||
"$className": "ServerScriptService",
|
||||
"$ignoreUnknownInstances": true,
|
||||
"$path": "src/ServerScriptService"
|
||||
},
|
||||
"StarterPlayer": {
|
||||
"$className": "StarterPlayer",
|
||||
"StarterPlayerScripts": {
|
||||
"$className": "StarterPlayerScripts",
|
||||
"$ignoreUnknownInstances": true,
|
||||
"$path": "src/StarterPlayer/StarterPlayerScripts"
|
||||
},
|
||||
"$ignoreUnknownInstances": true
|
||||
},
|
||||
"TestService": {
|
||||
"$className": "TestService",
|
||||
"$ignoreUnknownInstances": true,
|
||||
"$path": "src/TestService"
|
||||
}
|
||||
}
|
||||
}
|
1
selene.toml
Normal file
1
selene.toml
Normal file
|
@ -0,0 +1 @@
|
|||
std = "roblox"
|
123
src/Chemical/Cache.lua
Normal file
123
src/Chemical/Cache.lua
Normal file
|
@ -0,0 +1,123 @@
|
|||
local module = {}
|
||||
module.Tokens = {}
|
||||
module.Stack = {}
|
||||
module.Queues = {}
|
||||
|
||||
do
|
||||
local tokenCount = 0
|
||||
local stringsToTokens = {}
|
||||
local tokensToSrings = {}
|
||||
|
||||
local tokenMap = {}
|
||||
|
||||
function module:Tokenize(key: string): number
|
||||
if stringsToTokens[key] then return stringsToTokens[key] end
|
||||
|
||||
tokenCount += 1
|
||||
|
||||
stringsToTokens[key] = tokenCount
|
||||
tokensToSrings[tokenCount] = key
|
||||
|
||||
return tokenCount
|
||||
end
|
||||
|
||||
function module:FromToken(token: number): string
|
||||
return tokensToSrings[token]
|
||||
end
|
||||
|
||||
|
||||
function module:MapAToken(key: string, token: number)
|
||||
tokenMap[token] = key
|
||||
end
|
||||
|
||||
function module:FromAMap(token: number): string
|
||||
return tokenMap[token]
|
||||
end
|
||||
|
||||
function module:TokenClear(key: string | number)
|
||||
if typeof(key) == "string" then
|
||||
local token = stringsToTokens[key]
|
||||
stringsToTokens[key] = nil
|
||||
tokensToSrings[token] = nil
|
||||
elseif typeof(key) == "number" then
|
||||
local str = tokensToSrings[key]
|
||||
stringsToTokens[str] = nil
|
||||
tokensToSrings[key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function module.Tokens.new()
|
||||
local tokenCount = 0
|
||||
local stringsToTokens = {}
|
||||
local tokensToSrings = {}
|
||||
|
||||
return {
|
||||
ToToken = function(self: {}, key: string): number
|
||||
if stringsToTokens[key] then return stringsToTokens[key] end
|
||||
|
||||
tokenCount += 1
|
||||
|
||||
stringsToTokens[key] = tokenCount
|
||||
tokensToSrings[tokenCount] = key
|
||||
return tokenCount
|
||||
end,
|
||||
|
||||
ToTokenPath = function(self: {}, keys: {string}): { number }
|
||||
local tokens = {}
|
||||
|
||||
for _, key in keys do
|
||||
table.insert(tokens, self:ToToken(key))
|
||||
end
|
||||
|
||||
return tokens
|
||||
end,
|
||||
|
||||
Is = function(self: {}, key: string): boolean
|
||||
return stringsToTokens[key] ~= nil
|
||||
end,
|
||||
|
||||
From = function(self: {}, token: number): string
|
||||
return tokensToSrings[token]
|
||||
end,
|
||||
|
||||
FromPath = function(self: {}, tokens: { number }): { string }
|
||||
local strings = {}
|
||||
for _, token in tokens do
|
||||
table.insert(strings, tokensToSrings[token])
|
||||
end
|
||||
return strings
|
||||
end,
|
||||
|
||||
Map = function(self: {}, stringsToTokens: { [string]: number })
|
||||
for key, value in stringsToTokens do
|
||||
if typeof(value) == "table" then
|
||||
self:Map(value)
|
||||
continue
|
||||
end
|
||||
stringsToTokens[key] = value
|
||||
tokensToSrings[value] = key
|
||||
end
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
local stack = {}
|
||||
function module.Stack:Push(entry: any): number
|
||||
table.insert(stack, entry)
|
||||
return #stack
|
||||
end
|
||||
|
||||
function module.Stack:Top(): any
|
||||
return stack[#stack]
|
||||
end
|
||||
|
||||
function module.Stack:Pop(index: number?)
|
||||
stack[index or #stack] = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
return module
|
41
src/Chemical/Classes/Object.lua
Normal file
41
src/Chemical/Classes/Object.lua
Normal file
|
@ -0,0 +1,41 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Types = require(RootFolder.Types)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local module = {}
|
||||
|
||||
type Object = Types.HasEntity & {
|
||||
use: (self: Object, ...{}) -> (),
|
||||
}
|
||||
|
||||
function module.new(metamethods: {}?): Object
|
||||
local inherits = {}
|
||||
metamethods = metamethods or {}
|
||||
|
||||
metamethods.__index = function(self, index)
|
||||
local has = rawget(self, index)
|
||||
if has then return has
|
||||
else if inherits[index] then return inherits[index] end
|
||||
end
|
||||
end
|
||||
|
||||
local object = setmetatable({
|
||||
entity = ECS.World:entity()
|
||||
}, metamethods)
|
||||
|
||||
object.use = function(self, ...: {})
|
||||
local classes = { ... }
|
||||
for _, class in classes do
|
||||
for key, value in class do
|
||||
inherits[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
object.use = nil
|
||||
end
|
||||
|
||||
return object
|
||||
end
|
||||
|
||||
return module
|
3
src/Chemical/Classes/init.meta.json
Normal file
3
src/Chemical/Classes/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
109
src/Chemical/ECS.lua
Normal file
109
src/Chemical/ECS.lua
Normal file
|
@ -0,0 +1,109 @@
|
|||
--!nonstrict
|
||||
local Packages = script.Parent.Packages
|
||||
local ECS = require(Packages.JECS)
|
||||
|
||||
export type Entity<T = nil> = ECS.Entity<T>
|
||||
|
||||
local Tags = {
|
||||
-- State Management Tags
|
||||
IsStatic = ECS.tag(),
|
||||
IsPrimitive = ECS.tag(),
|
||||
IsSettable = ECS.tag(),
|
||||
IsStateful = ECS.tag(),
|
||||
IsStatefulTable = ECS.tag(),
|
||||
IsStatefulDictionary = ECS.tag(),
|
||||
IsComputed = ECS.tag(),
|
||||
IsEffect = ECS.tag(),
|
||||
IsDirty = ECS.tag(),
|
||||
IsDeepComparable = ECS.tag(),
|
||||
|
||||
-- Relationship Tags
|
||||
SubscribesTo = ECS.tag(),
|
||||
HasSubscriber = ECS.tag(),
|
||||
InScope = ECS.tag(),
|
||||
ChildOf = ECS.ChildOf,
|
||||
|
||||
-- UI-specific Tags TODO
|
||||
IsHost = ECS.tag(),
|
||||
ManagedBy = ECS.tag(),
|
||||
UIParent = ECS.tag(),
|
||||
}
|
||||
|
||||
local World = ECS.world()
|
||||
|
||||
|
||||
local Components = {
|
||||
Name = ECS.Name,
|
||||
Object = World:component(),
|
||||
Value = World:component(),
|
||||
PrevValue = World:component(),
|
||||
Callback = World:component(),
|
||||
CallbackList = World:component(),
|
||||
OnChangeCallbacks = World:component(),
|
||||
OnKVChangeCallbacks = World:component(),
|
||||
Connection = World:component() :: ECS.Entity<RBXScriptConnection>,
|
||||
ConnectionList = World:component() :: ECS.Entity<{RBXScriptConnection}>,
|
||||
Instance = World:component(),
|
||||
ManagedItems = World:component(),
|
||||
LoopType = World:component(),
|
||||
|
||||
|
||||
ComputeFn = World:component(),
|
||||
EffectFn = World:component(),
|
||||
CleanupFn = World:component(),
|
||||
}
|
||||
|
||||
World:set(Components.Connection, ECS.OnRemove, function(entity)
|
||||
local connection = World:get(entity, Components.Connection)
|
||||
if connection then
|
||||
connection:Disconnect()
|
||||
end
|
||||
end)
|
||||
|
||||
World:set(Components.ConnectionList, ECS.OnRemove, function(entity)
|
||||
local connections = World:get(entity, Components.ConnectionList)
|
||||
if connections then
|
||||
for _, conn in ipairs(connections) do
|
||||
conn:Disconnect()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
World:set(Components.Instance, ECS.OnRemove, function(entity)
|
||||
if World:has(entity, Tags.IsHost) then
|
||||
for effectEntity in World:each(ECS.pair(Tags.InScope, entity)) do
|
||||
World:delete(effectEntity)
|
||||
end
|
||||
if World:has(entity, Components.ConnectionList) then
|
||||
World:remove(entity, Components.ConnectionList)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
World:set(Components.Value, ECS.OnChange, function(entity, id, data)
|
||||
if World:has(entity, Tags.IsSettable) then
|
||||
World:add(entity, Tags.IsDirty)
|
||||
end
|
||||
end)
|
||||
|
||||
World:set(Components.Object, ECS.OnRemove, function(entity: ECS.Entity<any>, id: ECS.Id<any>)
|
||||
local object = World:get(entity, Components.Object)
|
||||
if object and object.__internalDestroy then
|
||||
object:__internalDestroy()
|
||||
end
|
||||
end)
|
||||
|
||||
World:add(Tags.SubscribesTo, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
|
||||
World:add(Tags.HasSubscriber, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
|
||||
World:add(Tags.InScope, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
|
||||
World:add(Tags.ManagedBy, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
|
||||
World:add(Tags.UIParent, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
|
||||
|
||||
local module = {
|
||||
Components = Components,
|
||||
Tags = Tags,
|
||||
JECS = ECS,
|
||||
World = World,
|
||||
}
|
||||
|
||||
return module
|
46
src/Chemical/Factories/Computed.lua
Normal file
46
src/Chemical/Factories/Computed.lua
Normal file
|
@ -0,0 +1,46 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
|
||||
local Object = require(RootFolder.Classes.Object)
|
||||
|
||||
local Stateful = require(RootFolder.Mixins.Stateful)
|
||||
local Destroyable = require(RootFolder.Mixins.Destroyable)
|
||||
local Cleanable = require(RootFolder.Mixins.Cleanable)
|
||||
local Computable = require(RootFolder.Mixins.Computable)
|
||||
|
||||
|
||||
export type Computed<T> = Stateful.Stateful<T> & Computable.Computable<T> & Destroyable.Destroyable<T> & Cleanable.Cleanable<T>
|
||||
|
||||
return function<T>(computeFn: () -> T, cleanupFn: (T) -> ()?): Computed<T>
|
||||
local obj = Object.new({
|
||||
__tostring = function(self)
|
||||
local rawValue = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
return `Computed<{tostring(rawValue)}>`
|
||||
end,
|
||||
})
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsComputed)
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.ComputeFn, computeFn)
|
||||
if cleanupFn then ECS.World:set(obj.entity, ECS.Components.CleanupFn, cleanupFn) end
|
||||
|
||||
|
||||
obj:use(
|
||||
Computable,
|
||||
Stateful,
|
||||
Destroyable,
|
||||
Cleanable
|
||||
)
|
||||
|
||||
|
||||
obj:compute()
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
|
||||
return obj
|
||||
end
|
43
src/Chemical/Factories/Effect.lua
Normal file
43
src/Chemical/Factories/Effect.lua
Normal file
|
@ -0,0 +1,43 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Classes = RootFolder.Classes
|
||||
local Mixins = RootFolder.Mixins
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Object = require(Classes.Object)
|
||||
|
||||
local Effectable = require(Mixins.Effectable)
|
||||
local Destroyable = require(Mixins.Destroyable)
|
||||
local Cleanable = require(Mixins.Cleanable)
|
||||
|
||||
export type Effect = Effectable.Effectable & Destroyable.Destroyable & Cleanable.Cleanable
|
||||
type CleanUp = () -> ()
|
||||
|
||||
--- Effect
|
||||
-- Effects will fire after the batch of stateful object changes are propogated.
|
||||
-- The optional cleanup function will fire first, and then the effect's function.
|
||||
-- The effect function can optionally return a cleanup function.
|
||||
-- Effects will be deleted when any one of its dependent objects are destroyed.
|
||||
return function(effectFn: () -> ( CleanUp | nil )): Effect
|
||||
local obj = Object.new()
|
||||
|
||||
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsEffect)
|
||||
ECS.World:set(obj.entity, ECS.Components.EffectFn, effectFn)
|
||||
|
||||
|
||||
obj:use(
|
||||
Cleanable,
|
||||
Destroyable,
|
||||
Effectable
|
||||
)
|
||||
|
||||
|
||||
obj:run()
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
|
||||
return obj
|
||||
end
|
44
src/Chemical/Factories/Map.lua
Normal file
44
src/Chemical/Factories/Map.lua
Normal file
|
@ -0,0 +1,44 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Classes = RootFolder.Classes
|
||||
local Mixins = RootFolder.Mixins
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local Object = require(Classes.Object)
|
||||
|
||||
local Stateful = require(Mixins.Stateful)
|
||||
local StatefulTable = require(Mixins.StatefulTable)
|
||||
local StatefulDictionary = require(Mixins.StatefulDictionary)
|
||||
|
||||
local Settable = require(Mixins.Settable)
|
||||
local Numerical = require(Mixins.Numerical)
|
||||
local Destroyable = require(Mixins.Destroyable)
|
||||
|
||||
export type Value<T> = Stateful.Stateful<T> & Settable.Settable<T> & StatefulDictionary.StatefulDictionary<T> & Destroyable.Destroyable
|
||||
|
||||
return function<T>(value: T): Value<T>
|
||||
local obj = Object.new({
|
||||
__tostring = function(self)
|
||||
return `Map<{self.entity}>`
|
||||
end,
|
||||
})
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStatefulDictionary)
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsSettable)
|
||||
|
||||
|
||||
obj:use(
|
||||
Stateful,
|
||||
Settable,
|
||||
StatefulDictionary,
|
||||
Destroyable
|
||||
)
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Value, value)
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
|
||||
return obj
|
||||
end
|
70
src/Chemical/Factories/Observer.lua
Normal file
70
src/Chemical/Factories/Observer.lua
Normal file
|
@ -0,0 +1,70 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
|
||||
local LinkedList = require(RootFolder.Packages.LinkedList)
|
||||
|
||||
|
||||
local Classes = RootFolder.Classes
|
||||
local Mixins = RootFolder.Mixins
|
||||
local Functions = RootFolder.Functions
|
||||
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Is = require(Functions.Is)
|
||||
|
||||
|
||||
local Object = require(Classes.Object)
|
||||
local Stateful = require(Mixins.Stateful)
|
||||
local Observable = require(Mixins.Observable)
|
||||
local Destroyable = require(Mixins.Destroyable)
|
||||
|
||||
|
||||
export type Observer<T> = Observable.Observable<T> & Destroyable.Destroyable
|
||||
export type ObserverTable<T> = Observable.ObservableTable<T> & Destroyable.Destroyable
|
||||
|
||||
export type ObserverFactory = (<T>(sourceObject: Stateful.Stateful<{T}>) -> ObserverTable<T>)
|
||||
& (<T>(sourceObject: Stateful.Stateful<T>) -> Observer<T>)
|
||||
|
||||
|
||||
--- Observer
|
||||
-- Creates an observer that reacts to changes in a stateful source.
|
||||
-- If the subject's value is a table, upon first creation of Observer, onKVChange callbacks will be supported.
|
||||
-- @param sourceObject The stateful object to observe.
|
||||
-- @return A new observer object.
|
||||
local function createObserver<T>(sourceObject: Stateful.Stateful<T>)
|
||||
if not Is.Stateful(sourceObject) then
|
||||
error("The first argument of an Observer must be a stateful object.", 2)
|
||||
end
|
||||
|
||||
local obj = Object.new()
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsEffect)
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.OnChangeCallbacks, LinkedList.new())
|
||||
|
||||
|
||||
if typeof(sourceObject:get()) == "table" then
|
||||
ECS.World:add(sourceObject.entity, ECS.Tags.IsDeepComparable)
|
||||
ECS.World:set(obj.entity, ECS.Components.OnKVChangeCallbacks, LinkedList.new())
|
||||
end
|
||||
|
||||
|
||||
ECS.World:add(obj.entity, ECS.JECS.pair(ECS.Tags.SubscribesTo, sourceObject.entity))
|
||||
ECS.World:add(sourceObject.entity, ECS.JECS.pair(ECS.Tags.HasSubscriber, obj.entity))
|
||||
|
||||
|
||||
obj:use(
|
||||
Destroyable,
|
||||
Observable
|
||||
)
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
|
||||
return obj
|
||||
end
|
||||
|
||||
return (createObserver :: any) :: ObserverFactory
|
53
src/Chemical/Factories/Reaction.lua
Normal file
53
src/Chemical/Factories/Reaction.lua
Normal file
|
@ -0,0 +1,53 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
|
||||
local LinkedList = require(RootFolder.Packages.LinkedList)
|
||||
|
||||
|
||||
local Classes = RootFolder.Classes
|
||||
local Mixins = RootFolder.Mixins
|
||||
local Functions = RootFolder.Functions
|
||||
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Is = require(Functions.Is)
|
||||
local SetInScope = require(Functions.SetInScope)
|
||||
|
||||
|
||||
local Object = require(Classes.Object)
|
||||
local Stateful = require(Mixins.Stateful)
|
||||
local Destroyable = require(Mixins.Destroyable)
|
||||
local Serializable = require(Mixins.Serializable)
|
||||
|
||||
export type Reaction<T> = Stateful.Stateful<T> & Destroyable.Destroyable & Serializable.Serializable & T
|
||||
|
||||
--- Reaction
|
||||
-- A Stateful container with helper methods for converting data into different formats.
|
||||
local function createReaction<T>(name: string, key: string, container: T): Reaction<T>
|
||||
|
||||
local obj = Object.new({
|
||||
__tostring = function(self)
|
||||
local isAlive = Is.Dead(self) and "Dead" or "Alive"
|
||||
return `Reaction<{name}/{key}> - {isAlive}`
|
||||
end,
|
||||
})
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
|
||||
|
||||
SetInScope(container :: any, obj.entity)
|
||||
|
||||
obj:use(
|
||||
Stateful,
|
||||
Destroyable,
|
||||
Serializable,
|
||||
(container :: any)
|
||||
)
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Value, container)
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
return obj :: Reaction<T>
|
||||
end
|
||||
|
||||
return createReaction
|
48
src/Chemical/Factories/Table.lua
Normal file
48
src/Chemical/Factories/Table.lua
Normal file
|
@ -0,0 +1,48 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Classes = RootFolder.Classes
|
||||
local Mixins = RootFolder.Mixins
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local Object = require(Classes.Object)
|
||||
|
||||
local Stateful = require(Mixins.Stateful)
|
||||
local StatefulTable = require(Mixins.StatefulTable)
|
||||
local StatefulDictionary = require(Mixins.StatefulDictionary)
|
||||
|
||||
local Settable = require(Mixins.Settable)
|
||||
local Numerical = require(Mixins.Numerical)
|
||||
local Destroyable = require(Mixins.Destroyable)
|
||||
|
||||
export type Value<T> = Stateful.Stateful<T> & Settable.Settable<T> & StatefulTable.StatefulTable<T> & Destroyable.Destroyable
|
||||
|
||||
return function<T>(value: T): Value<T>
|
||||
local obj = Object.new({
|
||||
__len = function(self)
|
||||
return #ECS.World:get(self.entity, ECS.Components.Value)
|
||||
end,
|
||||
|
||||
__tostring = function(self)
|
||||
return `Table<{ tostring(self.entity) }>`
|
||||
end,
|
||||
})
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStatefulTable)
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsSettable)
|
||||
|
||||
|
||||
obj:use(
|
||||
Stateful,
|
||||
Settable,
|
||||
StatefulTable,
|
||||
Destroyable
|
||||
)
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Value, value)
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
|
||||
return obj
|
||||
end
|
65
src/Chemical/Factories/Value.lua
Normal file
65
src/Chemical/Factories/Value.lua
Normal file
|
@ -0,0 +1,65 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Classes = RootFolder.Classes
|
||||
local Mixins = RootFolder.Mixins
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local Object = require(Classes.Object)
|
||||
|
||||
local Stateful = require(Mixins.Stateful)
|
||||
local StatefulTable = require(Mixins.StatefulTable)
|
||||
local StatefulDictionary = require(Mixins.StatefulDictionary)
|
||||
|
||||
local Settable = require(Mixins.Settable)
|
||||
local Numerical = require(Mixins.Numerical)
|
||||
local Destroyable = require(Mixins.Destroyable)
|
||||
|
||||
type Value<T> = Stateful.Stateful<T> & Settable.Settable<T> & Destroyable.Destroyable
|
||||
|
||||
export type ValueFactory = (
|
||||
((value: number) -> Value<number> & Numerical.Numerical) &
|
||||
(<T>(value: T) -> Value<T>)
|
||||
)
|
||||
|
||||
--- Value
|
||||
-- Stateful value container which enables reactivity.
|
||||
-- Depending on the type of the initial value, certain methods are exposed correlating to the value type.
|
||||
-- @param value any -- The initial value to set.
|
||||
-- @return The Value object.
|
||||
return function<T>(value: T): Value<T>
|
||||
local obj = Object.new({
|
||||
__len = typeof(value) == "table" and function(self)
|
||||
return #ECS.World:get(self.entity, ECS.Components.Value)
|
||||
end or nil,
|
||||
|
||||
__tostring = function(self)
|
||||
local rawValue = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
return `Value<{tostring(rawValue)}>`
|
||||
end,
|
||||
})
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsSettable)
|
||||
|
||||
|
||||
local mtMethods: { {} } = {
|
||||
Stateful,
|
||||
Settable,
|
||||
Destroyable,
|
||||
}
|
||||
|
||||
|
||||
if typeof(value) == "number" then
|
||||
table.insert(mtMethods, Numerical)
|
||||
end
|
||||
|
||||
|
||||
obj:use(table.unpack(mtMethods))
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Value, value)
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
|
||||
return obj
|
||||
end
|
47
src/Chemical/Factories/Watch.lua
Normal file
47
src/Chemical/Factories/Watch.lua
Normal file
|
@ -0,0 +1,47 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
local Observer = require(RootFolder.Factories.Observer)
|
||||
|
||||
local Stateful = require(RootFolder.Mixins.Stateful)
|
||||
local Computable = require(RootFolder.Mixins.Computable)
|
||||
|
||||
|
||||
export type WatchHandle = {
|
||||
destroy: (self: WatchHandle) -> ()
|
||||
}
|
||||
|
||||
|
||||
type Watchable<T> = Stateful.Stateful<T> | Computable.Computable<T>
|
||||
|
||||
--- Creates a watcher that runs a callback function whenever a reactive source changes.
|
||||
-- @param source The Value or Computed object to watch.
|
||||
-- @param watchCallback A function that will be called with (newValue, oldValue).
|
||||
-- @returns A handle with a :destroy() method to stop watching.
|
||||
return function<T>(source: Watchable<T>, watchCallback: (new: T, old: T) -> ()): WatchHandle
|
||||
if not source or not source.entity then
|
||||
error("Chemical.Watch requires a valid Value or Computed object as its first argument.", 2)
|
||||
end
|
||||
|
||||
if typeof(watchCallback) ~= "function" then
|
||||
error("Chemical.Watch requires a function as its second argument.", 2)
|
||||
end
|
||||
|
||||
local obs = Observer(source)
|
||||
|
||||
obs:onChange(function(newValue, oldValue)
|
||||
local success, err = pcall(watchCallback, newValue, oldValue)
|
||||
if not success then
|
||||
warn("Chemical Watch Error: ", err)
|
||||
end
|
||||
end)
|
||||
|
||||
local handle: WatchHandle = {
|
||||
destroy = function()
|
||||
obs:destroy()
|
||||
end,
|
||||
}
|
||||
|
||||
return handle
|
||||
end
|
3
src/Chemical/Factories/init.meta.json
Normal file
3
src/Chemical/Factories/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
8
src/Chemical/Functions/Alive.lua
Normal file
8
src/Chemical/Functions/Alive.lua
Normal file
|
@ -0,0 +1,8 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
return function(obj: Types.HasEntity): boolean
|
||||
return ECS.World:contains(obj.entity)
|
||||
end
|
80
src/Chemical/Functions/Array.lua
Normal file
80
src/Chemical/Functions/Array.lua
Normal file
|
@ -0,0 +1,80 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
|
||||
|
||||
|
||||
|
||||
local module = {}
|
||||
|
||||
function module.Transform<K, V, R>(tbl: { [K]: V }, doFn: (k: K, v: V) -> R): { [K]: R }
|
||||
local newTbl = {}
|
||||
for key, value in tbl do
|
||||
if Is.Array(value) then
|
||||
newTbl[key] = module.Transform(value, doFn)
|
||||
else
|
||||
newTbl[key] = doFn(key, value)
|
||||
end
|
||||
end
|
||||
return newTbl
|
||||
end
|
||||
|
||||
function module.ShallowTransform<K, V, R>(tbl: { [K]: V }, doFn: (k: K, v: V) -> R): { [K]: R }
|
||||
local newTbl = {}
|
||||
for key, value in tbl do
|
||||
newTbl[key] = doFn(key, value)
|
||||
end
|
||||
return newTbl
|
||||
end
|
||||
|
||||
|
||||
function module.Traverse(tbl: {}, doFn: (k: any, v: any) -> ())
|
||||
for key, value in tbl do
|
||||
if Is.Array(value) then
|
||||
module.Traverse(value, doFn)
|
||||
else
|
||||
doFn(key, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Recursively walks a table, calling a visitor function for every
|
||||
value encountered. The visitor receives the path (an array of keys)
|
||||
and the value at that path.
|
||||
|
||||
@param target The table to walk.
|
||||
@param visitor The function to call, with signature: (path: {any}, value: any) -> ()
|
||||
--]]
|
||||
function module.Walk(target: {any}, visitor: (path: {any}, value: any) -> ())
|
||||
local function _walk(currentValue: any, currentPath: {any})
|
||||
visitor(currentPath, currentValue)
|
||||
|
||||
if Is.Array(currentValue) then
|
||||
for key, childValue in pairs(currentValue) do
|
||||
local childPath = table.clone(currentPath)
|
||||
table.insert(childPath, key)
|
||||
_walk(childValue, childPath)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
_walk(target, {})
|
||||
end
|
||||
|
||||
|
||||
|
||||
function module.FindOnPath<K, V>(tbl: {[K]: V}, path: { number | string }): V
|
||||
local current = tbl
|
||||
for _, key in path do
|
||||
current = current[key]
|
||||
end
|
||||
|
||||
return current
|
||||
end
|
||||
|
||||
return module
|
78
src/Chemical/Functions/Blueprint.lua
Normal file
78
src/Chemical/Functions/Blueprint.lua
Normal file
|
@ -0,0 +1,78 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
local Value = require(RootFolder.Factories.Value)
|
||||
local Table = require(RootFolder.Factories.Table)
|
||||
local Map = require(RootFolder.Factories.Map)
|
||||
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
local Peek = require(RootFolder.Functions.Peek)
|
||||
local Array = require(RootFolder.Functions.Array)
|
||||
|
||||
type Blueprint = { T: ECS.Entity, V: any }
|
||||
|
||||
local module = {}
|
||||
|
||||
|
||||
local function blueprintTreeFromValue(value: any): Blueprint
|
||||
if Is.Stateful(value) then
|
||||
if Is.StatefulTable(value) then
|
||||
return { T = ECS.Tags.IsStatefulTable, V = Peek(value) }
|
||||
elseif Is.StatefulDictionary(value) then
|
||||
return { T = ECS.Tags.IsStatefulDictionary, V = Peek(value) }
|
||||
else
|
||||
return { T = ECS.Tags.IsStateful, V = Peek(value) }
|
||||
end
|
||||
elseif typeof(value) == "table" then
|
||||
local childrenAsBlueprints = Array.ShallowTransform(value, function(k, v)
|
||||
return blueprintTreeFromValue(v)
|
||||
end)
|
||||
return { T = ECS.Tags.IsStatic, V = childrenAsBlueprints }
|
||||
else
|
||||
return { T = ECS.Tags.IsStatic, V = value }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function module:From(value: any): Blueprint
|
||||
return blueprintTreeFromValue(value)
|
||||
end
|
||||
|
||||
|
||||
local buildFromBlueprintTree
|
||||
|
||||
|
||||
local function buildFromABlueprint(blueprint: Blueprint)
|
||||
if blueprint.T == ECS.Tags.IsStateful then
|
||||
return Value(blueprint.V)
|
||||
elseif blueprint.T == ECS.Tags.IsStatefulTable then
|
||||
return Table(blueprint.V)
|
||||
elseif blueprint.T == ECS.Tags.IsStatefulDictionary then
|
||||
return Map(blueprint.V)
|
||||
elseif blueprint.T == ECS.Tags.IsStatic then
|
||||
if typeof(blueprint.V) == "table" then
|
||||
return buildFromBlueprintTree(blueprint.V)
|
||||
else
|
||||
return blueprint.V
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
buildFromBlueprintTree = function(blueprintTable: {any})
|
||||
return Array.ShallowTransform(blueprintTable, function(key, value)
|
||||
return buildFromABlueprint(value)
|
||||
end)
|
||||
end
|
||||
|
||||
function module:Read(rootBlueprint: Blueprint)
|
||||
return buildFromABlueprint(rootBlueprint)
|
||||
end
|
||||
|
||||
|
||||
return module
|
57
src/Chemical/Functions/Compare.lua
Normal file
57
src/Chemical/Functions/Compare.lua
Normal file
|
@ -0,0 +1,57 @@
|
|||
export type Change = {
|
||||
Path: {string | number},
|
||||
OldValue: any,
|
||||
NewValue: any,
|
||||
}
|
||||
|
||||
local DeepCompare = {}
|
||||
|
||||
local function compare(oldTable, newTable, path, changes)
|
||||
path = path or {}
|
||||
changes = changes or {}
|
||||
|
||||
for key, newValue in pairs(newTable) do
|
||||
local oldValue = oldTable and oldTable[key]
|
||||
local currentPath = table.clone(path)
|
||||
table.insert(currentPath, key)
|
||||
|
||||
if oldValue ~= newValue then
|
||||
if typeof(newValue) == "table" and typeof(oldValue) == "table" then
|
||||
compare(oldValue, newValue, currentPath, changes)
|
||||
else
|
||||
table.insert(changes, {
|
||||
Path = currentPath,
|
||||
OldValue = oldValue,
|
||||
NewValue = newValue,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if oldTable then
|
||||
for key, oldValue in pairs(oldTable) do
|
||||
if newTable[key] == nil then
|
||||
local currentPath = table.clone(path)
|
||||
table.insert(currentPath, key)
|
||||
|
||||
table.insert(changes, {
|
||||
Path = currentPath,
|
||||
OldValue = oldValue,
|
||||
NewValue = nil,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return changes
|
||||
end
|
||||
|
||||
--- Compares two tables deeply and returns an array of changes.
|
||||
-- Each change object contains a `Path`, `OldValue`, and `NewValue`.
|
||||
return function(oldTable: {}, newTable: {}): {Change}
|
||||
if typeof(oldTable) ~= "table" or typeof(newTable) ~= "table" then
|
||||
return {}
|
||||
end
|
||||
|
||||
return compare(oldTable, newTable)
|
||||
end
|
147
src/Chemical/Functions/Compose.lua
Normal file
147
src/Chemical/Functions/Compose.lua
Normal file
|
@ -0,0 +1,147 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Symbols = require(RootFolder.Symbols)
|
||||
|
||||
local Effect = require(RootFolder.Factories.Effect)
|
||||
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
local Has = require(RootFolder.Functions.Has)
|
||||
local GetInScope = require(RootFolder.Functions.GetInScope)
|
||||
|
||||
local JECS = ECS.JECS
|
||||
local World = ECS.World
|
||||
local Components = ECS.Components
|
||||
local Tags = ECS.Tags
|
||||
|
||||
local RESERVED_KEYS = { "Children", "Parent" }
|
||||
|
||||
local INSTANCE_TO_ENTITY = setmetatable({}, { __mode = "k" })
|
||||
|
||||
local function applyProperty(instance, prop, value)
|
||||
instance[prop] = value
|
||||
end
|
||||
|
||||
local function bindEvent(instance: Instance, instanceEntity: ECS.Entity, event, callback)
|
||||
local connection = instance[event]:Connect(callback)
|
||||
local connectionList = World:get(instanceEntity, Components.ConnectionList)
|
||||
table.insert(connectionList, connection)
|
||||
World:set(instanceEntity, Components.ConnectionList, connectionList)
|
||||
end
|
||||
|
||||
local function bindChange(instance: Instance, instanceEntity: ECS.Entity, prop, action)
|
||||
local connection
|
||||
|
||||
if Is.Settable(action) then
|
||||
connection = instance:GetPropertyChangedSignal(prop):Connect(function(...: any) action:set(instance[prop]) end)
|
||||
else
|
||||
connection = instance:GetPropertyChangedSignal(prop):Connect(action)
|
||||
end
|
||||
|
||||
local connectionList = World:get(instanceEntity, Components.ConnectionList)
|
||||
table.insert(connectionList, connection)
|
||||
World:set(instanceEntity, Components.ConnectionList, connectionList)
|
||||
end
|
||||
|
||||
local function bindReactive(instance: Instance, instanceEntity: ECS.Entity, prop, value): Effect.Effect
|
||||
local propType = typeof(instance[prop])
|
||||
local propIsString = propType == "string"
|
||||
|
||||
local propEffect = Effect(function()
|
||||
local currentValue = value:get()
|
||||
|
||||
if propIsString and typeof(currentValue) ~= "string" then
|
||||
instance[prop] = tostring(currentValue)
|
||||
else
|
||||
instance[prop] = currentValue
|
||||
end
|
||||
end)
|
||||
|
||||
applyProperty(instance, prop, value:get())
|
||||
|
||||
World:add(propEffect.entity, JECS.pair(Tags.InScope, instanceEntity))
|
||||
end
|
||||
|
||||
local function applyVirtualNode(instance: Instance, instanceEntity: ECS.Entity, properties: {})
|
||||
for key, value in properties do
|
||||
if table.find(RESERVED_KEYS, key) then continue end
|
||||
|
||||
if Is.Symbol(key) then
|
||||
if Has.Symbol("Event", key) then
|
||||
if Is.Stateful(value) then error("Chemical OnEvent Error: Chemical does not currently support Stateful values.") end
|
||||
if typeof(value) ~= "function" then error("Chemical OnEvent Error: can only be bound to a callback", 2) end
|
||||
|
||||
|
||||
bindEvent(instance, instanceEntity, key.Symbol, value)
|
||||
elseif Has.Symbol("Change", key) then
|
||||
if typeof(value) ~= "function"
|
||||
and not Is.Settable(value) then error("Chemical OnChange Error: can only be bound to a callback or settable Stateful object.", 2) end
|
||||
|
||||
|
||||
bindChange(instance, instanceEntity, key.Symbol, value)
|
||||
elseif Has.Symbol("Children", key) then
|
||||
for _, child in value do
|
||||
child.Parent = instance
|
||||
end
|
||||
end
|
||||
elseif Is.Stateful(value) then
|
||||
bindReactive(instance, instanceEntity, key, value)
|
||||
elseif Is.Literal(value) then
|
||||
applyProperty(instance, key, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function Compose(target: string | Instance)
|
||||
return function(properties: {})
|
||||
local instance: Instance
|
||||
local instanceEntity: ECS.Entity
|
||||
|
||||
if typeof(target) == "string" then
|
||||
instance = Instance.new(target)
|
||||
instanceEntity = World:entity()
|
||||
|
||||
World:add(instanceEntity, Tags.IsHost)
|
||||
World:set(instanceEntity, Components.Instance, instance)
|
||||
World:set(instanceEntity, Components.ConnectionList, {})
|
||||
|
||||
World:set(instanceEntity, Components.Connection, instance.Destroying:Once(function()
|
||||
INSTANCE_TO_ENTITY[instance] = nil
|
||||
|
||||
if World:contains(instanceEntity) then
|
||||
World:delete(instanceEntity)
|
||||
end
|
||||
end))
|
||||
else
|
||||
instance = target
|
||||
instanceEntity = INSTANCE_TO_ENTITY[instance]
|
||||
|
||||
if not instanceEntity or not World:contains(instanceEntity) then
|
||||
instanceEntity = World:entity()
|
||||
INSTANCE_TO_ENTITY[instance] = instanceEntity
|
||||
|
||||
World:add(instanceEntity, Tags.IsHost)
|
||||
World:set(instanceEntity, Components.Instance, instance)
|
||||
World:set(instanceEntity, Components.ConnectionList, {})
|
||||
|
||||
World:set(instanceEntity, Components.Connection, instance.Destroying:Once(function()
|
||||
INSTANCE_TO_ENTITY[instance] = nil
|
||||
|
||||
if World:contains(instanceEntity) then
|
||||
World:delete(instanceEntity)
|
||||
end
|
||||
end))
|
||||
end
|
||||
end
|
||||
|
||||
applyVirtualNode(instance, instanceEntity, properties)
|
||||
|
||||
if properties.Parent and not instance.Parent then
|
||||
instance.Parent = properties.Parent
|
||||
end
|
||||
|
||||
return instance
|
||||
end
|
||||
end
|
||||
|
||||
return Compose
|
38
src/Chemical/Functions/Destroy.lua
Normal file
38
src/Chemical/Functions/Destroy.lua
Normal file
|
@ -0,0 +1,38 @@
|
|||
export type Destroyable = Computed | Value | Observer | { destroy: (self: {}) -> () } | Instance | RBXScriptConnection | { Destroyable } | () -> () | thread
|
||||
|
||||
local function Destroy(subject: Destroyable )
|
||||
if typeof(subject) == "table" then
|
||||
if subject.destroy then
|
||||
subject:destroy()
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if subject.Destroy then
|
||||
subject:Destroy()
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if getmetatable(subject) then
|
||||
setmetatable(subject, nil)
|
||||
table.clear(subject)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
for _, value in subject do
|
||||
Destroy(value)
|
||||
end
|
||||
elseif typeof(subject) == "Instance" then
|
||||
subject:Destroy()
|
||||
elseif typeof(subject) == "RBXScriptConnection" then
|
||||
subject:Disconnect()
|
||||
elseif typeof(subject) == "function" then
|
||||
subject()
|
||||
elseif typeof(subject) == "thread" then
|
||||
task.cancel(subject)
|
||||
end
|
||||
end
|
||||
|
||||
return Destroy
|
13
src/Chemical/Functions/GetInScope.lua
Normal file
13
src/Chemical/Functions/GetInScope.lua
Normal file
|
@ -0,0 +1,13 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local function getInScope(entity: ECS.Entity)
|
||||
local scoped = {}
|
||||
for scopedEntity in ECS.World:each(ECS.JECS.pair(ECS.Tags.InScope, entity)) do
|
||||
table.insert(scoped, scopedEntity)
|
||||
end
|
||||
return scoped
|
||||
end
|
||||
|
||||
return getInScope
|
20
src/Chemical/Functions/GetSubscribers.lua
Normal file
20
src/Chemical/Functions/GetSubscribers.lua
Normal file
|
@ -0,0 +1,20 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
|
||||
|
||||
local function getSubscribers(entity: ECS.Entity)
|
||||
local subscribers = {}
|
||||
|
||||
local subscriberQuery = ECS.World:query(ECS.Components.Object)
|
||||
:with(ECS.JECS.pair(ECS.Tags.SubscribesTo, entity))
|
||||
|
||||
for subscriberEntity, _ in subscriberQuery:iter() do
|
||||
table.insert(subscribers, subscriberEntity)
|
||||
end
|
||||
|
||||
return subscribers
|
||||
end
|
||||
|
||||
return getSubscribers
|
14
src/Chemical/Functions/GetSymbol.lua
Normal file
14
src/Chemical/Functions/GetSymbol.lua
Normal file
|
@ -0,0 +1,14 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local symbolMap = {}
|
||||
|
||||
export type Symbol<S, T> = { Symbol: S, Type: T }
|
||||
|
||||
return function<S, T>(symbolName: S, symbolType: T): Symbol<S, T>
|
||||
if symbolMap[symbolName] then return symbolMap[symbolName] end
|
||||
|
||||
local symbol = { Symbol = symbolName, Type = symbolType }
|
||||
symbolMap[symbolName] = symbol
|
||||
|
||||
return symbol
|
||||
end
|
12
src/Chemical/Functions/Has.lua
Normal file
12
src/Chemical/Functions/Has.lua
Normal file
|
@ -0,0 +1,12 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local module = {}
|
||||
|
||||
function module.Symbol(typeOf: string, obj: {}): boolean
|
||||
return obj.Type == typeOf
|
||||
end
|
||||
|
||||
|
||||
return module
|
83
src/Chemical/Functions/Is.lua
Normal file
83
src/Chemical/Functions/Is.lua
Normal file
|
@ -0,0 +1,83 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local module = {}
|
||||
|
||||
function module.Stateful(obj: any): boolean
|
||||
if typeof(obj) == "table" and obj.entity then
|
||||
return ECS.World:has(obj.entity, ECS.Tags.IsStateful)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function module.Settable(obj: any): boolean
|
||||
if typeof(obj) == "table" and obj.entity then
|
||||
return ECS.World:has(obj.entity, ECS.Tags.IsSettable)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function module.Primitive(obj: any): boolean
|
||||
local typeA = typeof(obj)
|
||||
return typeA ~= "table" and typeA ~= "userdata"
|
||||
end
|
||||
|
||||
function module.Literal(obj: any): boolean
|
||||
local typeA = typeof(obj)
|
||||
return typeA ~= "table" and typeA ~= "userdata" and typeA ~= "thread" and typeA ~= "function" and typeA ~= "Instance"
|
||||
end
|
||||
|
||||
function module.Symbol(obj: any, typeOf: string?): boolean
|
||||
local is = typeof(obj) == "table" and obj.Type and obj.Symbol
|
||||
return typeOf == nil and is or is and obj.Type == typeOf
|
||||
end
|
||||
|
||||
function module.Array(obj: any): boolean
|
||||
return typeof(obj) == "table" and obj.entity == nil
|
||||
end
|
||||
|
||||
function module.StatefulTable(obj: any): boolean
|
||||
if typeof(obj) == "table" and obj.entity then
|
||||
return ECS.World:has(obj.entity, ECS.Tags.IsStateful) and ECS.World:has(obj.entity, ECS.Tags.IsStatefulTable)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function module.StatefulDictionary(obj: any): boolean
|
||||
if typeof(obj) == "table" and obj.entity then
|
||||
return ECS.World:has(obj.entity, ECS.Tags.IsStateful) and ECS.World:has(obj.entity, ECS.Tags.IsStatefulDictionary)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function module.Blueprint(obj: any): boolean
|
||||
if typeof(obj) == "table" and obj.T and obj.V then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function module.Dead(obj: any)
|
||||
return typeof(obj) == "table" and obj.__destroyed
|
||||
end
|
||||
|
||||
function module.Destroyed(obj: Instance): boolean
|
||||
if obj.Parent == nil then
|
||||
local Success, Error = pcall(function()
|
||||
obj.Parent = UserSettings() :: any
|
||||
end)
|
||||
|
||||
return Error ~= "Not allowed to add that under settings"
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
return module
|
15
src/Chemical/Functions/Peek.lua
Normal file
15
src/Chemical/Functions/Peek.lua
Normal file
|
@ -0,0 +1,15 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
|
||||
--- Peek
|
||||
-- View a stateful's value without triggering and scoped dependencies/subscriptions.
|
||||
return function(obj: any): any?
|
||||
if Is.Stateful(obj) then
|
||||
return ECS.World:get(obj.entity, ECS.Components.Value)
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
21
src/Chemical/Functions/SetInScope.lua
Normal file
21
src/Chemical/Functions/SetInScope.lua
Normal file
|
@ -0,0 +1,21 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Array = require(RootFolder.Functions.Array)
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
|
||||
local function setInScope(scopable: {ECS.Entity} | ECS.Entity, entity: ECS.Entity)
|
||||
if Is.Stateful(scopable) then
|
||||
ECS.World:add(scopable.entity, ECS.JECS.pair(ECS.Tags.InScope, entity))
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
Array.Traverse(scopable, function(k, v)
|
||||
if Is.Stateful(v) then
|
||||
ECS.World:add(v.entity, ECS.JECS.pair(ECS.Tags.InScope, entity))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return setInScope
|
3
src/Chemical/Functions/init.meta.json
Normal file
3
src/Chemical/Functions/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
19
src/Chemical/Mixins/Cleanable.lua
Normal file
19
src/Chemical/Mixins/Cleanable.lua
Normal file
|
@ -0,0 +1,19 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
export type Cleanable = Types.HasEntity & {
|
||||
clean: (self: Cleanable) -> (),
|
||||
}
|
||||
|
||||
return {
|
||||
clean = function(self: Cleanable)
|
||||
local cleanupFn = ECS.World:get(self.entity, ECS.Components.CleanupFn)
|
||||
if cleanupFn then
|
||||
cleanupFn()
|
||||
end
|
||||
end,
|
||||
}
|
53
src/Chemical/Mixins/Computable.lua
Normal file
53
src/Chemical/Mixins/Computable.lua
Normal file
|
@ -0,0 +1,53 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
local GetSubscribers = require(RootFolder.Functions.GetSubscribers)
|
||||
|
||||
export type Computable<T = any> = Types.HasEntity & {
|
||||
compute: (self: Computable<T>) -> (),
|
||||
}
|
||||
|
||||
type MaybeCleanable = {
|
||||
clean: (self: MaybeCleanable) -> ()
|
||||
}
|
||||
|
||||
return {
|
||||
compute = function(self: Computable & MaybeCleanable)
|
||||
local computeFn = ECS.World:get(self.entity, ECS.Components.ComputeFn)
|
||||
if not computeFn then return end
|
||||
|
||||
|
||||
local oldValue = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local cleanupFn = ECS.World:get(self.entity, ECS.Components.CleanupFn)
|
||||
|
||||
if oldValue and cleanupFn then
|
||||
cleanupFn(oldValue)
|
||||
end
|
||||
|
||||
Cache.Stack:Push(self.entity)
|
||||
local s, result = pcall(computeFn)
|
||||
Cache.Stack:Pop()
|
||||
|
||||
if not s then
|
||||
warn("Chemical Computed Error: ", result)
|
||||
return
|
||||
end
|
||||
|
||||
if result ~= oldValue then
|
||||
ECS.World:set(self.entity, ECS.Components.PrevValue, oldValue)
|
||||
ECS.World:set(self.entity, ECS.Components.Value, result)
|
||||
|
||||
local subscribers = GetSubscribers(self.entity)
|
||||
for _, subscriberEntity in ipairs(subscribers) do
|
||||
if not ECS.World:has(subscriberEntity, ECS.Tags.IsDirty) then
|
||||
ECS.World:add(subscriberEntity, ECS.Tags.IsDirty)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
37
src/Chemical/Mixins/Destroyable.lua
Normal file
37
src/Chemical/Mixins/Destroyable.lua
Normal file
|
@ -0,0 +1,37 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Types = require(RootFolder.Types)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
export type Destroyable = Types.HasEntity & {
|
||||
__destroyed: boolean,
|
||||
|
||||
destroy: (self: Destroyable) -> (),
|
||||
--__internalDestroy: (self: Destroyable) -> (),
|
||||
}
|
||||
|
||||
local methods = {}
|
||||
|
||||
function methods:__internalDestroy()
|
||||
if self.__destroyed then return end
|
||||
self.__destroyed = true
|
||||
|
||||
local cleanupFn = ECS.World:get(self.entity, ECS.Components.CleanupFn)
|
||||
if cleanupFn then
|
||||
cleanupFn()
|
||||
|
||||
ECS.World:remove(self.entity, ECS.Components.CleanupFn)
|
||||
end
|
||||
end
|
||||
|
||||
function methods:destroy()
|
||||
if self.__destroyed then return end
|
||||
|
||||
self:__internalDestroy()
|
||||
|
||||
ECS.World:delete(self.entity)
|
||||
end
|
||||
|
||||
return methods
|
33
src/Chemical/Mixins/Effectable.lua
Normal file
33
src/Chemical/Mixins/Effectable.lua
Normal file
|
@ -0,0 +1,33 @@
|
|||
--!strict
|
||||
local RootFolder = script.Parent.Parent
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
export type Effectable = Types.HasEntity & {
|
||||
run: (self: Effectable) -> (),
|
||||
}
|
||||
|
||||
return {
|
||||
run = function(self: Effectable)
|
||||
local effectFn = ECS.World:get(self.entity, ECS.Components.EffectFn)
|
||||
if not effectFn then return end
|
||||
|
||||
|
||||
local oldCleanupFn = ECS.World:get(self.entity, ECS.Components.CleanupFn)
|
||||
if oldCleanupFn then
|
||||
oldCleanupFn()
|
||||
|
||||
|
||||
ECS.World:remove(self.entity, ECS.Components.CleanupFn)
|
||||
end
|
||||
|
||||
Cache.Stack:Push(self.entity)
|
||||
local newCleanupFn = effectFn()
|
||||
Cache.Stack:Pop()
|
||||
|
||||
if newCleanupFn then
|
||||
ECS.World:set(self.entity, ECS.Components.CleanupFn, newCleanupFn)
|
||||
end
|
||||
end,
|
||||
}
|
28
src/Chemical/Mixins/Numerical.lua
Normal file
28
src/Chemical/Mixins/Numerical.lua
Normal file
|
@ -0,0 +1,28 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
export type Numerical = Types.HasEntity & {
|
||||
increment: (self: Numerical, n: number) -> (),
|
||||
decrement: (self: Numerical, n: number) -> ()
|
||||
}
|
||||
|
||||
return {
|
||||
increment = function(self: Numerical, n: number)
|
||||
local cachedValue = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.PrevValue, cachedValue)
|
||||
ECS.World:set(self.entity, ECS.Components.Value, cachedValue + n)
|
||||
end,
|
||||
|
||||
decrement = function(self: Numerical, n: number)
|
||||
local cachedValue = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.PrevValue, cachedValue)
|
||||
ECS.World:set(self.entity, ECS.Components.Value, cachedValue - n)
|
||||
end,
|
||||
}
|
103
src/Chemical/Mixins/Observable.lua
Normal file
103
src/Chemical/Mixins/Observable.lua
Normal file
|
@ -0,0 +1,103 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local LinkedList = require(RootFolder.Packages.LinkedList)
|
||||
|
||||
local Types = require(RootFolder.Types)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local DeepCompare = require(RootFolder.Functions.Compare)
|
||||
|
||||
|
||||
export type Observable<T = any> = Types.HasEntity & {
|
||||
__destroyed: boolean,
|
||||
onChange: (self: Observable<T>, callback: (new: T, old: T) -> ()) -> { disconnect: () -> () },
|
||||
run: (self: Observable<T>) -> (),
|
||||
destroy: (self: Observable<T>) -> (),
|
||||
}
|
||||
|
||||
export type ObservableTable<T = {}> = Observable<T> & {
|
||||
onKVChange: (self: ObservableTable<T>, callback: (path: {string|number}, new: any, old: any) -> ()) -> { disconnect: () -> () },
|
||||
}
|
||||
|
||||
return {
|
||||
onChange = function(self: Observable, callback: (any, any) -> ())
|
||||
local callbackList = ECS.World:get(self.entity, ECS.Components.OnChangeCallbacks)
|
||||
callbackList:InsertBack(callback)
|
||||
|
||||
return {
|
||||
disconnect = function()
|
||||
callbackList:Remove(callback)
|
||||
end,
|
||||
}
|
||||
end,
|
||||
|
||||
onKVChange = function(self: Observable, callback: (path: {any}, any, any) -> ())
|
||||
local kvCallbackList = ECS.World:get(self.entity, ECS.Components.OnKVChangeCallbacks)
|
||||
kvCallbackList:InsertBack(callback)
|
||||
|
||||
return {
|
||||
disconnect = function()
|
||||
kvCallbackList:Remove(callback)
|
||||
end,
|
||||
}
|
||||
end,
|
||||
|
||||
run = function(self: Observable)
|
||||
local sourceEntity = ECS.World:target(self.entity, ECS.Tags.SubscribesTo)
|
||||
if not sourceEntity then return end
|
||||
|
||||
local newValue = ECS.World:get(sourceEntity, ECS.Components.Value)
|
||||
local oldValue = ECS.World:get(sourceEntity, ECS.Components.PrevValue)
|
||||
|
||||
|
||||
local callbacksList = ECS.World:get(self.entity, ECS.Components.OnChangeCallbacks)
|
||||
if callbacksList then
|
||||
for link, callback in callbacksList:IterateForward() do
|
||||
local s, err = pcall(callback, newValue, oldValue)
|
||||
if not s then warn("Chemical Observer Error: onChange: ", err) end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if ECS.World:has(sourceEntity, ECS.Tags.IsDeepComparable) then
|
||||
local kvCallbackList = ECS.World:get(self.entity, ECS.Components.OnKVChangeCallbacks)
|
||||
if kvCallbackList then
|
||||
local changes = DeepCompare(oldValue, newValue)
|
||||
for _, change in ipairs(changes) do
|
||||
for link, callback in kvCallbackList:IterateForward() do
|
||||
local s, err = pcall(callback, change.Path, change.NewValue, change.OldValue)
|
||||
if not s then warn("Chemical Observer Error: onKVChange: ", err) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
|
||||
__internalDestroy = function(self: Observable & Types.MaybeCleanable)
|
||||
if self.__destroyed then return end
|
||||
self.__destroyed = true
|
||||
|
||||
|
||||
local callbacksList = ECS.World:get(self.entity, ECS.Components.OnChangeCallbacks)
|
||||
if callbacksList then callbacksList:Destroy() end
|
||||
|
||||
local kvCallbackList = ECS.World:get(self.entity, ECS.Components.OnKVChangeCallbacks)
|
||||
if kvCallbackList then kvCallbackList:Destroy() end
|
||||
|
||||
|
||||
if self.clean then self:clean() end
|
||||
|
||||
|
||||
setmetatable(self, nil)
|
||||
end,
|
||||
|
||||
destroy = function(self: Observable & Types.MaybeCleanable & Types.MaybeDestroyable)
|
||||
if self.__destroyed then return end
|
||||
|
||||
self:__internalDestroy()
|
||||
|
||||
ECS.World:delete(self.entity)
|
||||
end,
|
||||
}
|
73
src/Chemical/Mixins/Serializable.lua
Normal file
73
src/Chemical/Mixins/Serializable.lua
Normal file
|
@ -0,0 +1,73 @@
|
|||
--!strict
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
local Peek = require(RootFolder.Functions.Peek)
|
||||
local Array = require(RootFolder.Functions.Array)
|
||||
local Blueprint = require(RootFolder.Functions.Blueprint)
|
||||
|
||||
export type Serializable = Types.HasEntity & {
|
||||
serialize: (self: Serializable) -> (any),
|
||||
snapshot: (self: Serializable) -> (any),
|
||||
blueprint: (self: Serializable) -> (any | { any }),
|
||||
}
|
||||
|
||||
return {
|
||||
serialize = function(self: Serializable): any
|
||||
local value = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local serialized
|
||||
|
||||
if Is.Stateful(value) then
|
||||
local theValue = Peek(value)
|
||||
serialized = Is.Primitive(theValue) and theValue
|
||||
|
||||
elseif Is.Array(value) then
|
||||
serialized = Array.Transform(value, function(k, v)
|
||||
if Is.Stateful(v) then
|
||||
local theValue = Peek(v)
|
||||
return Is.Primitive(theValue) and theValue or nil
|
||||
elseif Is.Primitive(v) then
|
||||
return v
|
||||
end
|
||||
|
||||
return nil
|
||||
end)
|
||||
|
||||
elseif Is.Primitive(value) then
|
||||
serialized = value
|
||||
|
||||
end
|
||||
|
||||
return serialized, if not serialized then warn("There was nothing to serialize, or the value was unserializable.") else nil
|
||||
end,
|
||||
|
||||
snapshot = function(self: Serializable): any
|
||||
local value = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
|
||||
if Is.Stateful(value) then
|
||||
return Peek(value)
|
||||
elseif Is.Array(value) then
|
||||
return Array.Transform(value, function(k, v)
|
||||
if Is.Stateful(v) then
|
||||
local theValue = Peek(v)
|
||||
return Peek(v)
|
||||
elseif Is.Primitive(v) then
|
||||
return v
|
||||
end
|
||||
|
||||
return nil
|
||||
end)
|
||||
else
|
||||
return value
|
||||
end
|
||||
end,
|
||||
|
||||
blueprint = function(self: Serializable): any | { any }
|
||||
local value = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
return Blueprint:From(value)
|
||||
end,
|
||||
}
|
24
src/Chemical/Mixins/Settable.lua
Normal file
24
src/Chemical/Mixins/Settable.lua
Normal file
|
@ -0,0 +1,24 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
export type Settable<T = any> = Types.HasEntity & {
|
||||
set: (self: Settable<T>, T) -> ()
|
||||
}
|
||||
|
||||
return {
|
||||
set = function(self: Settable, value: any)
|
||||
local cachedValue = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
|
||||
if value == cachedValue then
|
||||
return
|
||||
end
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.PrevValue, cachedValue)
|
||||
ECS.World:set(self.entity, ECS.Components.Value, value)
|
||||
end,
|
||||
}
|
23
src/Chemical/Mixins/Stateful.lua
Normal file
23
src/Chemical/Mixins/Stateful.lua
Normal file
|
@ -0,0 +1,23 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
export type Stateful<T = any> = Types.HasEntity & {
|
||||
get: (self: Stateful<T>) -> (T)
|
||||
}
|
||||
|
||||
return {
|
||||
get = function(self: Stateful)
|
||||
local withinEntity = Cache.Stack:Top()
|
||||
if withinEntity then
|
||||
ECS.World:add(withinEntity, ECS.JECS.pair(ECS.Tags.SubscribesTo, self.entity))
|
||||
ECS.World:add(self.entity, ECS.JECS.pair(ECS.Tags.HasSubscriber, withinEntity))
|
||||
end
|
||||
|
||||
return ECS.World:get(self.entity, ECS.Components.Value)
|
||||
end,
|
||||
}
|
52
src/Chemical/Mixins/StatefulDictionary.lua
Normal file
52
src/Chemical/Mixins/StatefulDictionary.lua
Normal file
|
@ -0,0 +1,52 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
local Destroy = require(RootFolder.Functions:FindFirstChild("Destroy"))
|
||||
|
||||
export type StatefulDictionary<T = {}> = Types.HasEntity & {
|
||||
key: <K, V>(self: StatefulDictionary<T>, key: K, value: V?) -> (),
|
||||
clear: <V>(self: StatefulDictionary<T>, cleanup: (value: V) -> ()?) -> (any?),
|
||||
}
|
||||
|
||||
local function recursive(tbl, func)
|
||||
for key, value in tbl do
|
||||
if typeof(value) == "table" and not value.type then
|
||||
recursive(value, func)
|
||||
|
||||
continue
|
||||
end
|
||||
|
||||
func(value)
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
key = function<T, V>(self: StatefulDictionary, key: T, value: V?)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local newTbl = table.clone(tbl)
|
||||
|
||||
newTbl[key] = value
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
|
||||
end,
|
||||
|
||||
clear = function(self: StatefulDictionary, cleanup: (any) -> ())
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local newTbl = table.clone(tbl)
|
||||
|
||||
if cleanup then
|
||||
recursive(newTbl, cleanup)
|
||||
end
|
||||
|
||||
newTbl = {}
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
|
||||
|
||||
end,
|
||||
|
||||
}
|
71
src/Chemical/Mixins/StatefulTable.lua
Normal file
71
src/Chemical/Mixins/StatefulTable.lua
Normal file
|
@ -0,0 +1,71 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
export type StatefulTable<T = {}> = Types.HasEntity & {
|
||||
insert: <V>(self: StatefulTable<T>, value: V) -> (),
|
||||
remove: <V>(self: StatefulTable<T>, value: V) -> (),
|
||||
find: <V>(self: StatefulTable<T>, value: V) -> (number)?,
|
||||
|
||||
setAt: <V>(self: StatefulTable<T>, index: number, value: V) -> (),
|
||||
getAt: (self: StatefulTable<T>, index: number) -> (any?),
|
||||
|
||||
clear: (self: StatefulTable<T>) -> (),
|
||||
}
|
||||
|
||||
return {
|
||||
insert = function<T>(self: StatefulTable, value: T)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local newTbl = table.clone(tbl)
|
||||
|
||||
table.insert(newTbl, value)
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
|
||||
end,
|
||||
|
||||
remove = function<T>(self: StatefulTable, value: T)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local newTbl = table.clone(tbl)
|
||||
|
||||
local index = table.find(newTbl, value)
|
||||
local poppedValue = table.remove(newTbl, index)
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
|
||||
|
||||
return poppedValue
|
||||
end,
|
||||
|
||||
find = function<T>(self: StatefulTable, value: T)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local found = table.find(tbl, value)
|
||||
|
||||
return found
|
||||
end,
|
||||
|
||||
setAt = function<T>(self: StatefulTable, index: number, value: T)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local newTbl = table.clone(tbl)
|
||||
|
||||
newTbl[index] = value
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
|
||||
end,
|
||||
|
||||
getAt = function(self: StatefulTable, index: number)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
return tbl[index]
|
||||
end,
|
||||
|
||||
clear = function(self: StatefulTable)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local newTbl = table.clone(tbl)
|
||||
|
||||
table.clear(newTbl)
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
|
||||
end,
|
||||
}
|
3
src/Chemical/Mixins/init.meta.json
Normal file
3
src/Chemical/Mixins/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
52
src/Chemical/Packages/Datastore/Proxy.lua
Normal file
52
src/Chemical/Packages/Datastore/Proxy.lua
Normal file
|
@ -0,0 +1,52 @@
|
|||
-- Variables
|
||||
local Constructor = {}
|
||||
local Index, NewIndex
|
||||
|
||||
|
||||
|
||||
|
||||
-- Types
|
||||
export type Constructor = {
|
||||
new: (data: {[any]: any}, public: {[any]: any}?) -> ({}, {[any]: any}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Constructor
|
||||
Constructor.new = function(data, public)
|
||||
local proxy = newproxy(true)
|
||||
local metatable = getmetatable(proxy)
|
||||
for index, value in data do metatable[index] = value end
|
||||
metatable.__index = Index
|
||||
metatable.__newindex = NewIndex
|
||||
metatable.__public = public or {}
|
||||
return proxy, metatable
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-- Functions
|
||||
Index = function(proxy, index)
|
||||
local metatable = getmetatable(proxy)
|
||||
local public = metatable.__public[index]
|
||||
return if public == nil then metatable.__shared[index] else public
|
||||
end
|
||||
|
||||
NewIndex = function(proxy, index, value)
|
||||
local metatable = getmetatable(proxy)
|
||||
local set = metatable.__set[index]
|
||||
if set == nil then
|
||||
metatable.__public[index] = value
|
||||
elseif set == false then
|
||||
error("Attempt to modify a readonly value", 2)
|
||||
else
|
||||
set(proxy, metatable, value)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
return table.freeze(Constructor) :: Constructor
|
219
src/Chemical/Packages/Datastore/Signal.lua
Normal file
219
src/Chemical/Packages/Datastore/Signal.lua
Normal file
|
@ -0,0 +1,219 @@
|
|||
-- Variables
|
||||
local Proxy = require(script.Parent.Proxy)
|
||||
local Constructor, Signal, Connection = {}, {}, {}
|
||||
local Thread, Call
|
||||
local threads = {}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Types
|
||||
export type Constructor = {
|
||||
new: () -> Signal,
|
||||
}
|
||||
|
||||
export type Signal = {
|
||||
[any]: any,
|
||||
Connections: number,
|
||||
Connected: (connected: boolean, signal: Signal) -> ()?,
|
||||
Connect: (self: Signal, func: (...any) -> (), ...any) -> Connection,
|
||||
Once: (self: Signal, func: (...any) -> (), ...any) -> Connection,
|
||||
Wait: (self: Signal, ...any) -> ...any,
|
||||
Fire: (self: Signal, ...any) -> (),
|
||||
FastFire: (self: Signal, ...any) -> (),
|
||||
DisconnectAll: (self: Signal) -> (),
|
||||
}
|
||||
|
||||
export type Connection = {
|
||||
[any]: any,
|
||||
Signal: Signal?,
|
||||
Disconnect: (self: Connection) -> (),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Constructor
|
||||
Constructor.new = function()
|
||||
local proxy, signal = Proxy.new(Signal, {Connections = 0})
|
||||
return proxy
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-- Signal
|
||||
Signal.__tostring = function(proxy)
|
||||
return "Signal"
|
||||
end
|
||||
|
||||
Signal.__shared = {
|
||||
Connect = function(proxy, func, ...)
|
||||
local signal = getmetatable(proxy)
|
||||
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to Connect failed: Passed value is not a Signal", 3) end
|
||||
if type(func) ~= "function" then error("Attempt to Connect failed: Passed value is not a function", 3) end
|
||||
signal.__public.Connections += 1
|
||||
local connectionProxy, connection = Proxy.new(Connection, {Signal = proxy})
|
||||
connection.FunctionOrThread = func
|
||||
connection.Parameters = if ... == nil then nil else {...}
|
||||
if signal.Last == nil then signal.First, signal.Last = connection, connection else connection.Previous, signal.Last.Next, signal.Last = signal.Last, connection, connection end
|
||||
if signal.__public.Connections == 1 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, true, proxy) end
|
||||
return connectionProxy
|
||||
end,
|
||||
Once = function(proxy, func, ...)
|
||||
local signal = getmetatable(proxy)
|
||||
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to Connect failed: Passed value is not a Signal", 3) end
|
||||
if type(func) ~= "function" then error("Attempt to Connect failed: Passed value is not a function", 3) end
|
||||
signal.__public.Connections += 1
|
||||
local connectionProxy, connection = Proxy.new(Connection, {Signal = proxy})
|
||||
connection.FunctionOrThread = func
|
||||
connection.Once = true
|
||||
connection.Parameters = if ... == nil then nil else {...}
|
||||
if signal.Last == nil then signal.First, signal.Last = connection, connection else connection.Previous, signal.Last.Next, signal.Last = signal.Last, connection, connection end
|
||||
if signal.__public.Connections == 1 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, true, proxy) end
|
||||
return connectionProxy
|
||||
end,
|
||||
Wait = function(proxy, ...)
|
||||
local signal = getmetatable(proxy)
|
||||
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to Connect failed: Passed value is not a Signal", 3) end
|
||||
signal.__public.Connections += 1
|
||||
local connectionProxy, connection = Proxy.new(Connection, {Signal = proxy})
|
||||
connection.FunctionOrThread = coroutine.running()
|
||||
connection.Once = true
|
||||
connection.Parameters = if ... == nil then nil else {...}
|
||||
if signal.Last == nil then signal.First, signal.Last = connection, connection else connection.Previous, signal.Last.Next, signal.Last = signal.Last, connection, connection end
|
||||
if signal.__public.Connections == 1 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, true, proxy) end
|
||||
return coroutine.yield()
|
||||
end,
|
||||
Fire = function(proxy, ...)
|
||||
local signal = getmetatable(proxy)
|
||||
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to connect failed: Passed value is not a Signal", 3) end
|
||||
local connection = signal.First
|
||||
while connection ~= nil do
|
||||
if connection.Once == true then
|
||||
signal.__public.Connections -= 1
|
||||
connection.__public.Signal = nil
|
||||
if signal.First == connection then signal.First = connection.Next end
|
||||
if signal.Last == connection then signal.Last = connection.Previous end
|
||||
if connection.Previous ~= nil then connection.Previous.Next = connection.Next end
|
||||
if connection.Next ~= nil then connection.Next.Previous = connection.Previous end
|
||||
if signal.__public.Connections == 0 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, false, proxy) end
|
||||
end
|
||||
if type(connection.FunctionOrThread) == "thread" then
|
||||
if connection.Parameters == nil then
|
||||
task.spawn(connection.FunctionOrThread, ...)
|
||||
else
|
||||
local parameters = {...}
|
||||
task.spawn(connection.FunctionOrThread, table.unpack(table.move(connection.Parameters, 1, #connection.Parameters, #parameters + 1, parameters)))
|
||||
end
|
||||
else
|
||||
local thread = table.remove(threads)
|
||||
if thread == nil then thread = coroutine.create(Thread) coroutine.resume(thread) end
|
||||
if connection.Parameters == nil then
|
||||
task.spawn(thread, thread, connection.FunctionOrThread, ...)
|
||||
else
|
||||
local parameters = {...}
|
||||
task.spawn(thread, thread, connection.FunctionOrThread, table.unpack(table.move(connection.Parameters, 1, #connection.Parameters, #parameters + 1, parameters)))
|
||||
end
|
||||
end
|
||||
connection = connection.Next
|
||||
end
|
||||
end,
|
||||
FastFire = function(proxy, ...)
|
||||
local signal = getmetatable(proxy)
|
||||
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to connect failed: Passed value is not a Signal", 3) end
|
||||
local connection = signal.First
|
||||
while connection ~= nil do
|
||||
if connection.Once == true then
|
||||
signal.__public.Connections -= 1
|
||||
connection.__public.Signal = nil
|
||||
if signal.First == connection then signal.First = connection.Next end
|
||||
if signal.Last == connection then signal.Last = connection.Previous end
|
||||
if connection.Previous ~= nil then connection.Previous.Next = connection.Next end
|
||||
if connection.Next ~= nil then connection.Next.Previous = connection.Previous end
|
||||
if signal.__public.Connections == 0 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, false, proxy) end
|
||||
end
|
||||
if type(connection.FunctionOrThread) == "thread" then
|
||||
if connection.Parameters == nil then
|
||||
coroutine.resume(connection.FunctionOrThread, ...)
|
||||
else
|
||||
local parameters = {...}
|
||||
coroutine.resume(connection.FunctionOrThread, table.unpack(table.move(connection.Parameters, 1, #connection.Parameters, #parameters + 1, parameters)))
|
||||
end
|
||||
else
|
||||
if connection.Parameters == nil then
|
||||
connection.FunctionOrThread(...)
|
||||
else
|
||||
local parameters = {...}
|
||||
connection.FunctionOrThread(table.unpack(table.move(connection.Parameters, 1, #connection.Parameters, #parameters + 1, parameters)))
|
||||
end
|
||||
end
|
||||
connection = connection.Next
|
||||
end
|
||||
end,
|
||||
DisconnectAll = function(proxy)
|
||||
local signal = getmetatable(proxy)
|
||||
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to Connect failed: Passed value is not a Signal", 3) end
|
||||
local connection = signal.First
|
||||
if connection == nil then return end
|
||||
while connection ~= nil do
|
||||
connection.__public.Signal = nil
|
||||
if type(connection.FunctionOrThread) == "thread" then task.cancel(connection.FunctionOrThread) end
|
||||
connection = connection.Next
|
||||
end
|
||||
if signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, false, proxy) end
|
||||
signal.__public.Connections, signal.First, signal.Last = 0, nil, nil
|
||||
end,
|
||||
}
|
||||
|
||||
Signal.__set = {
|
||||
Connections = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Connection
|
||||
Connection.__tostring = function(proxy)
|
||||
return "Connection"
|
||||
end
|
||||
|
||||
Connection.__shared = {
|
||||
Disconnect = function(proxy)
|
||||
local connection = getmetatable(proxy)
|
||||
if type(connection) ~= "table" or connection.__shared ~= Connection.__shared then error("Attempt to Disconnect failed: Passed value is not a Connection", 3) end
|
||||
local signal = getmetatable(connection.__public.Signal)
|
||||
if signal == nil then return end
|
||||
signal.__public.Connections -= 1
|
||||
connection.__public.Signal = nil
|
||||
if signal.First == connection then signal.First = connection.Next end
|
||||
if signal.Last == connection then signal.Last = connection.Previous end
|
||||
if connection.Previous ~= nil then connection.Previous.Next = connection.Next end
|
||||
if connection.Next ~= nil then connection.Next.Previous = connection.Previous end
|
||||
if type(connection.FunctionOrThread) == "thread" then task.cancel(connection.FunctionOrThread) end
|
||||
if signal.__public.Connections == 0 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, false, proxy) end
|
||||
end,
|
||||
}
|
||||
|
||||
Connection.__set = {
|
||||
Signal = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Functions
|
||||
Thread = function()
|
||||
while true do Call(coroutine.yield()) end
|
||||
end
|
||||
|
||||
Call = function(thread, func, ...)
|
||||
func(...)
|
||||
if #threads >= 16 then return end
|
||||
table.insert(threads, thread)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
return table.freeze(Constructor) :: Constructor
|
266
src/Chemical/Packages/Datastore/SynchronousTaskManager.lua
Normal file
266
src/Chemical/Packages/Datastore/SynchronousTaskManager.lua
Normal file
|
@ -0,0 +1,266 @@
|
|||
-- Variables
|
||||
local Proxy = require(script.Parent.Proxy)
|
||||
local Constructor, TaskManager, SynchronousTask, RunningTask = {}, {}, {}, {}
|
||||
local Run
|
||||
|
||||
|
||||
|
||||
|
||||
-- Types
|
||||
export type Constructor = {
|
||||
new: () -> TaskManager,
|
||||
}
|
||||
|
||||
export type TaskManager = {
|
||||
[any]: any,
|
||||
Enabled: boolean,
|
||||
Tasks: number,
|
||||
Running: SynchronousTask?,
|
||||
InsertFront: (self: TaskManager, func: (RunningTask, ...any) -> (), ...any) -> SynchronousTask,
|
||||
InsertBack: (self: TaskManager, func: (RunningTask, ...any) -> (), ...any) -> SynchronousTask,
|
||||
FindFirst: (self: TaskManager, func: (RunningTask, ...any) -> ()) -> (SynchronousTask?, number?),
|
||||
FindLast: (self: TaskManager, func: (RunningTask, ...any) -> ()) -> (SynchronousTask?, number?),
|
||||
CancelAll: (self: TaskManager, func: (RunningTask, ...any) -> ()?) -> (),
|
||||
}
|
||||
|
||||
export type SynchronousTask = {
|
||||
[any]: any,
|
||||
TaskManager: TaskManager?,
|
||||
Running: boolean,
|
||||
Wait: (self: SynchronousTask, ...any) -> ...any,
|
||||
Cancel: (self: SynchronousTask) -> (),
|
||||
}
|
||||
|
||||
export type RunningTask = {
|
||||
Next: (self: RunningTask) -> (thread, ...any),
|
||||
Iterate: (self: RunningTask) -> ((self: RunningTask) -> (thread, ...any), RunningTask),
|
||||
End: (self: RunningTask) -> (),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Constructor
|
||||
Constructor.new = function()
|
||||
local proxy, taskManager = Proxy.new(TaskManager, {Enabled = true, Tasks = 0})
|
||||
taskManager.Active = false
|
||||
return proxy
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-- TaskManager
|
||||
TaskManager.__tostring = function(proxy)
|
||||
return "Task Manager"
|
||||
end
|
||||
|
||||
TaskManager.__shared = {
|
||||
InsertFront = function(proxy, func, ...)
|
||||
local taskManager = getmetatable(proxy)
|
||||
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to InsertFront failed: Passed value is not a Task Manager", 3) end
|
||||
if type(func) ~= "function" then error("Attempt to InsertFront failed: Passed value is not a function", 3) end
|
||||
taskManager.__public.Tasks += 1
|
||||
local proxy, synchronousTask = Proxy.new(SynchronousTask, {TaskManager = proxy, Running = false})
|
||||
synchronousTask.Active = true
|
||||
synchronousTask.Function = func
|
||||
synchronousTask.Parameters = if ... == nil then nil else {...}
|
||||
if taskManager.First == nil then taskManager.First, taskManager.Last = proxy, proxy else synchronousTask.Next, getmetatable(taskManager.First).Previous, taskManager.First = taskManager.First, proxy, proxy end
|
||||
if taskManager.Active == false and taskManager.__public.Enabled == true then taskManager.Active = true task.defer(Run, taskManager) end
|
||||
return proxy
|
||||
end,
|
||||
InsertBack = function(proxy, func, ...)
|
||||
local taskManager = getmetatable(proxy)
|
||||
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to InsertBack failed: Passed value is not a Task Manager", 3) end
|
||||
if type(func) ~= "function" then error("Attempt to InsertBack failed: Passed value is not a function", 3) end
|
||||
taskManager.__public.Tasks += 1
|
||||
local proxy, synchronousTask = Proxy.new(SynchronousTask, {TaskManager = proxy, Running = false})
|
||||
synchronousTask.Active = true
|
||||
synchronousTask.Function = func
|
||||
synchronousTask.Parameters = if ... == nil then nil else {...}
|
||||
if taskManager.Last == nil then taskManager.First, taskManager.Last = proxy, proxy else synchronousTask.Previous, getmetatable(taskManager.Last).Next, taskManager.Last = taskManager.Last, proxy, proxy end
|
||||
if taskManager.Active == false and taskManager.__public.Enabled == true then taskManager.Active = true task.defer(Run, taskManager) end
|
||||
return proxy
|
||||
end,
|
||||
FindFirst = function(proxy, func)
|
||||
local taskManager = getmetatable(proxy)
|
||||
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to FindFirst failed: Passed value is not a Task Manager", 3) end
|
||||
if type(func) ~= "function" then error("Attempt to FindFirst failed: Passed value is not a function", 3) end
|
||||
proxy = taskManager.__public.Running
|
||||
if proxy ~= nil then
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if synchronousTask.Active == true and synchronousTask.Function == func then return proxy, 0 end
|
||||
end
|
||||
local index = 1
|
||||
proxy = taskManager.First
|
||||
while proxy ~= nil do
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if synchronousTask.Function == func then return proxy, index end
|
||||
proxy = synchronousTask.Next
|
||||
index += 1
|
||||
end
|
||||
end,
|
||||
FindLast = function(proxy, func)
|
||||
local taskManager = getmetatable(proxy)
|
||||
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to FindLast failed: Passed value is not a Task Manager", 3) end
|
||||
if type(func) ~= "function" then error("Attempt to FindFirst failed: Passed value is not a function", 3) end
|
||||
local index = if taskManager.__public.Running == nil then taskManager.__public.Tasks else taskManager.__public.Tasks - 1
|
||||
proxy = taskManager.Last
|
||||
while proxy ~= nil do
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if synchronousTask.Function == func then return proxy, index end
|
||||
proxy = synchronousTask.Previous
|
||||
index -= 1
|
||||
end
|
||||
proxy = taskManager.__public.Running
|
||||
if proxy ~= nil then
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if synchronousTask.Active == true and synchronousTask.Function == func then return proxy, 0 end
|
||||
end
|
||||
end,
|
||||
CancelAll = function(proxy, func)
|
||||
local taskManager = getmetatable(proxy)
|
||||
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to FindLast failed: Passed value is not a Task Manager", 3) end
|
||||
if func == nil then
|
||||
local proxy = taskManager.First
|
||||
taskManager.First = nil
|
||||
taskManager.Last = nil
|
||||
if taskManager.__public.Running == nil then taskManager.__public.Tasks = 0 else taskManager.__public.Tasks = 1 end
|
||||
while proxy ~= nil do
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
proxy, synchronousTask.Active, synchronousTask.__public.TaskManager, synchronousTask.Previous, synchronousTask.Next = synchronousTask.Next, false, nil, nil, nil
|
||||
end
|
||||
else
|
||||
if type(func) ~= "function" then error("Attempt to CancelAll failed: Passed value is not nil or function", 3) end
|
||||
local proxy = taskManager.First
|
||||
while proxy ~= nil do
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if synchronousTask.Function == func then
|
||||
taskManager.__public.Tasks -= 1
|
||||
if taskManager.First == proxy then taskManager.First = synchronousTask.Next end
|
||||
if taskManager.Last == proxy then taskManager.Last = synchronousTask.Previous end
|
||||
if synchronousTask.Previous ~= nil then getmetatable(synchronousTask.Previous).Next = synchronousTask.Next end
|
||||
if synchronousTask.Next ~= nil then getmetatable(synchronousTask.Next).Previous = synchronousTask.Previous end
|
||||
proxy, synchronousTask.Active, synchronousTask.__public.TaskManager, synchronousTask.Previous, synchronousTask.Next = synchronousTask.Next, false, nil, nil, nil
|
||||
else
|
||||
proxy = synchronousTask.Next
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
TaskManager.__set = {
|
||||
Enabled = function(proxy, taskManager, value)
|
||||
if type(value) ~= "boolean" then error("Attempt to set Enabled failed: Passed value is not a boolean", 3) end
|
||||
taskManager.__public.Enabled = value
|
||||
if value == false or taskManager.First == nil or taskManager.Active == true then return end
|
||||
taskManager.Active = true
|
||||
task.defer(Run, taskManager)
|
||||
end,
|
||||
Tasks = false,
|
||||
Running = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- SynchronousTask
|
||||
SynchronousTask.__tostring = function(proxy)
|
||||
return "Synchronous Task"
|
||||
end
|
||||
|
||||
SynchronousTask.__shared = {
|
||||
Wait = function(proxy, ...)
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if type(synchronousTask) ~= "table" or synchronousTask.__shared ~= SynchronousTask.__shared then error("Attempt to Wait failed: Passed value is not a Synchronous Task", 3) end
|
||||
if synchronousTask.Active == false then return end
|
||||
local waiter = {coroutine.running(), ...}
|
||||
if synchronousTask.Last == nil then synchronousTask.First, synchronousTask.Last = waiter, waiter else synchronousTask.Last.Next, synchronousTask.Last = waiter, waiter end
|
||||
return coroutine.yield()
|
||||
end,
|
||||
Cancel = function(proxy)
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if type(synchronousTask) ~= "table" or synchronousTask.__shared ~= SynchronousTask.__shared then error("Attempt to Cancel failed: Passed value is not a Synchronous Task", 3) end
|
||||
if synchronousTask.__public.Running == true then return false end
|
||||
local taskManager = synchronousTask.__public.TaskManager
|
||||
if taskManager == nil then return false end
|
||||
taskManager = getmetatable(taskManager)
|
||||
taskManager.__public.Tasks -= 1
|
||||
if taskManager.First == proxy then taskManager.First = synchronousTask.Next end
|
||||
if taskManager.Last == proxy then taskManager.Last = synchronousTask.Previous end
|
||||
if synchronousTask.Previous ~= nil then getmetatable(synchronousTask.Previous).Next = synchronousTask.Next end
|
||||
if synchronousTask.Next ~= nil then getmetatable(synchronousTask.Next).Previous = synchronousTask.Previous end
|
||||
synchronousTask.Active, synchronousTask.__public.TaskManager, synchronousTask.Previous, synchronousTask.Next = false, nil, nil, nil
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
SynchronousTask.__set = {
|
||||
TaskManager = false,
|
||||
Running = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- RunningTask
|
||||
RunningTask.__tostring = function(proxy)
|
||||
return "Running Task"
|
||||
end
|
||||
|
||||
RunningTask.__shared = {
|
||||
Next = function(proxy)
|
||||
local runningTask = getmetatable(proxy)
|
||||
if type(runningTask) ~= "table" or runningTask.__shared ~= RunningTask.__shared then error("Attempt to Next failed: Passed value is not a Running Task", 3) end
|
||||
local synchronousTask = runningTask.SynchronousTask
|
||||
local waiter = synchronousTask.First
|
||||
if waiter == nil then return end
|
||||
synchronousTask.First = waiter.Next
|
||||
if synchronousTask.Last == waiter then synchronousTask.Last = nil end
|
||||
return table.unpack(waiter)
|
||||
end,
|
||||
Iterate = function(proxy)
|
||||
local runningTask = getmetatable(proxy)
|
||||
if type(runningTask) ~= "table" or runningTask.__shared ~= RunningTask.__shared then error("Attempt to Iterate failed: Passed value is not a Running Task", 3) end
|
||||
return runningTask.__shared.Next, proxy
|
||||
end,
|
||||
End = function(proxy)
|
||||
local runningTask = getmetatable(proxy)
|
||||
if type(runningTask) ~= "table" or runningTask.__shared ~= RunningTask.__shared then error("Attempt to End failed: Passed value is not a Running Task", 3) end
|
||||
runningTask.SynchronousTask.Active = false
|
||||
end,
|
||||
}
|
||||
|
||||
RunningTask.__set = {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Functions
|
||||
Run = function(taskManager)
|
||||
if taskManager.__public.Enabled == false then taskManager.Active = false return end
|
||||
local proxy = taskManager.First
|
||||
if proxy == nil then taskManager.Active = false return end
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
taskManager.__public.Running = proxy
|
||||
taskManager.First = synchronousTask.Next
|
||||
synchronousTask.__public.Running = true
|
||||
if synchronousTask.Next == nil then taskManager.Last = nil else getmetatable(synchronousTask.Next).Previous = nil synchronousTask.Next = nil end
|
||||
local proxy, runningTask = Proxy.new(RunningTask)
|
||||
runningTask.SynchronousTask = synchronousTask
|
||||
if synchronousTask.Parameters == nil then synchronousTask.Function(proxy) else synchronousTask.Function(proxy, table.unpack(synchronousTask.Parameters)) end
|
||||
taskManager.__public.Tasks -= 1
|
||||
taskManager.__public.Running = nil
|
||||
synchronousTask.Active = false
|
||||
synchronousTask.__public.TaskManager = nil
|
||||
synchronousTask.__public.Running = false
|
||||
if taskManager.__public.Enabled == false or taskManager.First == nil then taskManager.Active = false else task.defer(Run, taskManager) end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
return table.freeze(Constructor) :: Constructor
|
734
src/Chemical/Packages/Datastore/init.lua
Normal file
734
src/Chemical/Packages/Datastore/init.lua
Normal file
|
@ -0,0 +1,734 @@
|
|||
-- Variables
|
||||
local Proxy = require(script.Proxy)
|
||||
local Signal = require(script.Signal)
|
||||
local SynchronousTaskManager = require(script.SynchronousTaskManager)
|
||||
local dataStoreService, memoryStoreService, httpService = game:GetService("DataStoreService"), game:GetService("MemoryStoreService"), game:GetService("HttpService")
|
||||
local Constructor, DataStore = {}, {}
|
||||
local OpenTask, ReadTask, LockTask, SaveTask, CloseTask, DestroyTask, Lock, Unlock, Load, Save, StartSaveTimer, StopSaveTimer, SaveTimerEnded, StartLockTimer, StopLockTimer, LockTimerEnded, ProcessQueue, SignalConnected, Clone, Reconcile, Compress, Decompress, Encode, Decode, BindToClose
|
||||
local dataStores, bindToClose, active = {}, {}, true
|
||||
local characters = {[0] = "0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","!","$","%","&","'",",",".","/",":",";","=","?","@","[","]","^","_","`","{","}","~"}
|
||||
local bytes = {} for i = (0), #characters do bytes[string.byte(characters[i])] = i end
|
||||
local base = #characters + 1
|
||||
|
||||
|
||||
|
||||
|
||||
-- Types
|
||||
export type Constructor = {
|
||||
new: (name: string, scope: string, key: string?) -> DataStore,
|
||||
hidden: (name: string, scope: string, key: string?) -> DataStore,
|
||||
find: (name: string, scope: string, key: string?) -> DataStore?,
|
||||
Response: {Success: string, Saved: string, Locked: string, State: string, Error: string},
|
||||
}
|
||||
|
||||
export type DataStore = {
|
||||
Value: any,
|
||||
Metadata: {[string]: any},
|
||||
UserIds: {any},
|
||||
SaveInterval: number,
|
||||
SaveDelay: number,
|
||||
LockInterval: number,
|
||||
LockAttempts: number,
|
||||
SaveOnClose: boolean,
|
||||
Id: string,
|
||||
UniqueId: string,
|
||||
Key: string,
|
||||
State: boolean?,
|
||||
Hidden: boolean,
|
||||
AttemptsRemaining: number,
|
||||
CreatedTime: number,
|
||||
UpdatedTime: number,
|
||||
Version: string,
|
||||
CompressedValue: string,
|
||||
StateChanged: Signal.Signal,
|
||||
Saving: Signal.Signal,
|
||||
Saved: Signal.Signal,
|
||||
AttemptsChanged: Signal.Signal,
|
||||
ProcessQueue: Signal.Signal,
|
||||
Open: (self: DataStore, template: any?) -> (string, any),
|
||||
Read: (self: DataStore, template: any?) -> (string, any),
|
||||
Save: (self: DataStore) -> (string, any),
|
||||
Close: (self: DataStore) -> (string, any),
|
||||
Destroy: (self: DataStore) -> (string, any),
|
||||
Queue: (self: DataStore, value: any, expiration: number?, priority: number?) -> (string, any),
|
||||
Remove: (self: DataStore, id: string) -> (string, any),
|
||||
Clone: (self: DataStore) -> any,
|
||||
Reconcile: (self: DataStore, template: any) -> (),
|
||||
Usage: (self: DataStore) -> (number, number),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Constructor
|
||||
Constructor.new = function(name, scope, key)
|
||||
if key == nil then key, scope = scope, "global" end
|
||||
local id = name .. "/" .. scope .. "/" .. key
|
||||
if dataStores[id] ~= nil then return dataStores[id] end
|
||||
local proxy, dataStore = Proxy.new(DataStore, {
|
||||
Metadata = {},
|
||||
UserIds = {},
|
||||
SaveInterval = 30,
|
||||
SaveDelay = 0,
|
||||
LockInterval = 60,
|
||||
LockAttempts = 5,
|
||||
SaveOnClose = true,
|
||||
Id = id,
|
||||
UniqueId = httpService:GenerateGUID(false),
|
||||
Key = key,
|
||||
State = false,
|
||||
Hidden = false,
|
||||
AttemptsRemaining = 0,
|
||||
CreatedTime = 0,
|
||||
UpdatedTime = 0,
|
||||
Version = "",
|
||||
CompressedValue = "",
|
||||
StateChanged = Signal.new(),
|
||||
Saving = Signal.new(),
|
||||
Saved = Signal.new(),
|
||||
AttemptsChanged = Signal.new(),
|
||||
ProcessQueue = Signal.new(),
|
||||
})
|
||||
dataStore.TaskManager = SynchronousTaskManager.new()
|
||||
dataStore.LockTime = -math.huge
|
||||
dataStore.SaveTime = -math.huge
|
||||
dataStore.ActiveLockInterval = 0
|
||||
dataStore.ProcessingQueue = false
|
||||
dataStore.DataStore = dataStoreService:GetDataStore(name, scope)
|
||||
dataStore.MemoryStore = memoryStoreService:GetSortedMap(id)
|
||||
dataStore.Queue = memoryStoreService:GetQueue(id)
|
||||
dataStore.Options = Instance.new("DataStoreSetOptions")
|
||||
dataStore.__public.ProcessQueue.DataStore = proxy
|
||||
dataStore.__public.ProcessQueue.Connected = SignalConnected
|
||||
dataStores[id] = proxy
|
||||
if active == true then bindToClose[dataStore.__public.UniqueId] = proxy end
|
||||
return proxy
|
||||
end
|
||||
|
||||
Constructor.hidden = function(name, scope, key)
|
||||
if key == nil then key, scope = scope, "global" end
|
||||
local id = name .. "/" .. scope .. "/" .. key
|
||||
local proxy, dataStore = Proxy.new(DataStore, {
|
||||
Metadata = {},
|
||||
UserIds = {},
|
||||
SaveInterval = 30,
|
||||
SaveDelay = 0,
|
||||
LockInterval = 60,
|
||||
LockAttempts = 5,
|
||||
SaveOnClose = true,
|
||||
Id = id,
|
||||
UniqueId = httpService:GenerateGUID(false),
|
||||
Key = key,
|
||||
State = false,
|
||||
Hidden = true,
|
||||
AttemptsRemaining = 0,
|
||||
CreatedTime = 0,
|
||||
UpdatedTime = 0,
|
||||
Version = "",
|
||||
CompressedValue = "",
|
||||
StateChanged = Signal.new(),
|
||||
Saving = Signal.new(),
|
||||
Saved = Signal.new(),
|
||||
AttemptsChanged = Signal.new(),
|
||||
ProcessQueue = Signal.new(),
|
||||
})
|
||||
dataStore.TaskManager = SynchronousTaskManager.new()
|
||||
dataStore.LockTime = -math.huge
|
||||
dataStore.SaveTime = -math.huge
|
||||
dataStore.ActiveLockInterval = 0
|
||||
dataStore.ProcessingQueue = false
|
||||
dataStore.DataStore = dataStoreService:GetDataStore(name, scope)
|
||||
dataStore.MemoryStore = memoryStoreService:GetSortedMap(id)
|
||||
dataStore.Queue = memoryStoreService:GetQueue(id)
|
||||
dataStore.Options = Instance.new("DataStoreSetOptions")
|
||||
dataStore.__public.ProcessQueue.DataStore = proxy
|
||||
dataStore.__public.ProcessQueue.Connected = SignalConnected
|
||||
if active == true then bindToClose[dataStore.__public.UniqueId] = proxy end
|
||||
return proxy
|
||||
end
|
||||
|
||||
Constructor.find = function(name, scope, key)
|
||||
if key == nil then key, scope = scope, "global" end
|
||||
local id = name .. "/" .. scope .. "/" .. key
|
||||
return dataStores[id]
|
||||
end
|
||||
|
||||
Constructor.Response = {Success = "Success", Saved = "Saved", Locked = "Locked", State = "State", Error = "Error"}
|
||||
|
||||
|
||||
|
||||
|
||||
-- DataStore
|
||||
DataStore.__tostring = function(proxy)
|
||||
return "DataStore"
|
||||
end
|
||||
|
||||
DataStore.__shared = {
|
||||
Open = function(proxy, template)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Open failed: Passed value is not a DataStore", 3) end
|
||||
if dataStore.__public.State == nil then return "State", "Destroyed" end
|
||||
local synchronousTask = dataStore.TaskManager:FindFirst(OpenTask)
|
||||
if synchronousTask ~= nil then return synchronousTask:Wait(template) end
|
||||
if dataStore.TaskManager:FindLast(DestroyTask) ~= nil then return "State", "Destroying" end
|
||||
if dataStore.__public.State == true and dataStore.TaskManager:FindLast(CloseTask) == nil then
|
||||
if dataStore.__public.Value == nil then
|
||||
dataStore.__public.Value = Clone(template)
|
||||
elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
|
||||
Reconcile(dataStore.__public.Value, template)
|
||||
end
|
||||
return "Success"
|
||||
end
|
||||
return dataStore.TaskManager:InsertBack(OpenTask, proxy):Wait(template)
|
||||
end,
|
||||
Read = function(proxy, template)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Read failed: Passed value is not a DataStore", 3) end
|
||||
local synchronousTask = dataStore.TaskManager:FindFirst(ReadTask)
|
||||
if synchronousTask ~= nil then return synchronousTask:Wait(template) end
|
||||
if dataStore.__public.State == true and dataStore.TaskManager:FindLast(CloseTask) == nil then return "State", "Open" end
|
||||
return dataStore.TaskManager:InsertBack(ReadTask, proxy):Wait(template)
|
||||
end,
|
||||
Save = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Save failed: Passed value is not a DataStore", 3) end
|
||||
if dataStore.__public.State == false then return "State", "Closed" end
|
||||
if dataStore.__public.State == nil then return "State", "Destroyed" end
|
||||
local synchronousTask = dataStore.TaskManager:FindFirst(SaveTask)
|
||||
if synchronousTask ~= nil then return synchronousTask:Wait() end
|
||||
if dataStore.TaskManager:FindLast(CloseTask) ~= nil then return "State", "Closing" end
|
||||
if dataStore.TaskManager:FindLast(DestroyTask) ~= nil then return "State", "Destroying" end
|
||||
return dataStore.TaskManager:InsertBack(SaveTask, proxy):Wait()
|
||||
end,
|
||||
Close = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Close failed: Passed value is not a DataStore", 3) end
|
||||
if dataStore.__public.State == nil then return "Success" end
|
||||
local synchronousTask = dataStore.TaskManager:FindFirst(CloseTask)
|
||||
if synchronousTask ~= nil then return synchronousTask:Wait() end
|
||||
if dataStore.__public.State == false and dataStore.TaskManager:FindLast(OpenTask) == nil then return "Success" end
|
||||
local synchronousTask = dataStore.TaskManager:FindFirst(DestroyTask)
|
||||
if synchronousTask ~= nil then return synchronousTask:Wait() end
|
||||
StopLockTimer(dataStore)
|
||||
StopSaveTimer(dataStore)
|
||||
return dataStore.TaskManager:InsertBack(CloseTask, proxy):Wait()
|
||||
end,
|
||||
Destroy = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Destroy failed: Passed value is not a DataStore", 3) end
|
||||
if dataStore.__public.State == nil then return "Success" end
|
||||
dataStores[dataStore.__public.Id] = nil
|
||||
StopLockTimer(dataStore)
|
||||
StopSaveTimer(dataStore)
|
||||
return (dataStore.TaskManager:FindFirst(DestroyTask) or dataStore.TaskManager:InsertBack(DestroyTask, proxy)):Wait()
|
||||
end,
|
||||
Queue = function(proxy, value, expiration, priority)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Queue failed: Passed value is not a DataStore", 3) end
|
||||
if expiration ~= nil and type(expiration) ~= "number" then error("Attempt to Queue failed: Passed value is not nil or number", 3) end
|
||||
if priority ~= nil and type(priority) ~= "number" then error("Attempt to Queue failed: Passed value is not nil or number", 3) end
|
||||
local success, errorMessage
|
||||
for i = 1, 3 do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, errorMessage = pcall(dataStore.Queue.AddAsync, dataStore.Queue, value, expiration or 604800, priority)
|
||||
if success == true then return "Success" end
|
||||
end
|
||||
return "Error", errorMessage
|
||||
end,
|
||||
Remove = function(proxy, id)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Remove failed: Passed value is not a DataStore", 3) end
|
||||
if type(id) ~= "string" then error("Attempt to RemoveQueue failed: Passed value is not a string", 3) end
|
||||
local success, errorMessage
|
||||
for i = 1, 3 do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, errorMessage = pcall(dataStore.Queue.RemoveAsync, dataStore.Queue, id)
|
||||
if success == true then return "Success" end
|
||||
end
|
||||
return "Error", errorMessage
|
||||
end,
|
||||
Clone = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Clone failed: Passed value is not a DataStore", 3) end
|
||||
return Clone(dataStore.__public.Value)
|
||||
end,
|
||||
Reconcile = function(proxy, template)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Reconcile failed: Passed value is not a DataStore", 3) end
|
||||
if dataStore.__public.Value == nil then
|
||||
dataStore.__public.Value = Clone(template)
|
||||
elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
|
||||
Reconcile(dataStore.__public.Value, template)
|
||||
end
|
||||
end,
|
||||
Usage = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Usage failed: Passed value is not a DataStore", 3) end
|
||||
if dataStore.__public.Value == nil then return 0, 0 end
|
||||
if type(dataStore.__public.Metadata.Compress) ~= "table" then
|
||||
local characters = #httpService:JSONEncode(dataStore.__public.Value)
|
||||
return characters, characters / 4194303
|
||||
else
|
||||
local level = dataStore.__public.Metadata.Compress.Level or 2
|
||||
local decimals = 10 ^ (dataStore.__public.Metadata.Compress.Decimals or 3)
|
||||
local safety = if dataStore.__public.Metadata.Compress.Safety == nil then true else dataStore.__public.Metadata.Compress.Safety
|
||||
dataStore.__public.CompressedValue = Compress(dataStore.__public.Value, level, decimals, safety)
|
||||
local characters = #httpService:JSONEncode(dataStore.__public.CompressedValue)
|
||||
return characters, characters / 4194303
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
DataStore.__set = {
|
||||
Metadata = function(proxy, dataStore, value)
|
||||
if type(value) ~= "table" then error("Attempt to set Metadata failed: Passed value is not a table", 3) end
|
||||
dataStore.__public.Metadata = value
|
||||
end,
|
||||
UserIds = function(proxy, dataStore, value)
|
||||
if type(value) ~= "table" then error("Attempt to set UserIds failed: Passed value is not a table", 3) end
|
||||
dataStore.__public.UserIds = value
|
||||
end,
|
||||
SaveInterval = function(proxy, dataStore, value)
|
||||
if type(value) ~= "number" then error("Attempt to set SaveInterval failed: Passed value is not a number", 3) end
|
||||
if value < 10 and value ~= 0 then error("Attempt to set SaveInterval failed: Passed value is less then 10 and not 0", 3) end
|
||||
if value > 1000 then error("Attempt to set SaveInterval failed: Passed value is more then 1000", 3) end
|
||||
if value == dataStore.__public.SaveInterval then return end
|
||||
dataStore.__public.SaveInterval = value
|
||||
if dataStore.__public.State ~= true then return end
|
||||
if value == 0 then
|
||||
StopSaveTimer(dataStore)
|
||||
elseif dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then
|
||||
StartSaveTimer(proxy)
|
||||
end
|
||||
end,
|
||||
SaveDelay = function(proxy, dataStore, value)
|
||||
if type(value) ~= "number" then error("Attempt to set SaveDelay failed: Passed value is not a number", 3) end
|
||||
if value < 0 then error("Attempt to set SaveDelay failed: Passed value is less then 0", 3) end
|
||||
if value > 10 then error("Attempt to set SaveDelay failed: Passed value is more then 10", 3) end
|
||||
dataStore.__public.SaveDelay = value
|
||||
end,
|
||||
LockInterval = function(proxy, dataStore, value)
|
||||
if type(value) ~= "number" then error("Attempt to set LockInterval failed: Passed value is not a number", 3) end
|
||||
if value < 10 then error("Attempt to set LockInterval failed: Passed value is less then 10", 3) end
|
||||
if value > 1000 then error("Attempt to set LockInterval failed: Passed value is more then 1000", 3) end
|
||||
dataStore.__public.LockInterval = value
|
||||
end,
|
||||
LockAttempts = function(proxy, dataStore, value)
|
||||
if type(value) ~= "number" then error("Attempt to set LockAttempts failed: Passed value is not a number", 3) end
|
||||
if value < 1 then error("Attempt to set LockAttempts failed: Passed value is less then 1", 3) end
|
||||
if value > 100 then error("Attempt to set LockAttempts failed: Passed value is more then 100", 3) end
|
||||
dataStore.__public.LockAttempts = value
|
||||
end,
|
||||
SaveOnClose = function(proxy, dataStore, value)
|
||||
if type(value) ~= "boolean" then error("Attempt to set SaveOnClose failed: Passed value is not a boolean", 3) end
|
||||
dataStore.__public.SaveOnClose = value
|
||||
end,
|
||||
Id = false,
|
||||
UniqueId = false,
|
||||
Key = false,
|
||||
State = false,
|
||||
Hidden = false,
|
||||
AttemptsRemaining = false,
|
||||
CreatedTime = false,
|
||||
UpdatedTime = false,
|
||||
Version = false,
|
||||
CompressedValue = false,
|
||||
StateChanged = false,
|
||||
Saving = false,
|
||||
Saved = false,
|
||||
AttemptsChanged = false,
|
||||
ProcessQueue = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Functions
|
||||
OpenTask = function(runningTask, proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
local response, responseData = Lock(dataStore, 3)
|
||||
if response ~= "Success" then for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end return end
|
||||
local response, responseData = Load(dataStore, 3)
|
||||
if response ~= "Success" then Unlock(dataStore, 3) for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end return end
|
||||
dataStore.__public.State = true
|
||||
if dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then
|
||||
StartSaveTimer(proxy)
|
||||
StartLockTimer(proxy)
|
||||
end
|
||||
for thread, template in runningTask:Iterate() do
|
||||
if dataStore.__public.Value == nil then
|
||||
dataStore.__public.Value = Clone(template)
|
||||
elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
|
||||
Reconcile(dataStore.__public.Value, template)
|
||||
end
|
||||
task.defer(thread, response)
|
||||
end
|
||||
if dataStore.ProcessingQueue == false and dataStore.__public.ProcessQueue.Connections > 0 then task.defer(ProcessQueue, proxy) end
|
||||
dataStore.__public.StateChanged:Fire(true, proxy)
|
||||
end
|
||||
|
||||
ReadTask = function(runningTask, proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.__public.State == true then for thread in runningTask:Iterate() do task.defer(thread, "State", "Open") end return end
|
||||
local response, responseData = Load(dataStore, 3)
|
||||
if response ~= "Success" then for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end return end
|
||||
for thread, template in runningTask:Iterate() do
|
||||
if dataStore.__public.Value == nil then
|
||||
dataStore.__public.Value = Clone(template)
|
||||
elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
|
||||
Reconcile(dataStore.__public.Value, template)
|
||||
end
|
||||
task.defer(thread, response)
|
||||
end
|
||||
end
|
||||
|
||||
LockTask = function(runningTask, proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
local attemptsRemaining = dataStore.__public.AttemptsRemaining
|
||||
local response, responseData = Lock(dataStore, 3)
|
||||
if response ~= "Success" then dataStore.__public.AttemptsRemaining -= 1 end
|
||||
if dataStore.__public.AttemptsRemaining ~= attemptsRemaining then dataStore.__public.AttemptsChanged:Fire(dataStore.__public.AttemptsRemaining, proxy) end
|
||||
if dataStore.__public.AttemptsRemaining > 0 then
|
||||
if dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then StartLockTimer(proxy) end
|
||||
else
|
||||
dataStore.__public.State = false
|
||||
StopLockTimer(dataStore)
|
||||
StopSaveTimer(dataStore)
|
||||
if dataStore.__public.SaveOnClose == true then Save(proxy, 3) end
|
||||
Unlock(dataStore, 3)
|
||||
dataStore.__public.StateChanged:Fire(false, proxy)
|
||||
end
|
||||
for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
|
||||
end
|
||||
|
||||
SaveTask = function(runningTask, proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.__public.State == false then for thread in runningTask:Iterate() do task.defer(thread, "State", "Closed") end return end
|
||||
StopSaveTimer(dataStore)
|
||||
runningTask:End()
|
||||
local response, responseData = Save(proxy, 3)
|
||||
if dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then StartSaveTimer(proxy) end
|
||||
for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
|
||||
end
|
||||
|
||||
CloseTask = function(runningTask, proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.__public.State == false then for thread in runningTask:Iterate() do task.defer(thread, "Success") end return end
|
||||
dataStore.__public.State = false
|
||||
local response, responseData = nil, nil
|
||||
if dataStore.__public.SaveOnClose == true then response, responseData = Save(proxy, 3) end
|
||||
Unlock(dataStore, 3)
|
||||
dataStore.__public.StateChanged:Fire(false, proxy)
|
||||
if response == "Saved" then
|
||||
for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
|
||||
else
|
||||
for thread in runningTask:Iterate() do task.defer(thread, "Success") end
|
||||
end
|
||||
end
|
||||
|
||||
DestroyTask = function(runningTask, proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
local response, responseData = nil, nil
|
||||
if dataStore.__public.State == false then
|
||||
dataStore.__public.State = nil
|
||||
else
|
||||
dataStore.__public.State = nil
|
||||
if dataStore.__public.SaveOnClose == true then response, responseData = Save(proxy, 3) end
|
||||
Unlock(dataStore, 3)
|
||||
end
|
||||
dataStore.__public.StateChanged:Fire(nil, proxy)
|
||||
dataStore.__public.StateChanged:DisconnectAll()
|
||||
dataStore.__public.Saving:DisconnectAll()
|
||||
dataStore.__public.Saved:DisconnectAll()
|
||||
dataStore.__public.AttemptsChanged:DisconnectAll()
|
||||
dataStore.__public.ProcessQueue:DisconnectAll()
|
||||
bindToClose[dataStore.__public.UniqueId] = nil
|
||||
if response == "Saved" then
|
||||
for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
|
||||
else
|
||||
for thread in runningTask:Iterate() do task.defer(thread, "Success") end
|
||||
end
|
||||
end
|
||||
|
||||
Lock = function(dataStore, attempts)
|
||||
local success, value, id, lockTime, lockInterval, lockAttempts = nil, nil, nil, nil, dataStore.__public.LockInterval, dataStore.__public.LockAttempts
|
||||
for i = 1, attempts do
|
||||
if i > 1 then task.wait(1) end
|
||||
lockTime = os.clock()
|
||||
success, value = pcall(dataStore.MemoryStore.UpdateAsync, dataStore.MemoryStore, "Id", function(value) id = value return if id == nil or id == dataStore.__public.UniqueId then dataStore.__public.UniqueId else nil end, lockInterval * lockAttempts + 30)
|
||||
if success == true then break end
|
||||
end
|
||||
if success == false then return "Error", value end
|
||||
if value == nil then return "Locked", id end
|
||||
dataStore.LockTime = lockTime + lockInterval * lockAttempts
|
||||
dataStore.ActiveLockInterval = lockInterval
|
||||
dataStore.__public.AttemptsRemaining = lockAttempts
|
||||
return "Success"
|
||||
end
|
||||
|
||||
Unlock = function(dataStore, attempts)
|
||||
local success, value, id = nil, nil, nil
|
||||
for i = 1, attempts do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, value = pcall(dataStore.MemoryStore.UpdateAsync, dataStore.MemoryStore, "Id", function(value) id = value return if id == dataStore.__public.UniqueId then dataStore.__public.UniqueId else nil end, 0)
|
||||
if success == true then break end
|
||||
end
|
||||
if success == false then return "Error", value end
|
||||
if value == nil and id ~= nil then return "Locked", id end
|
||||
return "Success"
|
||||
end
|
||||
|
||||
Load = function(dataStore, attempts)
|
||||
local success, value, info = nil, nil, nil
|
||||
for i = 1, attempts do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, value, info = pcall(dataStore.DataStore.GetAsync, dataStore.DataStore, dataStore.__public.Key)
|
||||
if success == true then break end
|
||||
end
|
||||
if success == false then return "Error", value end
|
||||
if info == nil then
|
||||
dataStore.__public.Metadata, dataStore.__public.UserIds, dataStore.__public.CreatedTime, dataStore.__public.UpdatedTime, dataStore.__public.Version = {}, {}, 0, 0, ""
|
||||
else
|
||||
dataStore.__public.Metadata, dataStore.__public.UserIds, dataStore.__public.CreatedTime, dataStore.__public.UpdatedTime, dataStore.__public.Version = info:GetMetadata(), info:GetUserIds(), info.CreatedTime, info.UpdatedTime, info.Version
|
||||
end
|
||||
if type(dataStore.__public.Metadata.Compress) ~= "table" then
|
||||
dataStore.__public.Value = value
|
||||
else
|
||||
dataStore.__public.CompressedValue = value
|
||||
local decimals = 10 ^ (dataStore.__public.Metadata.Compress.Decimals or 3)
|
||||
dataStore.__public.Value = Decompress(dataStore.__public.CompressedValue, decimals)
|
||||
end
|
||||
return "Success"
|
||||
end
|
||||
|
||||
Save = function(proxy, attempts)
|
||||
local dataStore = getmetatable(proxy)
|
||||
local deltaTime = os.clock() - dataStore.SaveTime
|
||||
if deltaTime < dataStore.__public.SaveDelay then task.wait(dataStore.__public.SaveDelay - deltaTime) end
|
||||
dataStore.__public.Saving:Fire(dataStore.__public.Value, proxy)
|
||||
local success, value, info = nil, nil, nil
|
||||
if dataStore.__public.Value == nil then
|
||||
for i = 1, attempts do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, value, info = pcall(dataStore.DataStore.RemoveAsync, dataStore.DataStore, dataStore.__public.Key)
|
||||
if success == true then break end
|
||||
end
|
||||
if success == false then dataStore.__public.Saved:Fire("Error", value, proxy) return "Error", value end
|
||||
dataStore.__public.Metadata, dataStore.__public.UserIds, dataStore.__public.CreatedTime, dataStore.__public.UpdatedTime, dataStore.__public.Version = {}, {}, 0, 0, ""
|
||||
elseif type(dataStore.__public.Metadata.Compress) ~= "table" then
|
||||
dataStore.Options:SetMetadata(dataStore.__public.Metadata)
|
||||
for i = 1, attempts do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, value = pcall(dataStore.DataStore.SetAsync, dataStore.DataStore, dataStore.__public.Key, dataStore.__public.Value, dataStore.__public.UserIds, dataStore.Options)
|
||||
if success == true then break end
|
||||
end
|
||||
if success == false then dataStore.__public.Saved:Fire("Error", value, proxy) return "Error", value end
|
||||
dataStore.__public.Version = value
|
||||
else
|
||||
local level = dataStore.__public.Metadata.Compress.Level or 2
|
||||
local decimals = 10 ^ (dataStore.__public.Metadata.Compress.Decimals or 3)
|
||||
local safety = if dataStore.__public.Metadata.Compress.Safety == nil then true else dataStore.__public.Metadata.Compress.Safety
|
||||
dataStore.__public.CompressedValue = Compress(dataStore.__public.Value, level, decimals, safety)
|
||||
dataStore.Options:SetMetadata(dataStore.__public.Metadata)
|
||||
for i = 1, attempts do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, value = pcall(dataStore.DataStore.SetAsync, dataStore.DataStore, dataStore.__public.Key, dataStore.__public.CompressedValue, dataStore.__public.UserIds, dataStore.Options)
|
||||
if success == true then break end
|
||||
end
|
||||
if success == false then dataStore.__public.Saved:Fire("Error", value, proxy) return "Error", value end
|
||||
dataStore.Version = value
|
||||
end
|
||||
dataStore.SaveTime = os.clock()
|
||||
dataStore.__public.Saved:Fire("Saved", dataStore.__public.Value, proxy)
|
||||
return "Saved", dataStore.__public.Value
|
||||
end
|
||||
|
||||
StartSaveTimer = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.SaveThread ~= nil then task.cancel(dataStore.SaveThread) end
|
||||
if dataStore.__public.SaveInterval == 0 then return end
|
||||
dataStore.SaveThread = task.delay(dataStore.__public.SaveInterval, SaveTimerEnded, proxy)
|
||||
end
|
||||
|
||||
StopSaveTimer = function(dataStore)
|
||||
if dataStore.SaveThread == nil then return end
|
||||
task.cancel(dataStore.SaveThread)
|
||||
dataStore.SaveThread = nil
|
||||
end
|
||||
|
||||
SaveTimerEnded = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
dataStore.SaveThread = nil
|
||||
if dataStore.TaskManager:FindLast(SaveTask) ~= nil then return end
|
||||
dataStore.TaskManager:InsertBack(SaveTask, proxy)
|
||||
end
|
||||
|
||||
StartLockTimer = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.LockThread ~= nil then task.cancel(dataStore.LockThread) end
|
||||
local startTime = dataStore.LockTime - dataStore.__public.AttemptsRemaining * dataStore.ActiveLockInterval
|
||||
dataStore.LockThread = task.delay(startTime - os.clock() + dataStore.ActiveLockInterval, LockTimerEnded, proxy)
|
||||
end
|
||||
|
||||
StopLockTimer = function(dataStore)
|
||||
if dataStore.LockThread == nil then return end
|
||||
task.cancel(dataStore.LockThread)
|
||||
dataStore.LockThread = nil
|
||||
end
|
||||
|
||||
LockTimerEnded = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
dataStore.LockThread = nil
|
||||
if dataStore.TaskManager:FindFirst(LockTask) ~= nil then return end
|
||||
dataStore.TaskManager:InsertBack(LockTask, proxy)
|
||||
end
|
||||
|
||||
ProcessQueue = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.__public.State ~= true then return end
|
||||
if dataStore.__public.ProcessQueue.Connections == 0 then return end
|
||||
if dataStore.ProcessingQueue == true then return end
|
||||
dataStore.ProcessingQueue = true
|
||||
while true do
|
||||
local success, values, id = pcall(dataStore.Queue.ReadAsync, dataStore.Queue, 100, false, 30)
|
||||
if dataStore.__public.State ~= true then break end
|
||||
if dataStore.__public.ProcessQueue.Connections == 0 then break end
|
||||
if success == true and id ~= nil then dataStore.__public.ProcessQueue:Fire(id, values, proxy) end
|
||||
end
|
||||
dataStore.ProcessingQueue = false
|
||||
end
|
||||
|
||||
SignalConnected = function(connected, signal)
|
||||
if connected == false then return end
|
||||
ProcessQueue(signal.DataStore)
|
||||
end
|
||||
|
||||
Clone = function(original)
|
||||
if type(original) ~= "table" then return original end
|
||||
local clone = {}
|
||||
for index, value in original do clone[index] = Clone(value) end
|
||||
return clone
|
||||
end
|
||||
|
||||
Reconcile = function(target, template)
|
||||
for index, value in template do
|
||||
if type(index) == "number" then continue end
|
||||
if target[index] == nil then
|
||||
target[index] = Clone(value)
|
||||
elseif type(target[index]) == "table" and type(value) == "table" then
|
||||
Reconcile(target[index], value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Compress = function(value, level, decimals, safety)
|
||||
local data = {}
|
||||
if type(value) == "boolean" then
|
||||
table.insert(data, if value == false then "-" else "+")
|
||||
elseif type(value) == "number" then
|
||||
if value % 1 == 0 then
|
||||
table.insert(data, if value < 0 then "<" .. Encode(-value) else ">" .. Encode(value))
|
||||
else
|
||||
table.insert(data, if value < 0 then "(" .. Encode(math.round(-value * decimals)) else ")" .. Encode(math.round(value * decimals)))
|
||||
end
|
||||
elseif type(value) == "string" then
|
||||
if safety == true then value = value:gsub("", " ") end
|
||||
table.insert(data, "#" .. value .. "")
|
||||
elseif type(value) == "table" then
|
||||
if #value > 0 and level == 2 then
|
||||
table.insert(data, "|")
|
||||
for i = 1, #value do table.insert(data, Compress(value[i], level, decimals, safety)) end
|
||||
table.insert(data, "")
|
||||
else
|
||||
table.insert(data, "*")
|
||||
for key, tableValue in value do table.insert(data, Compress(key, level, decimals, safety)) table.insert(data, Compress(tableValue, level, decimals, safety)) end
|
||||
table.insert(data, "")
|
||||
end
|
||||
end
|
||||
return table.concat(data)
|
||||
end
|
||||
|
||||
Decompress = function(value, decimals, index)
|
||||
local i1, i2, dataType, data = value:find("([-+<>()#|*])", index or 1)
|
||||
if dataType == "-" then
|
||||
return false, i2
|
||||
elseif dataType == "+" then
|
||||
return true, i2
|
||||
elseif dataType == "<" then
|
||||
i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
|
||||
return -Decode(data), i2
|
||||
elseif dataType == ">" then
|
||||
i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
|
||||
return Decode(data), i2
|
||||
elseif dataType == "(" then
|
||||
i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
|
||||
return -Decode(data) / decimals, i2
|
||||
elseif dataType == ")" then
|
||||
i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
|
||||
return Decode(data) / decimals, i2
|
||||
elseif dataType == "#" then
|
||||
i1, i2, data = value:find("(.-)", i2 + 1)
|
||||
return data, i2
|
||||
elseif dataType == "|" then
|
||||
local array = {}
|
||||
while true do
|
||||
data, i2 = Decompress(value, decimals, i2 + 1)
|
||||
if data == nil then break end
|
||||
table.insert(array, data)
|
||||
end
|
||||
return array, i2
|
||||
elseif dataType == "*" then
|
||||
local dictionary, key = {}, nil
|
||||
while true do
|
||||
key, i2 = Decompress(value, decimals, i2 + 1)
|
||||
if key == nil then break end
|
||||
data, i2 = Decompress(value, decimals, i2 + 1)
|
||||
dictionary[key] = data
|
||||
end
|
||||
return dictionary, i2
|
||||
end
|
||||
return nil, i2
|
||||
end
|
||||
|
||||
Encode = function(value)
|
||||
if value == 0 then return "0" end
|
||||
local data = {}
|
||||
while value > 0 do
|
||||
table.insert(data, characters[value % base])
|
||||
value = math.floor(value / base)
|
||||
end
|
||||
return table.concat(data)
|
||||
end
|
||||
|
||||
Decode = function(value)
|
||||
local number, power, data = 0, 1, {string.byte(value, 1, #value)}
|
||||
for i, code in data do
|
||||
number += bytes[code] * power
|
||||
power *= base
|
||||
end
|
||||
return number
|
||||
end
|
||||
|
||||
BindToClose = function()
|
||||
active = false
|
||||
for uniqueId, proxy in bindToClose do
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.__public.State == nil then continue end
|
||||
dataStores[dataStore.__public.Id] = nil
|
||||
StopLockTimer(dataStore)
|
||||
StopSaveTimer(dataStore)
|
||||
if dataStore.TaskManager:FindFirst(DestroyTask) == nil then dataStore.TaskManager:InsertBack(DestroyTask, proxy) end
|
||||
end
|
||||
while next(bindToClose) ~= nil do task.wait() end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-- Events
|
||||
game:BindToClose(BindToClose)
|
||||
|
||||
|
||||
|
||||
|
||||
return table.freeze(Constructor) :: Constructor
|
3
src/Chemical/Packages/Datastore/init.meta.json
Normal file
3
src/Chemical/Packages/Datastore/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
2716
src/Chemical/Packages/JECS.lua
Normal file
2716
src/Chemical/Packages/JECS.lua
Normal file
File diff suppressed because it is too large
Load diff
160
src/Chemical/Packages/LinkedList.lua
Normal file
160
src/Chemical/Packages/LinkedList.lua
Normal file
|
@ -0,0 +1,160 @@
|
|||
local listClass, linkClass = {}, {}
|
||||
listClass.__index, linkClass.__index = listClass, linkClass
|
||||
|
||||
export type List<T> = typeof(listClass.new((1 :: any) :: T))
|
||||
|
||||
function listClass.new()
|
||||
local self = setmetatable({}, listClass)
|
||||
self.List = self
|
||||
self.Links = {}
|
||||
self.Length = 0
|
||||
self.Next = self
|
||||
self.Previous = self
|
||||
return self
|
||||
end
|
||||
|
||||
function listClass:InsertFront(value)
|
||||
assert(self.Links[value] == nil, "Value already exists")
|
||||
local link = {Value = value, List = self, Previous = self, Next = self.Next}
|
||||
self.Links[value] = link
|
||||
self.Length += 1
|
||||
self.Next.Previous = link
|
||||
self.Next = link
|
||||
return setmetatable(link, linkClass)
|
||||
end
|
||||
|
||||
function listClass:InsertAfter(value)
|
||||
assert(self.Links[value] == nil, "Value already exists")
|
||||
local link = {Value = value, List = self, Previous = self, Next = self.Next}
|
||||
self.Links[value] = link
|
||||
self.Length += 1
|
||||
self.Next.Previous = link
|
||||
self.Next = link
|
||||
return setmetatable(link, linkClass)
|
||||
end
|
||||
|
||||
function listClass:InsertBack(value)
|
||||
assert(self.Links[value] == nil, "Value already exists")
|
||||
local link = {Value = value, List = self, Next = self, Previous = self.Previous}
|
||||
self.Links[value] = link
|
||||
self.Length += 1
|
||||
self.Previous.Next = link
|
||||
self.Previous = link
|
||||
return setmetatable(link, linkClass)
|
||||
end
|
||||
|
||||
function listClass:InsertBefore(value)
|
||||
assert(self.Links[value] == nil, "Value already exists")
|
||||
local link = {Value = value, List = self, Next = self, Previous = self.Previous}
|
||||
self.Links[value] = link
|
||||
self.Length += 1
|
||||
self.Previous.Next = link
|
||||
self.Previous = link
|
||||
return setmetatable(link, linkClass)
|
||||
end
|
||||
|
||||
function listClass:GetNext(link)
|
||||
link = (link or self).Next
|
||||
if link ~= self then return link, link.Value end
|
||||
end
|
||||
|
||||
function listClass:GetPrevious(link)
|
||||
link = (link or self).Previous
|
||||
if link ~= self then return link, link.Value end
|
||||
end
|
||||
|
||||
function listClass:IterateForward(link)
|
||||
return listClass.GetNext, self, link
|
||||
end
|
||||
|
||||
function listClass:IterateBackward(link)
|
||||
return listClass.GetPrevious, self, link
|
||||
end
|
||||
|
||||
function listClass:Remove(value, clean: boolean?)
|
||||
local link = self.Links[value]
|
||||
if link ~= nil then
|
||||
self.Links[value] = nil
|
||||
self.Length -= 1
|
||||
link.List = nil
|
||||
link.Previous.Next = link.Next
|
||||
link.Next.Previous = link.Previous
|
||||
|
||||
if clean then setmetatable(link, nil) return end
|
||||
return link
|
||||
end
|
||||
end
|
||||
|
||||
function listClass:RemoveFirst()
|
||||
local link = self.Next
|
||||
if link ~= self then
|
||||
self.Links[link.Value] = nil
|
||||
self.Length -= 1
|
||||
link.List = nil
|
||||
link.Previous.Next = link.Next
|
||||
link.Next.Previous = link.Previous
|
||||
return link.Value, link
|
||||
end
|
||||
end
|
||||
|
||||
function listClass:RemoveLast()
|
||||
local link = self.Previous
|
||||
if link ~= self then
|
||||
self.Links[link.Value] = nil
|
||||
self.Length -= 1
|
||||
link.List = nil
|
||||
link.Previous.Next = link.Next
|
||||
link.Next.Previous = link.Previous
|
||||
return link.Value, link
|
||||
end
|
||||
end
|
||||
|
||||
function listClass:Destroy()
|
||||
local l = self.List.Length
|
||||
for link, value in self:IterateForward() do
|
||||
self:Remove(value, true)
|
||||
end
|
||||
setmetatable(self, nil)
|
||||
table.clear(self)
|
||||
end
|
||||
|
||||
function linkClass:InsertAfter(value)
|
||||
assert(self.List.Links[value] == nil, "Value already exists")
|
||||
local link = {Value = value, List = self.List, Previous = self, Next = self.Next}
|
||||
self.List.Links[value] = link
|
||||
self.List.Length += 1
|
||||
self.Next.Previous = link
|
||||
self.Next = link
|
||||
return setmetatable(link, linkClass)
|
||||
end
|
||||
|
||||
function linkClass:InsertBefore(value)
|
||||
assert(self.List.Links[value] == nil, "Value already exists")
|
||||
local link = {Value = value, List = self.List, Next = self, Previous = self.Previous}
|
||||
self.List.Links[value] = link
|
||||
self.List.Length += 1
|
||||
self.Previous.Next = link
|
||||
self.Previous = link
|
||||
return setmetatable(link, linkClass)
|
||||
end
|
||||
|
||||
function linkClass:GetNext()
|
||||
local link = self.Next
|
||||
if link ~= link.List then return link end
|
||||
end
|
||||
|
||||
function linkClass:GetPrevious()
|
||||
local link = self.Previous
|
||||
if link ~= link.List then return link end
|
||||
end
|
||||
|
||||
function linkClass:Remove()
|
||||
assert(self.List ~= nil, "Link is not in a list")
|
||||
self.List.Links[self.Value] = nil
|
||||
self.List.Length -= 1
|
||||
self.List = nil
|
||||
self.Previous.Next = self.Next
|
||||
self.Next.Previous = self.Previous
|
||||
end
|
||||
|
||||
return listClass
|
101
src/Chemical/Packages/Packet/Signal.lua
Normal file
101
src/Chemical/Packages/Packet/Signal.lua
Normal file
|
@ -0,0 +1,101 @@
|
|||
--!strict
|
||||
|
||||
|
||||
-- Requires
|
||||
local Task = require(script.Parent.Task)
|
||||
|
||||
|
||||
-- Types
|
||||
export type Signal<A... = ()> = {
|
||||
Type: "Signal",
|
||||
Previous: Connection<A...>,
|
||||
Next: Connection<A...>,
|
||||
Fire: (self: Signal<A...>, A...) -> (),
|
||||
Connect: (self: Signal<A...>, func: (A...) -> ()) -> Connection<A...>,
|
||||
Once: (self: Signal<A...>, func: (A...) -> ()) -> Connection<A...>,
|
||||
Wait: (self: Signal<A...>) -> A...,
|
||||
}
|
||||
|
||||
export type Connection<A... = ()> = {
|
||||
Type: "Connection",
|
||||
Previous: Connection<A...>,
|
||||
Next: Connection<A...>,
|
||||
Once: boolean,
|
||||
Function: (player: Player, A...) -> (),
|
||||
Thread: thread,
|
||||
Disconnect: (self: Connection<A...>) -> (),
|
||||
}
|
||||
|
||||
|
||||
-- Varables
|
||||
local Signal = {} :: Signal<...any>
|
||||
local Connection = {} :: Connection<...any>
|
||||
|
||||
|
||||
-- Constructor
|
||||
local function Constructor<A...>()
|
||||
local signal = (setmetatable({}, Signal) :: any) :: Signal<A...>
|
||||
signal.Previous = signal :: any
|
||||
signal.Next = signal :: any
|
||||
return signal
|
||||
end
|
||||
|
||||
|
||||
-- Signal
|
||||
Signal["__index"] = Signal
|
||||
Signal.Type = "Signal"
|
||||
|
||||
function Signal:Connect(func)
|
||||
local connection = (setmetatable({}, Connection) :: any) :: Connection
|
||||
connection.Previous = self.Previous
|
||||
connection.Next = self :: any
|
||||
connection.Once = false
|
||||
connection.Function = func
|
||||
self.Previous.Next = connection
|
||||
self.Previous = connection
|
||||
return connection
|
||||
end
|
||||
|
||||
function Signal:Once(func)
|
||||
local connection = (setmetatable({}, Connection) :: any) :: Connection
|
||||
connection.Previous = self.Previous
|
||||
connection.Next = self :: any
|
||||
connection.Once = true
|
||||
connection.Function = func
|
||||
self.Previous.Next = connection
|
||||
self.Previous = connection
|
||||
return connection
|
||||
end
|
||||
|
||||
function Signal:Wait()
|
||||
local connection = (setmetatable({}, Connection) :: any) :: Connection
|
||||
connection.Previous = self.Previous
|
||||
connection.Next = self :: any
|
||||
connection.Once = true
|
||||
connection.Thread = coroutine.running()
|
||||
self.Previous.Next = connection
|
||||
self.Previous = connection
|
||||
return coroutine.yield()
|
||||
end
|
||||
|
||||
function Signal:Fire(...)
|
||||
local connection = self.Next
|
||||
while connection.Type == "Connection" do
|
||||
if connection.Function then Task:Defer(connection.Function, ...) else task.defer(connection.Thread, ...) end
|
||||
if connection.Once then connection.Previous.Next = connection.Next connection.Next.Previous = connection.Previous end
|
||||
connection = connection.Next
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Connection
|
||||
Connection["__index"] = Connection
|
||||
Connection.Type = "Connection"
|
||||
|
||||
function Connection:Disconnect()
|
||||
self.Previous.Next = self.Next
|
||||
self.Next.Previous = self.Previous
|
||||
end
|
||||
|
||||
|
||||
return Constructor
|
46
src/Chemical/Packages/Packet/Task.lua
Normal file
46
src/Chemical/Packages/Packet/Task.lua
Normal file
|
@ -0,0 +1,46 @@
|
|||
--!strict
|
||||
|
||||
|
||||
-- Types
|
||||
export type Task = {
|
||||
Type: "Task",
|
||||
Spawn: (self: Task, func: (...any) -> (), ...any) -> thread,
|
||||
Defer: (self: Task, func: (...any) -> (), ...any) -> thread,
|
||||
Delay: (self: Task, duration: number, func: (...any) -> (), ...any) -> thread,
|
||||
}
|
||||
|
||||
|
||||
-- Varables
|
||||
local Call, Thread
|
||||
local Task = {} :: Task
|
||||
local threads = {} :: {thread}
|
||||
|
||||
|
||||
-- Task
|
||||
Task.Type = "Task"
|
||||
|
||||
function Task:Spawn(func, ...)
|
||||
return task.spawn(table.remove(threads) or task.spawn(Thread), func, ...)
|
||||
end
|
||||
|
||||
function Task:Defer(func, ...)
|
||||
return task.defer(table.remove(threads) or task.spawn(Thread), func, ...)
|
||||
end
|
||||
|
||||
function Task:Delay(duration, func, ...)
|
||||
return task.delay(duration, table.remove(threads) or task.spawn(Thread), func, ...)
|
||||
end
|
||||
|
||||
|
||||
-- Functions
|
||||
function Call(func: (...any) -> (), ...)
|
||||
func(...)
|
||||
table.insert(threads, coroutine.running())
|
||||
end
|
||||
|
||||
function Thread()
|
||||
while true do Call(coroutine.yield()) end
|
||||
end
|
||||
|
||||
|
||||
return Task
|
6
src/Chemical/Packages/Packet/Types/Characters.lua
Normal file
6
src/Chemical/Packages/Packet/Types/Characters.lua
Normal file
|
@ -0,0 +1,6 @@
|
|||
return {[0] = -- Recommended character array lengths: 2, 4, 8, 16, 32, 64, 128, 256
|
||||
" ", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D",
|
||||
"E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
||||
}
|
12
src/Chemical/Packages/Packet/Types/Enums.lua
Normal file
12
src/Chemical/Packages/Packet/Types/Enums.lua
Normal file
|
@ -0,0 +1,12 @@
|
|||
return { -- Add any enum [Max: 255]
|
||||
Enum.AccessoryType,
|
||||
Enum.Axis,
|
||||
Enum.BodyPart,
|
||||
Enum.BodyPartR15,
|
||||
Enum.EasingDirection,
|
||||
Enum.EasingStyle,
|
||||
Enum.KeyCode,
|
||||
Enum.Material,
|
||||
Enum.NormalId,
|
||||
Enum.HumanoidStateType
|
||||
}
|
8
src/Chemical/Packages/Packet/Types/Static1.lua
Normal file
8
src/Chemical/Packages/Packet/Types/Static1.lua
Normal file
|
@ -0,0 +1,8 @@
|
|||
return {
|
||||
"DataStore Failed To Load",
|
||||
"Another Static String",
|
||||
math.pi,
|
||||
123456789,
|
||||
Vector3.new(1, 2, 3),
|
||||
"You can have upto 255 static values of any type"
|
||||
}
|
8
src/Chemical/Packages/Packet/Types/Static2.lua
Normal file
8
src/Chemical/Packages/Packet/Types/Static2.lua
Normal file
|
@ -0,0 +1,8 @@
|
|||
return {
|
||||
"DataStore Failed To Load",
|
||||
"Another Static String",
|
||||
math.pi,
|
||||
123456789,
|
||||
Vector3.new(1, 2, 3),
|
||||
"You can have upto 255 static values of any type"
|
||||
}
|
8
src/Chemical/Packages/Packet/Types/Static3.lua
Normal file
8
src/Chemical/Packages/Packet/Types/Static3.lua
Normal file
|
@ -0,0 +1,8 @@
|
|||
return {
|
||||
"DataStore Failed To Load",
|
||||
"Another Static String",
|
||||
math.pi,
|
||||
123456789,
|
||||
Vector3.new(1, 2, 3),
|
||||
"You can have upto 255 static values of any type"
|
||||
}
|
705
src/Chemical/Packages/Packet/Types/init.lua
Normal file
705
src/Chemical/Packages/Packet/Types/init.lua
Normal file
|
@ -0,0 +1,705 @@
|
|||
--!strict
|
||||
--!optimize 2
|
||||
|
||||
--[[
|
||||
S8 Minimum: -128 Maximum: 127
|
||||
S16 Minimum: -32768 Maximum: 32767
|
||||
S24 Minimum: -8388608 Maximum: 8388607
|
||||
S32 Minimum: -2147483648 Maximum: 2147483647
|
||||
|
||||
U8 Minimum: 0 Maximum: 255
|
||||
U16 Minimum: 0 Maximum: 65535
|
||||
U24 Minimum: 0 Maximum: 16777215
|
||||
U32 Minimum: 0 Maximum: 4294967295
|
||||
|
||||
F16 ±2048 [65520]
|
||||
F24 ±262144 [4294959104]
|
||||
F32 ±16777216 [170141183460469231731687303715884105728]
|
||||
F64 ±9007199254740992 [huge]
|
||||
]]
|
||||
|
||||
|
||||
-- Types
|
||||
export type Cursor = {
|
||||
Buffer: buffer,
|
||||
BufferLength: number,
|
||||
BufferOffset: number,
|
||||
Instances: {Instance},
|
||||
InstancesOffset: number,
|
||||
}
|
||||
|
||||
|
||||
-- Varables
|
||||
local activeCursor : Cursor
|
||||
local activeBuffer : buffer
|
||||
local bufferLength : number
|
||||
local bufferOffset : number
|
||||
local instances : {Instance}
|
||||
local instancesOffset : number
|
||||
local types = {}
|
||||
local reads = {}
|
||||
local writes = {}
|
||||
local anyReads = {} :: {[any]: () -> any}
|
||||
local anyWrites = {} :: {[any]: (any) -> ()}
|
||||
|
||||
|
||||
-- Functions
|
||||
local function Allocate(bytes: number)
|
||||
local targetLength = bufferOffset + bytes
|
||||
if bufferLength < targetLength then
|
||||
while bufferLength < targetLength do bufferLength *= 2 end
|
||||
local newBuffer = buffer.create(bufferLength)
|
||||
buffer.copy(newBuffer, 0, activeBuffer, 0, bufferOffset)
|
||||
activeCursor.Buffer = newBuffer
|
||||
activeBuffer = newBuffer
|
||||
end
|
||||
end
|
||||
|
||||
local function ReadS8(): number local value = buffer.readi8(activeBuffer, bufferOffset) bufferOffset += 1 return value end
|
||||
local function WriteS8(value: number) buffer.writei8(activeBuffer, bufferOffset, value) bufferOffset += 1 end
|
||||
local function ReadS16(): number local value = buffer.readi16(activeBuffer, bufferOffset) bufferOffset += 2 return value end
|
||||
local function WriteS16(value: number) buffer.writei16(activeBuffer, bufferOffset, value) bufferOffset += 2 end
|
||||
local function ReadS24(): number local value = buffer.readbits(activeBuffer, bufferOffset * 8, 24) - 8388608 bufferOffset += 3 return value end
|
||||
local function WriteS24(value: number) buffer.writebits(activeBuffer, bufferOffset * 8, 24, value + 8388608) bufferOffset += 3 end
|
||||
local function ReadS32(): number local value = buffer.readi32(activeBuffer, bufferOffset) bufferOffset += 4 return value end
|
||||
local function WriteS32(value: number) buffer.writei32(activeBuffer, bufferOffset, value) bufferOffset += 4 end
|
||||
local function ReadU8(): number local value = buffer.readu8(activeBuffer, bufferOffset) bufferOffset += 1 return value end
|
||||
local function WriteU8(value: number) buffer.writeu8(activeBuffer, bufferOffset, value) bufferOffset += 1 end
|
||||
local function ReadU16(): number local value = buffer.readu16(activeBuffer, bufferOffset) bufferOffset += 2 return value end
|
||||
local function WriteU16(value: number) buffer.writeu16(activeBuffer, bufferOffset, value) bufferOffset += 2 end
|
||||
local function ReadU24(): number local value = buffer.readbits(activeBuffer, bufferOffset * 8, 24) bufferOffset += 3 return value end
|
||||
local function WriteU24(value: number) buffer.writebits(activeBuffer, bufferOffset * 8, 24, value) bufferOffset += 3 end
|
||||
local function ReadU32(): number local value = buffer.readu32(activeBuffer, bufferOffset) bufferOffset += 4 return value end
|
||||
local function WriteU32(value: number) buffer.writeu32(activeBuffer, bufferOffset, value) bufferOffset += 4 end
|
||||
local function ReadF32(): number local value = buffer.readf32(activeBuffer, bufferOffset) bufferOffset += 4 return value end
|
||||
local function WriteF32(value: number) buffer.writef32(activeBuffer, bufferOffset, value) bufferOffset += 4 end
|
||||
local function ReadF64(): number local value = buffer.readf64(activeBuffer, bufferOffset) bufferOffset += 8 return value end
|
||||
local function WriteF64(value: number) buffer.writef64(activeBuffer, bufferOffset, value) bufferOffset += 8 end
|
||||
local function ReadString(length: number) local value = buffer.readstring(activeBuffer, bufferOffset, length) bufferOffset += length return value end
|
||||
local function WriteString(value: string) buffer.writestring(activeBuffer, bufferOffset, value) bufferOffset += #value end
|
||||
local function ReadBuffer(length: number) local value = buffer.create(length) buffer.copy(value, 0, activeBuffer, bufferOffset, length) bufferOffset += length return value end
|
||||
local function WriteBuffer(value: buffer) buffer.copy(activeBuffer, bufferOffset, value) bufferOffset += buffer.len(value) end
|
||||
local function ReadInstance() instancesOffset += 1 return instances[instancesOffset] end
|
||||
local function WriteInstance(value) instancesOffset += 1 instances[instancesOffset] = value end
|
||||
|
||||
local function ReadF16(): number
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 2
|
||||
local mantissa = buffer.readbits(activeBuffer, bitOffset + 0, 10)
|
||||
local exponent = buffer.readbits(activeBuffer, bitOffset + 10, 5)
|
||||
local sign = buffer.readbits(activeBuffer, bitOffset + 15, 1)
|
||||
if mantissa == 0b0000000000 then
|
||||
if exponent == 0b00000 then return 0 end
|
||||
if exponent == 0b11111 then return if sign == 0 then math.huge else -math.huge end
|
||||
elseif exponent == 0b11111 then return 0/0 end
|
||||
if sign == 0 then
|
||||
return (mantissa / 1024 + 1) * 2 ^ (exponent - 15)
|
||||
else
|
||||
return -(mantissa / 1024 + 1) * 2 ^ (exponent - 15)
|
||||
end
|
||||
end
|
||||
local function WriteF16(value: number)
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 2
|
||||
if value == 0 then
|
||||
buffer.writebits(activeBuffer, bitOffset, 16, 0b0_00000_0000000000)
|
||||
elseif value >= 65520 then
|
||||
buffer.writebits(activeBuffer, bitOffset, 16, 0b0_11111_0000000000)
|
||||
elseif value <= -65520 then
|
||||
buffer.writebits(activeBuffer, bitOffset, 16, 0b1_11111_0000000000)
|
||||
elseif value ~= value then
|
||||
buffer.writebits(activeBuffer, bitOffset, 16, 0b0_11111_0000000001)
|
||||
else
|
||||
local sign = 0
|
||||
if value < 0 then sign = 1 value = -value end
|
||||
local mantissa, exponent = math.frexp(value)
|
||||
buffer.writebits(activeBuffer, bitOffset + 0, 10, mantissa * 2048 - 1023.5)
|
||||
buffer.writebits(activeBuffer, bitOffset + 10, 5, exponent + 14)
|
||||
buffer.writebits(activeBuffer, bitOffset + 15, 1, sign)
|
||||
end
|
||||
end
|
||||
|
||||
local function ReadF24(): number
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 3
|
||||
local mantissa = buffer.readbits(activeBuffer, bitOffset + 0, 17)
|
||||
local exponent = buffer.readbits(activeBuffer, bitOffset + 17, 6)
|
||||
local sign = buffer.readbits(activeBuffer, bitOffset + 23, 1)
|
||||
if mantissa == 0b00000000000000000 then
|
||||
if exponent == 0b000000 then return 0 end
|
||||
if exponent == 0b111111 then return if sign == 0 then math.huge else -math.huge end
|
||||
elseif exponent == 0b111111 then return 0/0 end
|
||||
if sign == 0 then
|
||||
return (mantissa / 131072 + 1) * 2 ^ (exponent - 31)
|
||||
else
|
||||
return -(mantissa / 131072 + 1) * 2 ^ (exponent - 31)
|
||||
end
|
||||
end
|
||||
local function WriteF24(value: number)
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 3
|
||||
if value == 0 then
|
||||
buffer.writebits(activeBuffer, bitOffset, 24, 0b0_000000_00000000000000000)
|
||||
elseif value >= 4294959104 then
|
||||
buffer.writebits(activeBuffer, bitOffset, 24, 0b0_111111_00000000000000000)
|
||||
elseif value <= -4294959104 then
|
||||
buffer.writebits(activeBuffer, bitOffset, 24, 0b1_111111_00000000000000000)
|
||||
elseif value ~= value then
|
||||
buffer.writebits(activeBuffer, bitOffset, 24, 0b0_111111_00000000000000001)
|
||||
else
|
||||
local sign = 0
|
||||
if value < 0 then sign = 1 value = -value end
|
||||
local mantissa, exponent = math.frexp(value)
|
||||
buffer.writebits(activeBuffer, bitOffset + 0, 17, mantissa * 262144 - 131071.5)
|
||||
buffer.writebits(activeBuffer, bitOffset + 17, 6, exponent + 30)
|
||||
buffer.writebits(activeBuffer, bitOffset + 23, 1, sign)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Types
|
||||
types.Any = "Any" :: any
|
||||
reads.Any = function() return anyReads[ReadU8()]() end
|
||||
writes.Any = function(value: any) anyWrites[typeof(value)](value) end
|
||||
|
||||
types.Nil = ("Nil" :: any) :: nil
|
||||
reads.Nil = function() return nil end
|
||||
writes.Nil = function(value: nil) end
|
||||
|
||||
types.NumberS8 = ("NumberS8" :: any) :: number
|
||||
reads.NumberS8 = function() return ReadS8() end
|
||||
writes.NumberS8 = function(value: number) Allocate(1) WriteS8(value) end
|
||||
|
||||
types.NumberS16 = ("NumberS16" :: any) :: number
|
||||
reads.NumberS16 = function() return ReadS16() end
|
||||
writes.NumberS16 = function(value: number) Allocate(2) WriteS16(value) end
|
||||
|
||||
types.NumberS24 = ("NumberS24" :: any) :: number
|
||||
reads.NumberS24 = function() return ReadS24() end
|
||||
writes.NumberS24 = function(value: number) Allocate(3) WriteS24(value) end
|
||||
|
||||
types.NumberS32 = ("NumberS32" :: any) :: number
|
||||
reads.NumberS32 = function() return ReadS32() end
|
||||
writes.NumberS32 = function(value: number) Allocate(4) WriteS32(value) end
|
||||
|
||||
types.NumberU8 = ("NumberU8" :: any) :: number
|
||||
reads.NumberU8 = function() return ReadU8() end
|
||||
writes.NumberU8 = function(value: number) Allocate(1) WriteU8(value) end
|
||||
|
||||
types.NumberU16 = ("NumberU16" :: any) :: number
|
||||
reads.NumberU16 = function() return ReadU16() end
|
||||
writes.NumberU16 = function(value: number) Allocate(2) WriteU16(value) end
|
||||
|
||||
types.NumberU24 = ("NumberU24" :: any) :: number
|
||||
reads.NumberU24 = function() return ReadU24() end
|
||||
writes.NumberU24 = function(value: number) Allocate(3) WriteU24(value) end
|
||||
|
||||
types.NumberU32 = ("NumberU32" :: any) :: number
|
||||
reads.NumberU32 = function() return ReadU32() end
|
||||
writes.NumberU32 = function(value: number) Allocate(4) WriteU32(value) end
|
||||
|
||||
types.NumberF16 = ("NumberF16" :: any) :: number
|
||||
reads.NumberF16 = function() return ReadF16() end
|
||||
writes.NumberF16 = function(value: number) Allocate(2) WriteF16(value) end
|
||||
|
||||
types.NumberF24 = ("NumberF24" :: any) :: number
|
||||
reads.NumberF24 = function() return ReadF24() end
|
||||
writes.NumberF24 = function(value: number) Allocate(3) WriteF24(value) end
|
||||
|
||||
types.NumberF32 = ("NumberF32" :: any) :: number
|
||||
reads.NumberF32 = function() return ReadF32() end
|
||||
writes.NumberF32 = function(value: number) Allocate(4) WriteF32(value) end
|
||||
|
||||
types.NumberF64 = ("NumberF64" :: any) :: number
|
||||
reads.NumberF64 = function() return ReadF64() end
|
||||
writes.NumberF64 = function(value: number) Allocate(8) WriteF64(value) end
|
||||
|
||||
types.String = ("String" :: any) :: string
|
||||
reads.String = function() return ReadString(ReadU8()) end
|
||||
writes.String = function(value: string) local length = #value Allocate(1 + length) WriteU8(length) WriteString(value) end
|
||||
|
||||
types.StringLong = ("StringLong" :: any) :: string
|
||||
reads.StringLong = function() return ReadString(ReadU16()) end
|
||||
writes.StringLong = function(value: string) local length = #value Allocate(2 + length) WriteU16(length) WriteString(value) end
|
||||
|
||||
types.Buffer = ("Buffer" :: any) :: buffer
|
||||
reads.Buffer = function() return ReadBuffer(ReadU8()) end
|
||||
writes.Buffer = function(value: buffer) local length = buffer.len(value) Allocate(1 + length) WriteU8(length) WriteBuffer(value) end
|
||||
|
||||
types.BufferLong = ("BufferLong" :: any) :: buffer
|
||||
reads.BufferLong = function() return ReadBuffer(ReadU16()) end
|
||||
writes.BufferLong = function(value: buffer) local length = buffer.len(value) Allocate(2 + length) WriteU16(length) WriteBuffer(value) end
|
||||
|
||||
types.Instance = ("Instance" :: any) :: Instance
|
||||
reads.Instance = function() return ReadInstance() end
|
||||
writes.Instance = function(value: Instance) WriteInstance(value) end
|
||||
|
||||
types.Boolean8 = ("Boolean8" :: any) :: boolean
|
||||
reads.Boolean8 = function() return ReadU8() == 1 end
|
||||
writes.Boolean8 = function(value: boolean) Allocate(1) WriteU8(if value then 1 else 0) end
|
||||
|
||||
types.NumberRange = ("NumberRange" :: any) :: NumberRange
|
||||
reads.NumberRange = function() return NumberRange.new(ReadF32(), ReadF32()) end
|
||||
writes.NumberRange = function(value: NumberRange) Allocate(8) WriteF32(value.Min) WriteF32(value.Max) end
|
||||
|
||||
types.BrickColor = ("BrickColor" :: any) :: BrickColor
|
||||
reads.BrickColor = function() return BrickColor.new(ReadU16()) end
|
||||
writes.BrickColor = function(value: BrickColor) Allocate(2) WriteU16(value.Number) end
|
||||
|
||||
types.Color3 = ("Color3" :: any) :: Color3
|
||||
reads.Color3 = function() return Color3.fromRGB(ReadU8(), ReadU8(), ReadU8()) end
|
||||
writes.Color3 = function(value: Color3) Allocate(3) WriteU8(value.R * 255 + 0.5) WriteU8(value.G * 255 + 0.5) WriteU8(value.B * 255 + 0.5) end
|
||||
|
||||
types.UDim = ("UDim" :: any) :: UDim
|
||||
reads.UDim = function() return UDim.new(ReadS16() / 1000, ReadS16()) end
|
||||
writes.UDim = function(value: UDim) Allocate(4) WriteS16(value.Scale * 1000) WriteS16(value.Offset) end
|
||||
|
||||
types.UDim2 = ("UDim2" :: any) :: UDim2
|
||||
reads.UDim2 = function() return UDim2.new(ReadS16() / 1000, ReadS16(), ReadS16() / 1000, ReadS16()) end
|
||||
writes.UDim2 = function(value: UDim2) Allocate(8) WriteS16(value.X.Scale * 1000) WriteS16(value.X.Offset) WriteS16(value.Y.Scale * 1000) WriteS16(value.Y.Offset) end
|
||||
|
||||
types.Rect = ("Rect" :: any) :: Rect
|
||||
reads.Rect = function() return Rect.new(ReadF32(), ReadF32(), ReadF32(), ReadF32()) end
|
||||
writes.Rect = function(value: Rect) Allocate(16) WriteF32(value.Min.X) WriteF32(value.Min.Y) WriteF32(value.Max.X) WriteF32(value.Max.Y) end
|
||||
|
||||
types.Vector2S16 = ("Vector2S16" :: any) :: Vector2
|
||||
reads.Vector2S16 = function() return Vector2.new(ReadS16(), ReadS16()) end
|
||||
writes.Vector2S16 = function(value: Vector2) Allocate(4) WriteS16(value.X) WriteS16(value.Y) end
|
||||
|
||||
types.Vector2F24 = ("Vector2F24" :: any) :: Vector2
|
||||
reads.Vector2F24 = function() return Vector2.new(ReadF24(), ReadF24()) end
|
||||
writes.Vector2F24 = function(value: Vector2) Allocate(6) WriteF24(value.X) WriteF24(value.Y) end
|
||||
|
||||
types.Vector2F32 = ("Vector2F32" :: any) :: Vector2
|
||||
reads.Vector2F32 = function() return Vector2.new(ReadF32(), ReadF32()) end
|
||||
writes.Vector2F32 = function(value: Vector2) Allocate(8) WriteF32(value.X) WriteF32(value.Y) end
|
||||
|
||||
types.Vector3S16 = ("Vector3S16" :: any) :: Vector3
|
||||
reads.Vector3S16 = function() return Vector3.new(ReadS16(), ReadS16(), ReadS16()) end
|
||||
writes.Vector3S16 = function(value: Vector3) Allocate(6) WriteS16(value.X) WriteS16(value.Y) WriteS16(value.Z) end
|
||||
|
||||
types.Vector3F24 = ("Vector3F24" :: any) :: Vector3
|
||||
reads.Vector3F24 = function() return Vector3.new(ReadF24(), ReadF24(), ReadF24()) end
|
||||
writes.Vector3F24 = function(value: Vector3) Allocate(9) WriteF24(value.X) WriteF24(value.Y) WriteF24(value.Z) end
|
||||
|
||||
types.Vector3F32 = ("Vector3F32" :: any) :: Vector3
|
||||
reads.Vector3F32 = function() return Vector3.new(ReadF32(), ReadF32(), ReadF32()) end
|
||||
writes.Vector3F32 = function(value: Vector3) Allocate(12) WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z) end
|
||||
|
||||
types.NumberU4 = ("NumberU4" :: any) :: {number}
|
||||
reads.NumberU4 = function()
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 1
|
||||
return {
|
||||
buffer.readbits(activeBuffer, bitOffset + 0, 4),
|
||||
buffer.readbits(activeBuffer, bitOffset + 4, 4)
|
||||
}
|
||||
end
|
||||
writes.NumberU4 = function(value: {number})
|
||||
Allocate(1)
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 1
|
||||
buffer.writebits(activeBuffer, bitOffset + 0, 4, value[1])
|
||||
buffer.writebits(activeBuffer, bitOffset + 4, 4, value[2])
|
||||
end
|
||||
|
||||
types.BooleanNumber = ("BooleanNumber" :: any) :: {Boolean: boolean, Number: number}
|
||||
reads.BooleanNumber = function()
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 1
|
||||
return {
|
||||
Boolean = buffer.readbits(activeBuffer, bitOffset + 0, 1) == 1,
|
||||
Number = buffer.readbits(activeBuffer, bitOffset + 1, 7),
|
||||
}
|
||||
end
|
||||
writes.BooleanNumber = function(value: {Boolean: boolean, Number: number})
|
||||
Allocate(1)
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 1
|
||||
buffer.writebits(activeBuffer, bitOffset + 0, 1, if value.Boolean then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 1, 7, value.Number)
|
||||
end
|
||||
|
||||
types.Boolean1 = ("Boolean1" :: any) :: {boolean}
|
||||
reads.Boolean1 = function()
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 1
|
||||
return {
|
||||
buffer.readbits(activeBuffer, bitOffset + 0, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 1, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 2, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 3, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 4, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 5, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 6, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 7, 1) == 1,
|
||||
}
|
||||
end
|
||||
writes.Boolean1 = function(value: {boolean})
|
||||
Allocate(1)
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 1
|
||||
buffer.writebits(activeBuffer, bitOffset + 0, 1, if value[1] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 1, 1, if value[2] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 2, 1, if value[3] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 3, 1, if value[4] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 4, 1, if value[5] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 5, 1, if value[6] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 6, 1, if value[7] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 7, 1, if value[8] then 1 else 0)
|
||||
end
|
||||
|
||||
types.CFrameF24U8 = ("CFrameF24U8" :: any) :: CFrame
|
||||
reads.CFrameF24U8 = function()
|
||||
return CFrame.fromEulerAnglesXYZ(ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331)
|
||||
+ Vector3.new(ReadF24(), ReadF24(), ReadF24())
|
||||
end
|
||||
writes.CFrameF24U8 = function(value: CFrame)
|
||||
local rx, ry, rz = value:ToEulerAnglesXYZ()
|
||||
Allocate(12)
|
||||
WriteU8(rx * 40.58451048843331 + 0.5) WriteU8(ry * 40.58451048843331 + 0.5) WriteU8(rz * 40.58451048843331 + 0.5)
|
||||
WriteF24(value.X) WriteF24(value.Y) WriteF24(value.Z)
|
||||
end
|
||||
|
||||
types.CFrameF32U8 = ("CFrameF32U8" :: any) :: CFrame
|
||||
reads.CFrameF32U8 = function()
|
||||
return CFrame.fromEulerAnglesXYZ(ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331)
|
||||
+ Vector3.new(ReadF32(), ReadF32(), ReadF32())
|
||||
end
|
||||
writes.CFrameF32U8 = function(value: CFrame)
|
||||
local rx, ry, rz = value:ToEulerAnglesXYZ()
|
||||
Allocate(15)
|
||||
WriteU8(rx * 40.58451048843331 + 0.5) WriteU8(ry * 40.58451048843331 + 0.5) WriteU8(rz * 40.58451048843331 + 0.5)
|
||||
WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z)
|
||||
end
|
||||
|
||||
types.CFrameF32U16 = ("CFrameF32U16" :: any) :: CFrame
|
||||
reads.CFrameF32U16 = function()
|
||||
return CFrame.fromEulerAnglesXYZ(ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361)
|
||||
+ Vector3.new(ReadF32(), ReadF32(), ReadF32())
|
||||
end
|
||||
writes.CFrameF32U16 = function(value: CFrame)
|
||||
local rx, ry, rz = value:ToEulerAnglesXYZ()
|
||||
Allocate(18)
|
||||
WriteU16(rx * 10430.219195527361 + 0.5) WriteU16(ry * 10430.219195527361 + 0.5) WriteU16(rz * 10430.219195527361 + 0.5)
|
||||
WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z)
|
||||
end
|
||||
|
||||
types.Region3 = ("Region3" :: any) :: Region3
|
||||
reads.Region3 = function()
|
||||
return Region3.new(
|
||||
Vector3.new(ReadF32(), ReadF32(), ReadF32()),
|
||||
Vector3.new(ReadF32(), ReadF32(), ReadF32())
|
||||
)
|
||||
end
|
||||
writes.Region3 = function(value: Region3)
|
||||
local halfSize = value.Size / 2
|
||||
local minimum = value.CFrame.Position - halfSize
|
||||
local maximum = value.CFrame.Position + halfSize
|
||||
Allocate(24)
|
||||
WriteF32(minimum.X) WriteF32(minimum.Y) WriteF32(minimum.Z)
|
||||
WriteF32(maximum.X) WriteF32(maximum.Y) WriteF32(maximum.Z)
|
||||
end
|
||||
|
||||
types.NumberSequence = ("NumberSequence" :: any) :: NumberSequence
|
||||
reads.NumberSequence = function()
|
||||
local length = ReadU8()
|
||||
local keypoints = table.create(length)
|
||||
for index = 1, length do
|
||||
table.insert(keypoints, NumberSequenceKeypoint.new(ReadU8() / 255, ReadU8() / 255, ReadU8() / 255))
|
||||
end
|
||||
return NumberSequence.new(keypoints)
|
||||
end
|
||||
writes.NumberSequence = function(value: NumberSequence)
|
||||
local length = #value.Keypoints
|
||||
Allocate(1 + length * 3)
|
||||
WriteU8(length)
|
||||
for index, keypoint in value.Keypoints do
|
||||
WriteU8(keypoint.Time * 255 + 0.5) WriteU8(keypoint.Value * 255 + 0.5) WriteU8(keypoint.Envelope * 255 + 0.5)
|
||||
end
|
||||
end
|
||||
|
||||
types.ColorSequence = ("ColorSequence" :: any) :: ColorSequence
|
||||
reads.ColorSequence = function()
|
||||
local length = ReadU8()
|
||||
local keypoints = table.create(length)
|
||||
for index = 1, length do
|
||||
table.insert(keypoints, ColorSequenceKeypoint.new(ReadU8() / 255, Color3.fromRGB(ReadU8(), ReadU8(), ReadU8())))
|
||||
end
|
||||
return ColorSequence.new(keypoints)
|
||||
end
|
||||
writes.ColorSequence = function(value: ColorSequence)
|
||||
local length = #value.Keypoints
|
||||
Allocate(1 + length * 4)
|
||||
WriteU8(length)
|
||||
for index, keypoint in value.Keypoints do
|
||||
WriteU8(keypoint.Time * 255 + 0.5)
|
||||
WriteU8(keypoint.Value.R * 255 + 0.5) WriteU8(keypoint.Value.G * 255 + 0.5) WriteU8(keypoint.Value.B * 255 + 0.5)
|
||||
end
|
||||
end
|
||||
|
||||
local characterIndices = {}
|
||||
local characters = require(script.Characters)
|
||||
for index, value in characters do characterIndices[value] = index end
|
||||
local characterBits = math.ceil(math.log(#characters + 1, 2))
|
||||
local characterBytes = characterBits / 8
|
||||
types.Characters = ("Characters" :: any) :: string
|
||||
reads.Characters = function()
|
||||
local length = ReadU8()
|
||||
local characterArray = table.create(length)
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += math.ceil(length * characterBytes)
|
||||
for index = 1, length do
|
||||
table.insert(characterArray, characters[buffer.readbits(activeBuffer, bitOffset, characterBits)])
|
||||
bitOffset += characterBits
|
||||
end
|
||||
return table.concat(characterArray)
|
||||
end
|
||||
writes.Characters = function(value: string)
|
||||
local length = #value
|
||||
local bytes = math.ceil(length * characterBytes)
|
||||
Allocate(1 + bytes)
|
||||
WriteU8(length)
|
||||
local bitOffset = bufferOffset * 8
|
||||
for index = 1, length do
|
||||
buffer.writebits(activeBuffer, bitOffset, characterBits, characterIndices[value:sub(index, index)])
|
||||
bitOffset += characterBits
|
||||
end
|
||||
bufferOffset += bytes
|
||||
end
|
||||
|
||||
local enumIndices = {}
|
||||
local enums = require(script.Enums)
|
||||
for index, static in enums do enumIndices[static] = index end
|
||||
types.EnumItem = ("EnumItem" :: any) :: EnumItem
|
||||
reads.EnumItem = function() return enums[ReadU8()]:FromValue(ReadU16()) end
|
||||
writes.EnumItem = function(value: EnumItem) Allocate(3) WriteU8(enumIndices[value.EnumType]) WriteU16(value.Value) end
|
||||
|
||||
local staticIndices = {}
|
||||
local statics = require(script.Static1)
|
||||
for index, static in statics do staticIndices[static] = index end
|
||||
types.Static1 = ("Static1" :: any) :: any
|
||||
reads.Static1 = function() return statics[ReadU8()] end
|
||||
writes.Static1 = function(value: any) Allocate(1) WriteU8(staticIndices[value] or 0) end
|
||||
|
||||
local staticIndices = {}
|
||||
local statics = require(script.Static2)
|
||||
for index, static in statics do staticIndices[static] = index end
|
||||
types.Static2 = ("Static2" :: any) :: any
|
||||
reads.Static2 = function() return statics[ReadU8()] end
|
||||
writes.Static2 = function(value: any) Allocate(1) WriteU8(staticIndices[value] or 0) end
|
||||
|
||||
local staticIndices = {}
|
||||
local statics = require(script.Static3)
|
||||
for index, static in statics do staticIndices[static] = index end
|
||||
types.Static3 = ("Static3" :: any) :: any
|
||||
reads.Static3 = function() return statics[ReadU8()] end
|
||||
writes.Static3 = function(value: any) Allocate(1) WriteU8(staticIndices[value] or 0) end
|
||||
|
||||
|
||||
-- Any Types
|
||||
anyReads[0] = function() return nil end
|
||||
anyWrites["nil"] = function(value: nil) Allocate(1) WriteU8(0) end
|
||||
|
||||
anyReads[1] = function() return -ReadU8() end
|
||||
anyReads[2] = function() return -ReadU16() end
|
||||
anyReads[3] = function() return -ReadU24() end
|
||||
anyReads[4] = function() return -ReadU32() end
|
||||
anyReads[5] = function() return ReadU8() end
|
||||
anyReads[6] = function() return ReadU16() end
|
||||
anyReads[7] = function() return ReadU24() end
|
||||
anyReads[8] = function() return ReadU32() end
|
||||
anyReads[9] = function() return ReadF32() end
|
||||
anyReads[10] = function() return ReadF64() end
|
||||
anyWrites.number = function(value: number)
|
||||
if value % 1 == 0 then
|
||||
if value < 0 then
|
||||
if value > -256 then
|
||||
Allocate(2) WriteU8(1) WriteU8(-value)
|
||||
elseif value > -65536 then
|
||||
Allocate(3) WriteU8(2) WriteU16(-value)
|
||||
elseif value > -16777216 then
|
||||
Allocate(4) WriteU8(3) WriteU24(-value)
|
||||
elseif value > -4294967296 then
|
||||
Allocate(5) WriteU8(4) WriteU32(-value)
|
||||
else
|
||||
Allocate(9) WriteU8(10) WriteF64(value)
|
||||
end
|
||||
else
|
||||
if value < 256 then
|
||||
Allocate(2) WriteU8(5) WriteU8(value)
|
||||
elseif value < 65536 then
|
||||
Allocate(3) WriteU8(6) WriteU16(value)
|
||||
elseif value < 16777216 then
|
||||
Allocate(4) WriteU8(7) WriteU24(value)
|
||||
elseif value < 4294967296 then
|
||||
Allocate(5) WriteU8(8) WriteU32(value)
|
||||
else
|
||||
Allocate(9) WriteU8(10) WriteF64(value)
|
||||
end
|
||||
end
|
||||
elseif value > -1048576 and value < 1048576 then
|
||||
Allocate(5) WriteU8(9) WriteF32(value)
|
||||
else
|
||||
Allocate(9) WriteU8(10) WriteF64(value)
|
||||
end
|
||||
end
|
||||
|
||||
anyReads[11] = function() return ReadString(ReadU8()) end
|
||||
anyWrites.string = function(value: string) local length = #value Allocate(2 + length) WriteU8(11) WriteU8(length) WriteString(value) end
|
||||
|
||||
anyReads[12] = function() return ReadBuffer(ReadU8()) end
|
||||
anyWrites.buffer = function(value: buffer) local length = buffer.len(value) Allocate(2 + length) WriteU8(12) WriteU8(length) WriteBuffer(value) end
|
||||
|
||||
anyReads[13] = function() return ReadInstance() end
|
||||
anyWrites.Instance = function(value: Instance) Allocate(1) WriteU8(13) WriteInstance(value) end
|
||||
|
||||
anyReads[14] = function() return ReadU8() == 1 end
|
||||
anyWrites.boolean = function(value: boolean) Allocate(2) WriteU8(14) WriteU8(if value then 1 else 0) end
|
||||
|
||||
anyReads[15] = function() return NumberRange.new(ReadF32(), ReadF32()) end
|
||||
anyWrites.NumberRange = function(value: NumberRange) Allocate(9) WriteU8(15) WriteF32(value.Min) WriteF32(value.Max) end
|
||||
|
||||
anyReads[16] = function() return BrickColor.new(ReadU16()) end
|
||||
anyWrites.BrickColor = function(value: BrickColor) Allocate(3) WriteU8(16) WriteU16(value.Number) end
|
||||
|
||||
anyReads[17] = function() return Color3.fromRGB(ReadU8(), ReadU8(), ReadU8()) end
|
||||
anyWrites.Color3 = function(value: Color3) Allocate(4) WriteU8(17) WriteU8(value.R * 255 + 0.5) WriteU8(value.G * 255 + 0.5) WriteU8(value.B * 255 + 0.5) end
|
||||
|
||||
anyReads[18] = function() return UDim.new(ReadS16() / 1000, ReadS16()) end
|
||||
anyWrites.UDim = function(value: UDim) Allocate(5) WriteU8(18) WriteS16(value.Scale * 1000) WriteS16(value.Offset) end
|
||||
|
||||
anyReads[19] = function() return UDim2.new(ReadS16() / 1000, ReadS16(), ReadS16() / 1000, ReadS16()) end
|
||||
anyWrites.UDim2 = function(value: UDim2) Allocate(9) WriteU8(19) WriteS16(value.X.Scale * 1000) WriteS16(value.X.Offset) WriteS16(value.Y.Scale * 1000) WriteS16(value.Y.Offset) end
|
||||
|
||||
anyReads[20] = function() return Rect.new(ReadF32(), ReadF32(), ReadF32(), ReadF32()) end
|
||||
anyWrites.Rect = function(value: Rect) Allocate(17) WriteU8(20) WriteF32(value.Min.X) WriteF32(value.Min.Y) WriteF32(value.Max.X) WriteF32(value.Max.Y) end
|
||||
|
||||
anyReads[21] = function() return Vector2.new(ReadF32(), ReadF32()) end
|
||||
anyWrites.Vector2 = function(value: Vector2) Allocate(9) WriteU8(21) WriteF32(value.X) WriteF32(value.Y) end
|
||||
|
||||
anyReads[22] = function() return Vector3.new(ReadF32(), ReadF32(), ReadF32()) end
|
||||
anyWrites.Vector3 = function(value: Vector3) Allocate(13) WriteU8(22) WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z) end
|
||||
|
||||
anyReads[23] = function()
|
||||
return CFrame.fromEulerAnglesXYZ(ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361)
|
||||
+ Vector3.new(ReadF32(), ReadF32(), ReadF32())
|
||||
end
|
||||
anyWrites.CFrame = function(value: CFrame)
|
||||
local rx, ry, rz = value:ToEulerAnglesXYZ()
|
||||
Allocate(19)
|
||||
WriteU8(23)
|
||||
WriteU16(rx * 10430.219195527361 + 0.5) WriteU16(ry * 10430.219195527361 + 0.5) WriteU16(rz * 10430.219195527361 + 0.5)
|
||||
WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z)
|
||||
end
|
||||
|
||||
anyReads[24] = function()
|
||||
return Region3.new(
|
||||
Vector3.new(ReadF32(), ReadF32(), ReadF32()),
|
||||
Vector3.new(ReadF32(), ReadF32(), ReadF32())
|
||||
)
|
||||
end
|
||||
anyWrites.Region3 = function(value: Region3)
|
||||
local halfSize = value.Size / 2
|
||||
local minimum = value.CFrame.Position - halfSize
|
||||
local maximum = value.CFrame.Position + halfSize
|
||||
Allocate(25)
|
||||
WriteU8(24)
|
||||
WriteF32(minimum.X) WriteF32(minimum.Y) WriteF32(minimum.Z)
|
||||
WriteF32(maximum.X) WriteF32(maximum.Y) WriteF32(maximum.Z)
|
||||
end
|
||||
|
||||
anyReads[25] = function()
|
||||
local length = ReadU8()
|
||||
local keypoints = table.create(length)
|
||||
for index = 1, length do
|
||||
table.insert(keypoints, NumberSequenceKeypoint.new(ReadU8() / 255, ReadU8() / 255, ReadU8() / 255))
|
||||
end
|
||||
return NumberSequence.new(keypoints)
|
||||
end
|
||||
anyWrites.NumberSequence = function(value: NumberSequence)
|
||||
local length = #value.Keypoints
|
||||
Allocate(2 + length * 3)
|
||||
WriteU8(25)
|
||||
WriteU8(length)
|
||||
for index, keypoint in value.Keypoints do
|
||||
WriteU8(keypoint.Time * 255 + 0.5) WriteU8(keypoint.Value * 255 + 0.5) WriteU8(keypoint.Envelope * 255 + 0.5)
|
||||
end
|
||||
end
|
||||
|
||||
anyReads[26] = function()
|
||||
local length = ReadU8()
|
||||
local keypoints = table.create(length)
|
||||
for index = 1, length do
|
||||
table.insert(keypoints, ColorSequenceKeypoint.new(ReadU8() / 255, Color3.fromRGB(ReadU8(), ReadU8(), ReadU8())))
|
||||
end
|
||||
return ColorSequence.new(keypoints)
|
||||
end
|
||||
anyWrites.ColorSequence = function(value: ColorSequence)
|
||||
local length = #value.Keypoints
|
||||
Allocate(2 + length * 4)
|
||||
WriteU8(26)
|
||||
WriteU8(length)
|
||||
for index, keypoint in value.Keypoints do
|
||||
WriteU8(keypoint.Time * 255 + 0.5)
|
||||
WriteU8(keypoint.Value.R * 255 + 0.5) WriteU8(keypoint.Value.G * 255 + 0.5) WriteU8(keypoint.Value.B * 255 + 0.5)
|
||||
end
|
||||
end
|
||||
|
||||
anyReads[27] = function()
|
||||
return enums[ReadU8()]:FromValue(ReadU16())
|
||||
end
|
||||
anyWrites.EnumItem = function(value: EnumItem)
|
||||
Allocate(4)
|
||||
WriteU8(27)
|
||||
WriteU8(enumIndices[value.EnumType])
|
||||
WriteU16(value.Value)
|
||||
end
|
||||
|
||||
anyReads[28] = function()
|
||||
local value = {}
|
||||
while true do
|
||||
local typeId = ReadU8()
|
||||
if typeId == 0 then return value else value[anyReads[typeId]()] = anyReads[ReadU8()]() end
|
||||
end
|
||||
end
|
||||
anyWrites.table = function(value: {[any]: any})
|
||||
Allocate(1)
|
||||
WriteU8(28)
|
||||
for index, value in value do anyWrites[typeof(index)](index) anyWrites[typeof(value)](value) end
|
||||
Allocate(1)
|
||||
WriteU8(0)
|
||||
end
|
||||
|
||||
|
||||
return {
|
||||
Import = function(cursor: Cursor)
|
||||
activeCursor = cursor
|
||||
activeBuffer = cursor.Buffer
|
||||
bufferLength = cursor.BufferLength
|
||||
bufferOffset = cursor.BufferOffset
|
||||
instances = cursor.Instances
|
||||
instancesOffset = cursor.InstancesOffset
|
||||
end,
|
||||
|
||||
Export = function()
|
||||
activeCursor.BufferLength = bufferLength
|
||||
activeCursor.BufferOffset = bufferOffset
|
||||
activeCursor.InstancesOffset = instancesOffset
|
||||
return activeCursor
|
||||
end,
|
||||
|
||||
Truncate = function()
|
||||
local truncatedBuffer = buffer.create(bufferOffset)
|
||||
buffer.copy(truncatedBuffer, 0, activeBuffer, 0, bufferOffset)
|
||||
if instancesOffset == 0 then return truncatedBuffer else return truncatedBuffer, instances end
|
||||
end,
|
||||
|
||||
Ended = function()
|
||||
return bufferOffset >= bufferLength
|
||||
end,
|
||||
|
||||
Types = types,
|
||||
Reads = reads,
|
||||
Writes = writes,
|
||||
}
|
368
src/Chemical/Packages/Packet/init.lua
Normal file
368
src/Chemical/Packages/Packet/init.lua
Normal file
|
@ -0,0 +1,368 @@
|
|||
--!strict
|
||||
|
||||
|
||||
-- Requires
|
||||
local Signal = require(script.Signal)
|
||||
local Task = require(script.Task)
|
||||
local Types = require(script.Types)
|
||||
|
||||
|
||||
-- Types
|
||||
export type Packet<A... = (), B... = ()> = {
|
||||
Type: "Packet",
|
||||
Id: number,
|
||||
Name: string,
|
||||
Reads: {() -> any},
|
||||
Writes: {(any) -> ()},
|
||||
ResponseTimeout: number,
|
||||
ResponseTimeoutValue: any,
|
||||
ResponseReads: {() -> any},
|
||||
ResponseWrites: {(any) -> ()},
|
||||
OnServerEvent: Signal.Signal<(Player, A...)>,
|
||||
OnClientEvent: Signal.Signal<A...>,
|
||||
OnServerInvoke: nil | (player: Player, A...) -> B...,
|
||||
OnClientInvoke: nil | (A...) -> B...,
|
||||
Response: (self: Packet<A..., B...>, B...) -> Packet<A..., B...>,
|
||||
Fire: (self: Packet<A..., B...>, A...) -> B...,
|
||||
FireClient: (self: Packet<A..., B...>, player: Player, A...) -> B...,
|
||||
Serialize: (self: Packet<A..., B...>, A...) -> (buffer, {Instance}?),
|
||||
Deserialize: (self: Packet<A..., B...>, serializeBuffer: buffer, instances: {Instance}?) -> A...,
|
||||
}
|
||||
|
||||
|
||||
-- Varables
|
||||
local ParametersToFunctions, TableToFunctions, ReadParameters, WriteParameters, Timeout
|
||||
local RunService = game:GetService("RunService")
|
||||
local PlayersService = game:GetService("Players")
|
||||
local reads, writes, Import, Export, Truncate, Ended = Types.Reads, Types.Writes, Types.Import, Types.Export, Types.Truncate, Types.Ended
|
||||
local ReadU8, WriteU8, ReadU16, WriteU16 = reads.NumberU8, writes.NumberU8, reads.NumberU16, writes.NumberU16
|
||||
local Packet = {} :: Packet<...any, ...any>
|
||||
local packets = {} :: {[string | number]: Packet<...any, ...any>}
|
||||
local playerCursors : {[Player]: Types.Cursor}
|
||||
local playerThreads : {[Player]: {[number]: {Yielded: thread, Timeout: thread}, Index: number}}
|
||||
local threads : {[number]: {Yielded: thread, Timeout: thread}, Index: number}
|
||||
local remoteEvent : RemoteEvent
|
||||
local packetCounter : number
|
||||
local cursor = {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0}
|
||||
|
||||
|
||||
-- Constructor
|
||||
local function Constructor<A..., B...>(_, name: string, ...: A...)
|
||||
local packet = packets[name] :: Packet<A..., B...>
|
||||
if packet then return packet end
|
||||
local packet = (setmetatable({}, Packet) :: any) :: Packet<A..., B...>
|
||||
packet.Name = name
|
||||
if RunService:IsServer() then
|
||||
packet.Id = packetCounter
|
||||
packet.OnServerEvent = Signal() :: Signal.Signal<(Player, A...)>
|
||||
remoteEvent:SetAttribute(name, packetCounter)
|
||||
packets[packetCounter] = packet
|
||||
packetCounter += 1
|
||||
else
|
||||
packet.Id = remoteEvent:GetAttribute(name)
|
||||
packet.OnClientEvent = Signal() :: Signal.Signal<A...>
|
||||
if packet.Id then packets[packet.Id] = packet end
|
||||
end
|
||||
packet.Reads, packet.Writes = ParametersToFunctions(table.pack(...))
|
||||
packets[packet.Name] = packet
|
||||
return packet
|
||||
end
|
||||
|
||||
|
||||
-- Packet
|
||||
Packet["__index"] = Packet
|
||||
Packet.Type = "Packet"
|
||||
|
||||
function Packet:Response(...)
|
||||
self.ResponseTimeout = self.ResponseTimeout or 10
|
||||
self.ResponseReads, self.ResponseWrites = ParametersToFunctions(table.pack(...))
|
||||
return self
|
||||
end
|
||||
|
||||
function Packet:Fire(...)
|
||||
Import(cursor)
|
||||
WriteU8(self.Id)
|
||||
if self.ResponseReads then
|
||||
WriteU8(threads.Index)
|
||||
threads[threads.Index] = {Yielded = coroutine.running(), Timeout = Task:Delay(self.ResponseTimeout, Timeout, coroutine.running(), self.ResponseTimeoutValue)}
|
||||
threads.Index = (threads.Index + 1) % 128
|
||||
WriteParameters(self.Writes, {...})
|
||||
cursor = Export()
|
||||
return coroutine.yield()
|
||||
else
|
||||
WriteParameters(self.Writes, {...})
|
||||
cursor = Export()
|
||||
end
|
||||
end
|
||||
|
||||
function Packet:FireClient(player, ...)
|
||||
if player.Parent == nil then return end
|
||||
Import(playerCursors[player] or {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0})
|
||||
WriteU8(self.Id)
|
||||
if self.ResponseReads then
|
||||
local threads = playerThreads[player]
|
||||
if threads == nil then threads = {Index = 0} playerThreads[player] = threads end
|
||||
WriteU8(threads.Index)
|
||||
threads[threads.Index] = {Yielded = coroutine.running(), Timeout = Task:Delay(self.ResponseTimeout, Timeout, coroutine.running(), self.ResponseTimeoutValue)}
|
||||
threads.Index = (threads.Index + 1) % 128
|
||||
WriteParameters(self.Writes, {...})
|
||||
playerCursors[player] = Export()
|
||||
return coroutine.yield()
|
||||
else
|
||||
WriteParameters(self.Writes, {...})
|
||||
playerCursors[player] = Export()
|
||||
end
|
||||
end
|
||||
|
||||
function Packet:Serialize(...)
|
||||
Import({Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0})
|
||||
WriteParameters(self.Writes, {...})
|
||||
return Truncate()
|
||||
end
|
||||
|
||||
function Packet:Deserialize(serializeBuffer, instances)
|
||||
Import({Buffer = serializeBuffer, BufferLength = buffer.len(serializeBuffer), BufferOffset = 0, Instances = instances or {}, InstancesOffset = 0})
|
||||
return ReadParameters(self.Reads)
|
||||
end
|
||||
|
||||
|
||||
-- Functions
|
||||
function ParametersToFunctions(parameters: {any})
|
||||
local readFunctions, writeFunctions = table.create(#parameters), table.create(#parameters)
|
||||
for index, parameter in ipairs(parameters) do
|
||||
if type(parameter) == "table" then
|
||||
readFunctions[index], writeFunctions[index] = TableToFunctions(parameter)
|
||||
else
|
||||
readFunctions[index], writeFunctions[index] = reads[parameter], writes[parameter]
|
||||
end
|
||||
end
|
||||
return readFunctions, writeFunctions
|
||||
end
|
||||
|
||||
function TableToFunctions(parameters: {any})
|
||||
if #parameters == 1 then
|
||||
local parameter = parameters[1]
|
||||
local ReadFunction, WriteFunction
|
||||
if type(parameter) == "table" then
|
||||
ReadFunction, WriteFunction = TableToFunctions(parameter)
|
||||
else
|
||||
ReadFunction, WriteFunction = reads[parameter], writes[parameter]
|
||||
end
|
||||
local Read = function()
|
||||
local length = ReadU16()
|
||||
local values = table.create(length)
|
||||
for index = 1, length do values[index] = ReadFunction() end
|
||||
return values
|
||||
end
|
||||
local Write = function(values: {any})
|
||||
WriteU16(#values)
|
||||
for index, value in values do WriteFunction(value) end
|
||||
end
|
||||
return Read, Write
|
||||
else
|
||||
local keys = {} for key, value in parameters do table.insert(keys, key) end table.sort(keys)
|
||||
local readFunctions, writeFunctions = table.create(#keys), table.create(#keys)
|
||||
for index, key in keys do
|
||||
local parameter = parameters[key]
|
||||
if type(parameter) == "table" then
|
||||
readFunctions[index], writeFunctions[index] = TableToFunctions(parameter)
|
||||
else
|
||||
readFunctions[index], writeFunctions[index] = reads[parameter], writes[parameter]
|
||||
end
|
||||
end
|
||||
local Read = function()
|
||||
local values = {}
|
||||
for index, ReadFunction in readFunctions do values[keys[index]] = ReadFunction() end
|
||||
return values
|
||||
end
|
||||
local Write = function(values: {[any]: any})
|
||||
for index, WriteFunction in writeFunctions do WriteFunction(values[keys[index]]) end
|
||||
end
|
||||
return Read, Write
|
||||
end
|
||||
end
|
||||
|
||||
function ReadParameters(reads: {() -> any})
|
||||
local values = table.create(#reads)
|
||||
for index, func in reads do values[index] = func() end
|
||||
return table.unpack(values)
|
||||
end
|
||||
|
||||
function WriteParameters(writes: {(any) -> ()}, values: {any})
|
||||
for index, func in writes do func(values[index]) end
|
||||
end
|
||||
|
||||
function Timeout(thread: thread, value: any)
|
||||
task.defer(thread, value)
|
||||
end
|
||||
|
||||
|
||||
-- Initialize
|
||||
if RunService:IsServer() then
|
||||
playerCursors = {}
|
||||
playerThreads = {}
|
||||
packetCounter = 0
|
||||
remoteEvent = Instance.new("RemoteEvent", script)
|
||||
|
||||
local playerBytes = {}
|
||||
|
||||
local thread = task.spawn(function()
|
||||
while true do
|
||||
coroutine.yield()
|
||||
if cursor.BufferOffset > 0 then
|
||||
local truncatedBuffer = buffer.create(cursor.BufferOffset)
|
||||
buffer.copy(truncatedBuffer, 0, cursor.Buffer, 0, cursor.BufferOffset)
|
||||
if cursor.InstancesOffset == 0 then
|
||||
remoteEvent:FireAllClients(truncatedBuffer)
|
||||
else
|
||||
remoteEvent:FireAllClients(truncatedBuffer, cursor.Instances)
|
||||
cursor.InstancesOffset = 0
|
||||
table.clear(cursor.Instances)
|
||||
end
|
||||
cursor.BufferOffset = 0
|
||||
end
|
||||
for player, cursor in playerCursors do
|
||||
local truncatedBuffer = buffer.create(cursor.BufferOffset)
|
||||
buffer.copy(truncatedBuffer, 0, cursor.Buffer, 0, cursor.BufferOffset)
|
||||
if cursor.InstancesOffset == 0 then
|
||||
remoteEvent:FireClient(player, truncatedBuffer)
|
||||
else
|
||||
remoteEvent:FireClient(player, truncatedBuffer, cursor.Instances)
|
||||
end
|
||||
end
|
||||
table.clear(playerCursors)
|
||||
table.clear(playerBytes)
|
||||
end
|
||||
end)
|
||||
|
||||
local respond = function(packet: Packet, player: Player, threadIndex: number, ...)
|
||||
if packet.OnServerInvoke == nil then if RunService:IsStudio() then warn("OnServerInvoke not found for packet:", packet.Name, "discarding event:", ...) end return end
|
||||
local values = {packet.OnServerInvoke(player, ...)}
|
||||
if player.Parent == nil then return end
|
||||
Import(playerCursors[player] or {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0})
|
||||
WriteU8(packet.Id)
|
||||
WriteU8(threadIndex + 128)
|
||||
WriteParameters(packet.ResponseWrites, values)
|
||||
playerCursors[player] = Export()
|
||||
end
|
||||
|
||||
local onServerEvent = function(player: Player, receivedBuffer: buffer, instances: {Instance}?)
|
||||
local bytes = (playerBytes[player] or 0) + math.max(buffer.len(receivedBuffer), 800)
|
||||
if bytes > 8_000 then if RunService:IsStudio() then warn(player.Name, "is exceeding the data/rate limit; some events may be dropped") end return end
|
||||
playerBytes[player] = bytes
|
||||
Import({Buffer = receivedBuffer, BufferLength = buffer.len(receivedBuffer), BufferOffset = 0, Instances = instances or {}, InstancesOffset = 0})
|
||||
while Ended() == false do
|
||||
local packet = packets[ReadU8()]
|
||||
if packet.ResponseReads then
|
||||
local threadIndex = ReadU8()
|
||||
if threadIndex < 128 then
|
||||
Task:Defer(respond, packet, player, threadIndex, ReadParameters(packet.Reads))
|
||||
else
|
||||
threadIndex -= 128
|
||||
local threads = playerThreads[player][threadIndex]
|
||||
if threads then
|
||||
task.cancel(threads.Timeout)
|
||||
task.defer(threads.Yielded, ReadParameters(packet.ResponseReads))
|
||||
playerThreads[player][threadIndex] = nil
|
||||
elseif RunService:IsStudio() then
|
||||
warn("Response thread not found for packet:", packet.Name, "discarding response:", ReadParameters(packet.ResponseReads))
|
||||
else
|
||||
ReadParameters(packet.ResponseReads)
|
||||
end
|
||||
end
|
||||
else
|
||||
packet.OnServerEvent:Fire(player, ReadParameters(packet.Reads))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
remoteEvent.OnServerEvent:Connect(function(player: Player, ...)
|
||||
local success, errorMessage: string? = pcall(onServerEvent, player, ...)
|
||||
if errorMessage and RunService:IsStudio() then warn(player.Name, errorMessage) end
|
||||
end)
|
||||
|
||||
PlayersService.PlayerRemoving:Connect(function(player)
|
||||
playerCursors[player] = nil
|
||||
playerThreads[player] = nil
|
||||
playerBytes[player] = nil
|
||||
end)
|
||||
|
||||
RunService.Heartbeat:Connect(function(deltaTime) task.defer(thread) end)
|
||||
else
|
||||
threads = {Index = 0}
|
||||
remoteEvent = script:WaitForChild("RemoteEvent")
|
||||
local totalTime = 0
|
||||
|
||||
local thread = task.spawn(function()
|
||||
while true do
|
||||
coroutine.yield()
|
||||
if cursor.BufferOffset > 0 then
|
||||
local truncatedBuffer = buffer.create(cursor.BufferOffset)
|
||||
buffer.copy(truncatedBuffer, 0, cursor.Buffer, 0, cursor.BufferOffset)
|
||||
if cursor.InstancesOffset == 0 then
|
||||
remoteEvent:FireServer(truncatedBuffer)
|
||||
else
|
||||
remoteEvent:FireServer(truncatedBuffer, cursor.Instances)
|
||||
cursor.InstancesOffset = 0
|
||||
table.clear(cursor.Instances)
|
||||
end
|
||||
cursor.BufferOffset = 0
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local respond = function(packet: Packet, threadIndex: number, ...)
|
||||
if packet.OnClientInvoke == nil then warn("OnClientInvoke not found for packet:", packet.Name, "discarding event:", ...) return end
|
||||
local values = {packet.OnClientInvoke(...)}
|
||||
Import(cursor)
|
||||
WriteU8(packet.Id)
|
||||
WriteU8(threadIndex + 128)
|
||||
WriteParameters(packet.ResponseWrites, values)
|
||||
cursor = Export()
|
||||
end
|
||||
|
||||
remoteEvent.OnClientEvent:Connect(function(receivedBuffer: buffer, instances: {Instance}?)
|
||||
Import({Buffer = receivedBuffer, BufferLength = buffer.len(receivedBuffer), BufferOffset = 0, Instances = instances or {}, InstancesOffset = 0})
|
||||
while Ended() == false do
|
||||
local packet = packets[ReadU8()]
|
||||
if packet.ResponseReads then
|
||||
local threadIndex = ReadU8()
|
||||
if threadIndex < 128 then
|
||||
Task:Defer(respond, packet, threadIndex, ReadParameters(packet.Reads))
|
||||
else
|
||||
threadIndex -= 128
|
||||
local threads = threads[threadIndex]
|
||||
if threads then
|
||||
task.cancel(threads.Timeout)
|
||||
task.defer(threads.Yielded, ReadParameters(packet.ResponseReads))
|
||||
threads[threadIndex] = nil
|
||||
else
|
||||
warn("Response thread not found for packet:", packet.Name, "discarding response:", ReadParameters(packet.ResponseReads))
|
||||
end
|
||||
end
|
||||
else
|
||||
packet.OnClientEvent:Fire(ReadParameters(packet.Reads))
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
remoteEvent.AttributeChanged:Connect(function(name)
|
||||
local packet = packets[name]
|
||||
if packet then
|
||||
if packet.Id then packets[packet.Id] = nil end
|
||||
packet.Id = remoteEvent:GetAttribute(name)
|
||||
if packet.Id then packets[packet.Id] = packet end
|
||||
end
|
||||
end)
|
||||
|
||||
RunService.Heartbeat:Connect(function(deltaTime)
|
||||
totalTime += deltaTime
|
||||
if totalTime > 0.016666666666666666 then
|
||||
totalTime %= 0.016666666666666666
|
||||
task.defer(thread)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
return setmetatable(Types.Types, {__call = Constructor})
|
2203
src/Chemical/Packages/Promise.lua
Normal file
2203
src/Chemical/Packages/Promise.lua
Normal file
File diff suppressed because it is too large
Load diff
53
src/Chemical/Packages/Queue.lua
Normal file
53
src/Chemical/Packages/Queue.lua
Normal file
|
@ -0,0 +1,53 @@
|
|||
--!strict
|
||||
|
||||
--From Roblox docs
|
||||
|
||||
local Queue = {}
|
||||
Queue.__index = Queue
|
||||
|
||||
export type Queue<T> = typeof(setmetatable(
|
||||
{} :: {
|
||||
_first: number,
|
||||
_last: number,
|
||||
_queue: { T },
|
||||
},
|
||||
Queue
|
||||
))
|
||||
|
||||
function Queue.new<T>(): Queue<T>
|
||||
local self = setmetatable({
|
||||
_first = 0,
|
||||
_last = -1,
|
||||
_queue = {},
|
||||
}, Queue)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- Check if the queue is empty
|
||||
function Queue.isEmpty<T>(self: Queue<T>)
|
||||
return self._first > self._last
|
||||
end
|
||||
|
||||
-- Add a value to the queue
|
||||
function Queue.enqueue<T>(self: Queue<T>, value: T)
|
||||
local last = self._last + 1
|
||||
self._last = last
|
||||
self._queue[last] = value
|
||||
end
|
||||
|
||||
-- Remove a value from the queue
|
||||
function Queue.dequeue<T>(self: Queue<T>): T?
|
||||
if self:isEmpty() then
|
||||
return nil
|
||||
end
|
||||
|
||||
local first = self._first
|
||||
local value = self._queue[first]
|
||||
self._queue[first] = nil
|
||||
self._first = first + 1
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
return Queue
|
113
src/Chemical/Packages/Signals.lua
Normal file
113
src/Chemical/Packages/Signals.lua
Normal file
|
@ -0,0 +1,113 @@
|
|||
--- Lua-side duplication of the API of events on Roblox objects.
|
||||
-- Signals are needed for to ensure that for local events objects are passed by
|
||||
-- reference rather than by value where possible, as the BindableEvent objects
|
||||
-- always pass signal arguments by value, meaning tables will be deep copied.
|
||||
-- Roblox's deep copy method parses to a non-lua table compatable format.
|
||||
-- @classmod Signal
|
||||
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local ENABLE_TRACEBACK = false
|
||||
|
||||
local Signal = {}
|
||||
Signal.__index = Signal
|
||||
Signal.ClassName = "Signal"
|
||||
|
||||
--- Constructs a new signal.
|
||||
-- @constructor Signal.new()
|
||||
-- @treturn Signal
|
||||
function Signal.new()
|
||||
local self = setmetatable({}, Signal)
|
||||
|
||||
self._bindableEvent = Instance.new("BindableEvent")
|
||||
self._argMap = {}
|
||||
self._source = ENABLE_TRACEBACK and debug.traceback() or ""
|
||||
|
||||
-- Events in Roblox execute in reverse order as they are stored in a linked list and
|
||||
-- new connections are added at the head. This event will be at the tail of the list to
|
||||
-- clean up memory.
|
||||
self._bindableEvent.Event:Connect(function(key)
|
||||
self._argMap[key] = nil
|
||||
|
||||
-- We've been destroyed here and there's nothing left in flight.
|
||||
-- Let's remove the argmap too.
|
||||
-- This code may be slower than leaving this table allocated.
|
||||
if (not self._bindableEvent) and (not next(self._argMap)) then
|
||||
self._argMap = nil
|
||||
end
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Fire the event with the given arguments. All handlers will be invoked. Handlers follow
|
||||
-- Roblox signal conventions.
|
||||
-- @param ... Variable arguments to pass to handler
|
||||
-- @treturn nil
|
||||
function Signal:Fire(...)
|
||||
if not self._bindableEvent then
|
||||
warn(("Signal is already destroyed. %s"):format(self._source))
|
||||
return
|
||||
end
|
||||
|
||||
local args = table.pack(...)
|
||||
|
||||
-- TODO: Replace with a less memory/computationally expensive key generation scheme
|
||||
local key = HttpService:GenerateGUID(false)
|
||||
self._argMap[key] = args
|
||||
|
||||
-- Queues each handler onto the queue.
|
||||
self._bindableEvent:Fire(key)
|
||||
end
|
||||
|
||||
--- Connect a new handler to the event. Returns a connection object that can be disconnected.
|
||||
-- @tparam function handler Function handler called with arguments passed when `:Fire(...)` is called
|
||||
-- @treturn Connection Connection object that can be disconnected
|
||||
function Signal:Connect(handler)
|
||||
if not (type(handler) == "function") then
|
||||
error(("connect(%s)"):format(typeof(handler)), 2)
|
||||
end
|
||||
|
||||
return self._bindableEvent.Event:Connect(function(key)
|
||||
-- note we could queue multiple events here, but we'll do this just as Roblox events expect
|
||||
-- to behave.
|
||||
|
||||
local args = self._argMap[key]
|
||||
if args then
|
||||
handler(table.unpack(args, 1, args.n))
|
||||
else
|
||||
error("Missing arg data, probably due to reentrance.")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Wait for fire to be called, and return the arguments it was given.
|
||||
-- @treturn ... Variable arguments from connection
|
||||
function Signal:Wait()
|
||||
local key = self._bindableEvent.Event:Wait()
|
||||
local args = self._argMap[key]
|
||||
if args then
|
||||
return table.unpack(args, 1, args.n)
|
||||
else
|
||||
error("Missing arg data, probably due to reentrance.")
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Disconnects all connected events to the signal. Voids the signal as unusable.
|
||||
-- @treturn nil
|
||||
function Signal:Destroy()
|
||||
if self._bindableEvent then
|
||||
-- This should disconnect all events, but in-flight events should still be
|
||||
-- executed.
|
||||
|
||||
self._bindableEvent:Destroy()
|
||||
self._bindableEvent = nil
|
||||
end
|
||||
|
||||
-- Do not remove the argmap. It will be cleaned up by the cleanup connection.
|
||||
|
||||
setmetatable(self, nil)
|
||||
end
|
||||
|
||||
return Signal
|
237
src/Chemical/Packages/Trees.lua
Normal file
237
src/Chemical/Packages/Trees.lua
Normal file
|
@ -0,0 +1,237 @@
|
|||
-- Tree Library == Vibe coded by Sovereignty
|
||||
|
||||
export type Node<value = any> = {
|
||||
Key: string,
|
||||
Value: value,
|
||||
Children: {Node<value>},
|
||||
Parent: Node<value>?,
|
||||
FullPath: string,
|
||||
|
||||
new: (key: string, value: any?) -> Node<value>,
|
||||
AddChild: (self: Node<value>, key: string, value: any?) -> Node<value>,
|
||||
GetChild: (self: Node<value>, key: string) -> Node<value>?,
|
||||
GetChildren: (self: Node<value>) -> {Node<value>},
|
||||
GetAllDescendants: (self: Node<value>) -> {Node<value>},
|
||||
GetPath: (self: Node<value>) -> {string},
|
||||
SetValue: (self: Node<value>, value: any?) -> (),
|
||||
TraverseDFS: (self: Node<value>, callback: (node: Node<value>) -> ()) -> (),
|
||||
TraverseBFS: (self: Node<value>, callback: (node: Node<value>) -> ()) -> (),
|
||||
}
|
||||
|
||||
export type Tree<nodeValue = any> = {
|
||||
Root: Node<nodeValue>,
|
||||
|
||||
new: (rootKey: string?, rootValue: any?) -> (Tree<nodeValue>),
|
||||
AddNode: (self: Tree<nodeValue>, pathParts: {string}, value: any?) -> Node<nodeValue>,
|
||||
GetNode: (self: Tree<nodeValue>, pathParts: {string}) -> Node<nodeValue>?,
|
||||
GetNodeChildrenByPath: (self: Tree<nodeValue>, pathParts: {string}) -> {Node<nodeValue>},
|
||||
GetDescendantsByPath: (self: Tree<nodeValue>, pathParts: {string}) -> {Node<nodeValue>},
|
||||
FindNode: (self: Tree<nodeValue>, predicate: (node: Node<nodeValue>) -> boolean) -> Node<nodeValue>?,
|
||||
RemoveNode: (self: Tree<nodeValue>, pathParts: {string}) -> boolean,
|
||||
UpdateNode: (self: Tree<nodeValue>, pathParts: {string}, newValue: any) -> boolean,
|
||||
GetPathString: (self: Tree<nodeValue>, pathParts: {string}) -> string,
|
||||
Traverse: (self: Tree<nodeValue>, method: "DFS" | "BFS", callback: (node: Node<nodeValue>) -> ()) -> (),
|
||||
Print: (self: Tree<nodeValue>) -> (),
|
||||
}
|
||||
|
||||
local Node = {} :: Node
|
||||
Node.__index = Node
|
||||
|
||||
function Node.new(key: string, value: any?): Node
|
||||
return setmetatable({
|
||||
Key = key,
|
||||
Value = value,
|
||||
Children = {} :: {Node},
|
||||
Parent = nil :: Node?,
|
||||
FullPath = ""
|
||||
}, Node) :: any
|
||||
end
|
||||
|
||||
function Node:AddChild(key: string, value: any?): Node
|
||||
local child = Node.new(key, value)
|
||||
child.Parent = self
|
||||
|
||||
if self.FullPath == "/" then
|
||||
child.FullPath = "/" .. key
|
||||
else
|
||||
child.FullPath = self.FullPath .. "/" .. key
|
||||
end
|
||||
|
||||
table.insert(self.Children, child)
|
||||
return child
|
||||
end
|
||||
|
||||
function Node:GetChild(key: string): Node?
|
||||
for _, child in ipairs(self.Children) do
|
||||
if child.Key == key then
|
||||
return child
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Node:GetChildren(): {Node}
|
||||
return self.Children
|
||||
end
|
||||
|
||||
function Node:GetAllDescendants(): {Node}
|
||||
local descendants = {} :: {Node}
|
||||
local function traverse(node: Node)
|
||||
for _, child in ipairs(node.Children) do
|
||||
table.insert(descendants, child)
|
||||
traverse(child)
|
||||
end
|
||||
end
|
||||
traverse(self)
|
||||
return descendants
|
||||
end
|
||||
|
||||
function Node:GetPath(): {string}
|
||||
local parts = {} :: {string}
|
||||
local current: Node? = self
|
||||
while current do
|
||||
table.insert(parts, 1, current.Key)
|
||||
current = current.Parent
|
||||
end
|
||||
return parts
|
||||
end
|
||||
|
||||
function Node:SetValue(value: any?)
|
||||
self.Value = value
|
||||
end
|
||||
|
||||
function Node:TraverseDFS(callback: (node: Node) -> ())
|
||||
callback(self)
|
||||
for _, child in ipairs(self.Children) do
|
||||
child:TraverseDFS(callback)
|
||||
end
|
||||
end
|
||||
|
||||
function Node:TraverseBFS(callback: (node: Node) -> ())
|
||||
local queue = {self} :: {Node}
|
||||
while #queue > 0 do
|
||||
local current = table.remove(queue, 1)
|
||||
callback(current)
|
||||
for _, child in ipairs(current.Children) do
|
||||
table.insert(queue, child)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local Tree = {} :: Tree
|
||||
Tree.__index = Tree
|
||||
|
||||
function Tree.new(rootKey: string?, rootValue: any?): Tree
|
||||
local rootKey = rootKey or "/"
|
||||
local root = Node.new(rootKey, rootValue)
|
||||
|
||||
root.FullPath = rootKey == "/" and "/" or "/" .. rootKey
|
||||
|
||||
return setmetatable({
|
||||
Root = root
|
||||
}, Tree) :: Tree
|
||||
end
|
||||
|
||||
function Tree:AddNode(pathParts: {string}, value: any?): Node
|
||||
local current: Node = self.Root
|
||||
|
||||
for _, part in ipairs(pathParts) do
|
||||
local child = current:GetChild(part)
|
||||
|
||||
if not child then
|
||||
child = current:AddChild(part, nil)
|
||||
end
|
||||
|
||||
current = child
|
||||
end
|
||||
|
||||
current.Value = value
|
||||
return current
|
||||
end
|
||||
|
||||
function Tree:GetNode(pathParts: {string}): Node?
|
||||
local current: Node? = self.Root
|
||||
for _, part in ipairs(pathParts) do
|
||||
local nextNode = current:GetChild(part)
|
||||
if not nextNode then break end
|
||||
current = nextNode
|
||||
end
|
||||
return current ~= self.Root and current
|
||||
end
|
||||
|
||||
function Tree:GetNodeChildrenByPath(pathParts: {string}): {Node}
|
||||
local node = self:GetNode(pathParts)
|
||||
return node and node:GetChildren() or {}
|
||||
end
|
||||
|
||||
function Tree:GetDescendantsByPath(pathParts: {string}): {Node}
|
||||
local node = self:GetNode(pathParts)
|
||||
return node and node:GetAllDescendants() or {}
|
||||
end
|
||||
|
||||
function Tree:FindNode(predicate: (node: Node) -> boolean): Node?
|
||||
local found: Node? = nil
|
||||
self.Root:TraverseDFS(function(node)
|
||||
if predicate(node) then
|
||||
found = node
|
||||
end
|
||||
end)
|
||||
return found
|
||||
end
|
||||
|
||||
function Tree:RemoveNode(pathParts: {string}): boolean
|
||||
local node = self:GetNode(pathParts)
|
||||
if not node or node == self.Root then return false end
|
||||
|
||||
local parent = node.Parent
|
||||
if not parent then return false end
|
||||
|
||||
for i, child in ipairs(parent.Children) do
|
||||
if child == node then
|
||||
table.remove(parent.Children, i)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Tree:UpdateNode(pathParts: {string}, newValue: any): boolean
|
||||
local node = self:GetNode(pathParts)
|
||||
if node then
|
||||
node.Value = newValue
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Tree:GetPathString(pathParts: {string}): string
|
||||
return "/" .. table.concat(pathParts, "/")
|
||||
end
|
||||
|
||||
function Tree:Traverse(method: "DFS" | "BFS", callback: (node: Node) -> ())
|
||||
if method == "DFS" then
|
||||
self.Root:TraverseDFS(callback)
|
||||
elseif method == "BFS" then
|
||||
self.Root:TraverseBFS(callback)
|
||||
else
|
||||
error("Invalid traversal method. Use 'DFS' or 'BFS'")
|
||||
end
|
||||
end
|
||||
|
||||
function Tree:Print()
|
||||
print("Tree Structure:")
|
||||
self.Root:TraverseDFS(function(node)
|
||||
local indent = string.rep(" ", #node:GetPath() - 1)
|
||||
|
||||
-- Fix: Format root path correctly
|
||||
local displayPath = node.FullPath
|
||||
if node == self.Root and node.Key == "/" then
|
||||
displayPath = "/"
|
||||
end
|
||||
|
||||
print(indent .. node.Key .. " (" .. displayPath .. ")")
|
||||
end)
|
||||
end
|
||||
|
||||
return Tree
|
3
src/Chemical/Packages/init.meta.json
Normal file
3
src/Chemical/Packages/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
323
src/Chemical/Singletons/Reactor.lua
Normal file
323
src/Chemical/Singletons/Reactor.lua
Normal file
|
@ -0,0 +1,323 @@
|
|||
--[[
|
||||
@module Reactor
|
||||
|
||||
This module facilitates the creation and replication of stateful objects, called "Reactions,"
|
||||
from the server to clients. It is designed to be used in an ECS (Entity-Component System)
|
||||
environment.
|
||||
|
||||
- A "Reactor" is a factory for creating "Reactions" of a specific type.
|
||||
- A "Reaction" is a state object identified by a name and a unique key.
|
||||
- State changes within a Reaction are automatically replicated to the appropriate clients.
|
||||
- It uses a tokenization system to minimize network bandwidth for property names and paths.
|
||||
|
||||
Yes, the documentation was generated.
|
||||
]]
|
||||
|
||||
|
||||
local RunService = game:GetService("RunService")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
|
||||
local Packet = require(RootFolder.Packages.Packet)
|
||||
local Signal = require(RootFolder.Packages.Signals)
|
||||
local Promise = require(RootFolder.Packages.Promise)
|
||||
local Queue = require(RootFolder.Packages.Queue)
|
||||
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local Symbols = require(RootFolder.Symbols)
|
||||
local Effect = require(RootFolder.Factories.Effect)
|
||||
local Reaction = require(RootFolder.Factories.Reaction)
|
||||
local Array = require(RootFolder.Functions.Array)
|
||||
local Blueprint = require(RootFolder.Functions.Blueprint)
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
|
||||
|
||||
type Player = { UserId: number }
|
||||
type Symbol = Symbols.Symbol
|
||||
type Reaction<T> = Reaction.Reaction<T> & { To: { Player } | Symbol, Name: string, Key: string }
|
||||
|
||||
|
||||
local network = {
|
||||
Create = Packet("C__Create", Packet.String, Packet.String, { Packet.NumberU8 }, Packet.Any, Packet.Any),
|
||||
Update = Packet("C__Update", { Packet.NumberU8 }, { Packet.NumberU8 }, Packet.Any),
|
||||
UpdateChanges = Packet("C__UpdateChange", { Packet.NumberU8 }, { Packet.NumberU8 }, Packet.Any), -- TODO
|
||||
Destroy = Packet("C__Destroy", { Packet.NumberU8 }),
|
||||
}
|
||||
|
||||
local Tokens = Cache.Tokens.new()
|
||||
local Reactions: { [string]: { [string]: Reaction<any> } } = {}
|
||||
local ReactionQueues: { [string]: { [string]: Queue.Queue<{ { number } | any }> } } = {}
|
||||
local OnReactionCreated = Signal.new()
|
||||
|
||||
|
||||
|
||||
--- Recursively walks a table structure and creates a map of string keys to numerical tokens.
|
||||
-- This is used to prepare a state object for replication, ensuring the client can reconstruct it.
|
||||
local function createPathTokenMap(
|
||||
snapshotTable: { [any]: any },
|
||||
originalTable: { [any]: any }
|
||||
): { [string]: number }
|
||||
local result = {}
|
||||
|
||||
for key, snapshotValue in pairs(snapshotTable) do
|
||||
local originalValue = originalTable[key]
|
||||
|
||||
|
||||
if typeof(key) ~= "string" then
|
||||
continue
|
||||
end
|
||||
|
||||
|
||||
result[key] = Tokens:ToToken(key)
|
||||
|
||||
|
||||
if Is.Array(snapshotValue) and Is.Array(originalValue) then
|
||||
for k, token in createPathTokenMap(snapshotValue, originalValue) do
|
||||
result[k] = token
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- Sends a network packet to a specified target (all players or a list of players).
|
||||
local AllPlayers = Symbols.All("Players")
|
||||
local function sendToTarget(target: { Player } | Symbol, packet: any, ...: any)
|
||||
if target == AllPlayers then
|
||||
packet:Fire(...)
|
||||
else
|
||||
for _, player in target :: { Player } do
|
||||
packet:FireClient(player, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Handles the server-side logic for replicating a Reaction to clients.
|
||||
local function replicate(reaction: Reaction<any>)
|
||||
local name = reaction.Name
|
||||
local key = reaction.Key
|
||||
local target = reaction.To
|
||||
|
||||
|
||||
local nameToken = Tokens:ToToken(name)
|
||||
local keyToken = Tokens:ToToken(key)
|
||||
local reactionIdTokens = { nameToken, keyToken }
|
||||
|
||||
local blueprint = reaction:blueprint()
|
||||
local pathTokenMap = createPathTokenMap(reaction:snapshot(), reaction:get())
|
||||
|
||||
|
||||
sendToTarget(target, network.Create, name, key, reactionIdTokens, pathTokenMap, blueprint)
|
||||
|
||||
|
||||
Array.Walk(reaction:get(), function(path: { string }, value: any)
|
||||
if Is.Stateful(value) then
|
||||
local pathTokens = Tokens:ToTokenPath(path)
|
||||
|
||||
local eff = Effect(function()
|
||||
sendToTarget(target, network.Update, reactionIdTokens, pathTokens, value:get())
|
||||
end)
|
||||
|
||||
|
||||
ECS.World:add(eff.entity, ECS.JECS.pair(ECS.Tags.InScope, reaction.entity))
|
||||
end
|
||||
end)
|
||||
|
||||
-- Patch the reaction's cleanup to notify clients of its destruction.
|
||||
ECS.World:set(reaction.entity, ECS.Components.CleanupFn, function()
|
||||
if Reactions[name] and Reactions[name][key] then
|
||||
Reactions[name][key] = nil
|
||||
end
|
||||
sendToTarget(target, network.Destroy, reactionIdTokens)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
if RunService:IsClient() then
|
||||
--- Reconstructs a Reaction on the client based on data from the server.
|
||||
local function reconstruct(name: string, key: string, reactionIdTokens: { number }, pathTokenMap: { [string]: number }, blueprint: { string: {T: number, V: any} }): ()
|
||||
if Reactions[name] and Reactions[name][key] then
|
||||
return
|
||||
end
|
||||
|
||||
-- Map the incoming tokens so we can translate them back to strings later.
|
||||
Tokens:Map({ [name] = reactionIdTokens[1], [key] = reactionIdTokens[2] })
|
||||
Tokens:Map(pathTokenMap)
|
||||
|
||||
-- Create the local version of the Reaction
|
||||
local reaction = Reaction(name, key, Blueprint:Read(blueprint :: any))
|
||||
reaction.Name = name
|
||||
reaction.Key = key
|
||||
|
||||
if not Reactions[name] then
|
||||
Reactions[name] = {}
|
||||
end
|
||||
Reactions[name][key] = reaction
|
||||
|
||||
-- Process any queued updates that arrived before this creation packet.
|
||||
if ReactionQueues[name] and ReactionQueues[name][key] then
|
||||
local queue = ReactionQueues[name][key]
|
||||
while not queue:isEmpty() do
|
||||
local args = queue:dequeue()
|
||||
local pathTokens, value = table.unpack(args)
|
||||
local path = Tokens:FromPath(pathTokens)
|
||||
local statefulValue = Array.FindOnPath(reaction:get(), path)
|
||||
if statefulValue and statefulValue.set then
|
||||
statefulValue:set(value)
|
||||
end
|
||||
end
|
||||
ReactionQueues[name][key] = nil -- Clear the queue
|
||||
end
|
||||
|
||||
OnReactionCreated:Fire(name, key, reaction)
|
||||
end
|
||||
|
||||
--- Applies a state update from the server to a local Reaction.
|
||||
local function update(reactionIdTokens: { number }, pathTokens: { number }, value: any)
|
||||
local name = Tokens:From(reactionIdTokens[1])
|
||||
local key = Tokens:From(reactionIdTokens[2])
|
||||
local path = Tokens:FromPath(pathTokens)
|
||||
|
||||
if not name or not key then return end
|
||||
|
||||
local reaction = Reactions[name] and Reactions[name][key]
|
||||
|
||||
-- If the reaction doesn't exist yet, queue the update.
|
||||
if not reaction then
|
||||
if not ReactionQueues[name] then ReactionQueues[name] = {} end
|
||||
if not ReactionQueues[name][key] then ReactionQueues[name][key] = Queue.new() end
|
||||
|
||||
ReactionQueues[name][key]:enqueue({ pathTokens, value })
|
||||
return
|
||||
end
|
||||
|
||||
if reaction.__destroyed then return end
|
||||
|
||||
-- Apply the update
|
||||
local container = reaction:get()
|
||||
local statefulValue = Array.FindOnPath(container, path)
|
||||
if statefulValue and statefulValue.set then
|
||||
statefulValue:set(value)
|
||||
end
|
||||
end
|
||||
|
||||
--- Destroys a local Reaction when notified by the server.
|
||||
local function destroy(reactionIdTokens: { number })
|
||||
local name = Tokens:From(reactionIdTokens[1])
|
||||
local key = Tokens:From(reactionIdTokens[2])
|
||||
|
||||
if not name or not key then return end
|
||||
|
||||
local reaction = Reactions[name] and Reactions[name][key]
|
||||
|
||||
if not reaction or not reaction.entity then return end
|
||||
|
||||
reaction:destroy()
|
||||
Reactions[name][key] = nil
|
||||
end
|
||||
|
||||
-- Connect client network events to their handler functions
|
||||
network.Create.OnClientEvent:Connect(reconstruct)
|
||||
network.Update.OnClientEvent:Connect(update)
|
||||
network.Destroy.OnClientEvent:Connect(destroy)
|
||||
else
|
||||
Players.PlayerAdded:Connect(function(player: Player)
|
||||
for _, keyedReactions in Reactions do
|
||||
for _, reaction in keyedReactions do
|
||||
if reaction.To == Symbols.All("Players") or table.find(reaction.To, player) then
|
||||
replicate(reaction)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--// Public API
|
||||
local api = {}
|
||||
|
||||
--- Awaits the creation of a specific Reaction on the client.
|
||||
-- @param name The name of the Reactor that creates the Reaction.
|
||||
-- @param key The unique key of the Reaction.
|
||||
-- @return A promise that resolves with the Reaction once it's created.
|
||||
function api.await<T>(name: string, key: string): Reaction<T>
|
||||
if Reactions[name] and Reactions[name][key] then
|
||||
return Reactions[name][key]
|
||||
end
|
||||
|
||||
return Promise.fromEvent(OnReactionCreated, function(n: string, k: string, _)
|
||||
return name == n and key == k
|
||||
end):andThen(function(...)
|
||||
return select(3, ...) -- Return the reaction object
|
||||
end):expect()
|
||||
end
|
||||
|
||||
--- Listens for the creation of any Reaction from a specific Reactor.
|
||||
-- @param name The name of the Reactor.
|
||||
-- @param callback A function to call with the key and Reaction object.
|
||||
-- @return A connection object with a :Disconnect() method.
|
||||
function api.onCreate(name: string, callback: (key: string, reaction: Reaction<any>) -> ())
|
||||
return OnReactionCreated:Connect(function(n: string, k: string, reaction: Reaction<any>)
|
||||
if n == name then
|
||||
task.spawn(callback, k, reaction)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Creates a "Reactor" factory.
|
||||
-- @param config A configuration table with a 'Name' and optional 'Subjects' (players).
|
||||
-- @param constructor A function that returns the initial state for a new Reaction.
|
||||
-- @return A Reactor object with `create` and `await` methods.
|
||||
return function<T, U...>(
|
||||
config: { Name: string, Subjects: { Player } | Symbol? },
|
||||
constructor: (key: string, U...) -> T
|
||||
)
|
||||
local name = config.Name
|
||||
assert(name, "Reactor config must include a 'Name'.")
|
||||
|
||||
local to = config.Subjects or Symbols.All("Players")
|
||||
local reactor = {}
|
||||
|
||||
--- Creates and replicates a new Reaction. [SERVER-ONLY]
|
||||
-- @param self The reactor object.
|
||||
-- @param key A unique key for this Reaction.
|
||||
-- @param ... Additional arguments to be passed to the constructor.
|
||||
-- @return The created Reaction instance.
|
||||
function reactor:create(key: string, ...: U...): Reaction<T>
|
||||
assert(not RunService:IsClient(), "Reactions can only be created on the server.")
|
||||
if Reactions[name] and Reactions[name][key] then
|
||||
warn(string.format("Reactor '%s' is overwriting an existing reaction with key '%s'", name, key))
|
||||
end
|
||||
|
||||
local reaction = Reaction(name, key, constructor(key, ...))
|
||||
reaction.To = to
|
||||
reaction.Name = name
|
||||
reaction.Key = key
|
||||
|
||||
if not Reactions[name] then
|
||||
Reactions[name] = {}
|
||||
end
|
||||
Reactions[name][key] = reaction
|
||||
|
||||
-- The new, encapsulated replicate function handles all server-side logic.
|
||||
replicate(reaction)
|
||||
|
||||
return reaction
|
||||
end
|
||||
|
||||
--- Awaits a specific Reaction from this Reactor. [CLIENT-ONLY]
|
||||
function reactor:await(key: string): Reaction<T>
|
||||
return api.await(name, key)
|
||||
end
|
||||
|
||||
--- Listens for new Reactions created by this Reactor. [CLIENT-ONLY]
|
||||
function reactor:onCreate(callback: (key: string, reaction: Reaction<T>) -> ())
|
||||
return api.onCreate(name, callback)
|
||||
end
|
||||
|
||||
return reactor
|
||||
end
|
95
src/Chemical/Singletons/Scheduler.lua
Normal file
95
src/Chemical/Singletons/Scheduler.lua
Normal file
|
@ -0,0 +1,95 @@
|
|||
--!native
|
||||
--!optimize 2
|
||||
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local Components = ECS.Components
|
||||
local Tags = ECS.Tags
|
||||
local JECS = ECS.JECS
|
||||
local World = ECS.World
|
||||
|
||||
local MAX_COMPUTATION_DEPTH = 100
|
||||
|
||||
local dirtySourceQuery = World:query(Components.Object)
|
||||
:with(Tags.IsStateful, Tags.IsDirty)
|
||||
:without(Tags.IsEffect, Tags.IsComputed)
|
||||
:cached()
|
||||
|
||||
local dirtyComputedsQuery = World:query(Components.Object)
|
||||
:with(Tags.IsComputed, Tags.IsDirty)
|
||||
:cached()
|
||||
|
||||
local dirtyEffectsQuery = World:query(Components.Object)
|
||||
:with(Tags.IsEffect, Tags.IsDirty)
|
||||
:cached()
|
||||
|
||||
|
||||
local GetSubscribers = require(RootFolder.Functions.GetSubscribers)
|
||||
|
||||
|
||||
local Scheduler = {}
|
||||
|
||||
function Scheduler:Update()
|
||||
-- PROPAGATE DIRTINESS
|
||||
for sourceEntity, _ in dirtySourceQuery:iter() do
|
||||
local subscribers = GetSubscribers(sourceEntity)
|
||||
|
||||
for _, subscriberEntity in ipairs(subscribers) do
|
||||
if not World:has(subscriberEntity, Tags.IsDirty) then
|
||||
World:add(subscriberEntity, Tags.IsDirty)
|
||||
end
|
||||
end
|
||||
|
||||
World:remove(sourceEntity, Tags.IsDirty)
|
||||
end
|
||||
|
||||
-- RE-RUN COMPUTED VALUES
|
||||
for i = 1, MAX_COMPUTATION_DEPTH do
|
||||
local computedsToProcess = {}
|
||||
|
||||
for entity, computable in dirtyComputedsQuery:iter() do
|
||||
table.insert(computedsToProcess, computable)
|
||||
end
|
||||
|
||||
if #computedsToProcess == 0 then
|
||||
break
|
||||
end
|
||||
|
||||
for _, computable in ipairs(computedsToProcess) do
|
||||
|
||||
computable:compute()
|
||||
World:remove(computable.entity, Tags.IsDirty)
|
||||
|
||||
for _, subscriber in ipairs(GetSubscribers(computable.entity)) do
|
||||
World:add(subscriber, Tags.IsDirty)
|
||||
end
|
||||
end
|
||||
|
||||
if i == MAX_COMPUTATION_DEPTH then
|
||||
warn("Chemical: Max computation depth exceeded. Check for a circular dependency in your Computed values.")
|
||||
end
|
||||
end
|
||||
|
||||
-- RUN EFFECTS & OBSERVERS
|
||||
for _, runnable in dirtyEffectsQuery:iter() do
|
||||
runnable:run()
|
||||
World:remove(runnable.entity, Tags.IsDirty)
|
||||
end
|
||||
end
|
||||
|
||||
if RunService:IsServer() then
|
||||
RunService.Heartbeat:Connect(function()
|
||||
Scheduler:Update()
|
||||
end)
|
||||
else
|
||||
RunService.RenderStepped:Connect(function()
|
||||
Scheduler:Update()
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
return Scheduler
|
3
src/Chemical/Singletons/init.meta.json
Normal file
3
src/Chemical/Singletons/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
26
src/Chemical/Symbols.lua
Normal file
26
src/Chemical/Symbols.lua
Normal file
|
@ -0,0 +1,26 @@
|
|||
local RootFolder = script.Parent
|
||||
|
||||
local GuiTypes = require(RootFolder.Types.Gui)
|
||||
local GetSymbol = require(RootFolder.Functions.GetSymbol)
|
||||
|
||||
export type Symbol<S = string, T = string> = GetSymbol.Symbol<S, T>
|
||||
|
||||
local module = {}
|
||||
|
||||
module.OnEvent = function(eventName: GuiTypes.EventNames)
|
||||
return GetSymbol(eventName, "Event")
|
||||
end
|
||||
|
||||
module.OnChange = function(eventName: GuiTypes.PropertyNames)
|
||||
return GetSymbol(eventName, "Change")
|
||||
end
|
||||
|
||||
module.Children = GetSymbol("All", "Children")
|
||||
|
||||
module.All = function<S>(subjects: S)
|
||||
return GetSymbol(subjects, "All") :: Symbol<S, "All">
|
||||
end
|
||||
|
||||
--OnEvent symbols are handled by the OnEvent function.
|
||||
|
||||
return module
|
360
src/Chemical/Types/Gui.lua
Normal file
360
src/Chemical/Types/Gui.lua
Normal file
|
@ -0,0 +1,360 @@
|
|||
type Stateful<T> = { set: (T) -> (), get: () -> (T), __entity: number }
|
||||
|
||||
export type GuiBaseProperties = {
|
||||
Name: (Stateful<string> | string)?,
|
||||
Visible: (Stateful<boolean> | boolean)?,
|
||||
Active: (Stateful<boolean> | boolean)?,
|
||||
AnchorPoint: (Stateful<Vector2> | Vector2)?,
|
||||
Position: (Stateful<UDim2> | UDim2)?,
|
||||
Size: (Stateful<UDim2> | UDim2)?,
|
||||
Rotation: (Stateful<number> | number)?,
|
||||
ZIndex: (Stateful<number> | number)?,
|
||||
LayoutOrder: (Stateful<number> | number)?,
|
||||
BackgroundTransparency: (Stateful<number> | number)?,
|
||||
BackgroundColor3: (Stateful<Color3> | Color3)?,
|
||||
BorderSizePixel: (Stateful<number> | number)?,
|
||||
BorderColor3: (Stateful<Color3> | Color3)?,
|
||||
ClipsDescendants: (Stateful<boolean> | boolean)?,
|
||||
Selectable: (Stateful<boolean> | boolean)?,
|
||||
Parent: GuiObject?,
|
||||
Children: { [number]: Instance | Stateful<GuiObject> },
|
||||
|
||||
[any]: (() -> ())?
|
||||
}
|
||||
|
||||
type GuiBaseEvents = {
|
||||
InputBegan: (input: InputObject, gameProcessed: boolean) -> (),
|
||||
InputEnded: (input: InputObject, gameProcessed: boolean) -> (),
|
||||
InputChanged: (input: InputObject, gameProcessed: boolean) -> (),
|
||||
|
||||
-- Mouse Events
|
||||
MouseEnter: () -> (),
|
||||
MouseLeave: () -> (),
|
||||
MouseMoved: (deltaX: number, deltaY: number) -> (),
|
||||
MouseWheelForward: (scrollDelta: number) -> (),
|
||||
MouseWheelBackward: (scrollDelta: number) -> (),
|
||||
|
||||
-- Touch Events
|
||||
TouchTap: (touchPositions: {Vector2}, state: Enum.UserInputState) -> (),
|
||||
TouchPinch: (scale: number, velocity: number, state: Enum.UserInputState) -> (),
|
||||
TouchPan: (pan: Vector2, velocity: Vector2, state: Enum.UserInputState) -> (),
|
||||
TouchSwipe: (direction: Enum.SwipeDirection, touches: number) -> (),
|
||||
TouchRotate: (rotation: number, velocity: number, state: Enum.UserInputState) -> (),
|
||||
TouchLongPress: (duration: number) -> (),
|
||||
|
||||
-- Console/Selection Events
|
||||
SelectionGained: () -> (),
|
||||
SelectionLost: () -> (),
|
||||
SelectionChanged: (newSelection: Instance) -> (),
|
||||
}
|
||||
|
||||
type ImageGuiProperties = GuiBaseProperties & {
|
||||
Image: (Stateful<string> | string)?,
|
||||
ImageColor3: (Stateful<Color3> | Color3)?,
|
||||
ImageTransparency: (Stateful<number> | number)?,
|
||||
ScaleType: (Stateful<Enum.ScaleType> | Enum.ScaleType)?,
|
||||
SliceCenter: (Stateful<Rect> | Rect)?,
|
||||
TileSize: (Stateful<UDim2> | UDim2)?,
|
||||
ResampleMode: (Stateful<Enum.ResamplerMode> | Enum.ResamplerMode)?,
|
||||
}
|
||||
|
||||
type TextGuiProperties = GuiBaseProperties & {
|
||||
Text: (Stateful<string> | string)?,
|
||||
TextColor3: (Stateful<Color3> | Color3)?,
|
||||
TextTransparency: (Stateful<number> | number)?,
|
||||
TextStrokeColor3: (Stateful<Color3> | Color3)?,
|
||||
TextStrokeTransparency: (Stateful<number> | number)?,
|
||||
TextScaled: (Stateful<boolean> | boolean)?,
|
||||
TextSize: (Stateful<number> | number)?,
|
||||
TextWrapped: (Stateful<boolean> | boolean)?,
|
||||
FontFace: (Stateful<Font> | Font)?,
|
||||
LineHeight: (Stateful<number> | number)?,
|
||||
RichText: (Stateful<boolean> | boolean)?,
|
||||
TextXAlignment: (Stateful<Enum.TextXAlignment> | Enum.TextXAlignment)?,
|
||||
TextYAlignment: (Stateful<Enum.TextYAlignment> | Enum.TextYAlignment)?,
|
||||
TextTruncate: (Stateful<Enum.TextTruncate> | Enum.TextTruncate)?,
|
||||
[any]: (() -> ())?,
|
||||
}
|
||||
|
||||
export type FrameProperties = GuiBaseProperties
|
||||
export type TextLabelProperties = TextGuiProperties
|
||||
export type ImageLabelProperties = ImageGuiProperties
|
||||
|
||||
-- Interactive Elements
|
||||
type ButtonEvents = GuiBaseEvents & {
|
||||
Activated: (inputType: Enum.UserInputType?) -> (),
|
||||
MouseButton1Click: () -> (),
|
||||
MouseButton2Click: () -> (),
|
||||
MouseButton2Down: () -> (),
|
||||
MouseButton2Up: () -> (),
|
||||
|
||||
MouseWheelForward: nil,
|
||||
MouseWheelBackward: nil,
|
||||
}
|
||||
|
||||
export type ButtonProperties = {
|
||||
AutoButtonColor: (Stateful<boolean> | boolean)?,
|
||||
Modal: (Stateful<boolean> | boolean)?,
|
||||
Selected: (Stateful<boolean> | boolean)?,
|
||||
|
||||
ButtonHoverStyle: (Stateful<Enum.ButtonStyle> | Enum.ButtonStyle)?,
|
||||
ButtonPressStyle: (Stateful<Enum.ButtonStyle> | Enum.ButtonStyle)?,
|
||||
ActivationBehavior: (Stateful<Enum.ActivationBehavior> | Enum.ActivationBehavior)?,
|
||||
|
||||
SelectionGroup: (Stateful<number> | number)?,
|
||||
SelectionBehaviorUp: (Stateful<Enum.SelectionBehavior> | Enum.SelectionBehavior)?,
|
||||
SelectionBehaviorDown: (Stateful<Enum.SelectionBehavior> | Enum.SelectionBehavior)?,
|
||||
SelectionBehaviorLeft: (Stateful<Enum.SelectionBehavior> | Enum.SelectionBehavior)?,
|
||||
SelectionBehaviorRight: (Stateful<Enum.SelectionBehavior> | Enum.SelectionBehavior)?,
|
||||
GamepadPriority: (Stateful<number> | number)?,
|
||||
}
|
||||
|
||||
|
||||
export type TextButtonProperties = TextGuiProperties & ButtonProperties
|
||||
export type ImageButtonProperties = ImageGuiProperties & ButtonProperties
|
||||
|
||||
type TextBoxEvents = GuiBaseEvents & {
|
||||
FocusLost: (enterPressed: boolean) -> (),
|
||||
FocusGained: () -> (),
|
||||
TextChanged: (text: string) -> (),
|
||||
}
|
||||
|
||||
export type TextBoxProperties = TextGuiProperties & {
|
||||
ClearTextOnFocus: (Stateful<boolean> | boolean)?,
|
||||
MultiLine: (Stateful<boolean> | boolean)?,
|
||||
PlaceholderText: (Stateful<string> | string)?,
|
||||
PlaceholderColor3: (Stateful<Color3> | Color3)?,
|
||||
CursorPosition: (Stateful<number> | number)?,
|
||||
SelectionStart: (Stateful<number> | number)?,
|
||||
ShowNativeInput: (Stateful<boolean> | boolean)?,
|
||||
TextInputType: (Stateful<Enum.TextInputType> | Enum.TextInputType)?,
|
||||
}
|
||||
|
||||
|
||||
-- Containers
|
||||
type ScrollingFrameEvents = GuiBaseEvents & {
|
||||
Scrolled: (scrollVelocity: Vector2) -> (),
|
||||
}
|
||||
|
||||
export type ScrollingFrameProperties = FrameProperties & {
|
||||
ScrollBarImageColor3: (Stateful<Color3> | Color3)?,
|
||||
ScrollBarThickness: (Stateful<number> | number)?,
|
||||
ScrollingDirection: (Stateful<Enum.ScrollingDirection> | Enum.ScrollingDirection)?,
|
||||
CanvasSize: (Stateful<UDim2> | UDim2)?,
|
||||
CanvasPosition: (Stateful<Vector2> | Vector2)?,
|
||||
AutomaticCanvasSize: (Stateful<Enum.AutomaticSize> | Enum.AutomaticSize)?,
|
||||
VerticalScrollBarInset: (Stateful<Enum.ScrollBarInset> | Enum.ScrollBarInset)?,
|
||||
HorizontalScrollBarInset: (Stateful<Enum.ScrollBarInset> | Enum.ScrollBarInset)?,
|
||||
ScrollBarImageTransparency: (Stateful<number> | number)?,
|
||||
ElasticBehavior: (Stateful<Enum.ElasticBehavior> | Enum.ElasticBehavior)?,
|
||||
VerticalScrollBarPosition: (Stateful<Enum.VerticalScrollBarPosition> | Enum.VerticalScrollBarPosition)?,
|
||||
}
|
||||
|
||||
type ViewportFrameEvents = GuiBaseEvents & {
|
||||
ViewportResized: (newSize: Vector2) -> (),
|
||||
CameraChanged: (newCamera: Camera) -> (),
|
||||
}
|
||||
|
||||
export type ViewportFrameProperties = FrameProperties & {
|
||||
CurrentCamera: (Stateful<Camera> | Camera)?,
|
||||
ImageColor3: (Stateful<Color3> | Color3)?,
|
||||
LightColor: (Stateful<Color3> | Color3)?,
|
||||
LightDirection: (Stateful<Vector3> | Vector3)?,
|
||||
Ambient: (Stateful<Color3> | Color3)?,
|
||||
LightAngularInfluence: (Stateful<number> | number)?,
|
||||
}
|
||||
|
||||
-- Layouts
|
||||
export type UIListLayoutProperties = {
|
||||
Padding: (Stateful<UDim> | UDim)?,
|
||||
FillDirection: (Stateful<Enum.FillDirection> | Enum.FillDirection)?,
|
||||
HorizontalAlignment: (Stateful<Enum.HorizontalAlignment> | Enum.HorizontalAlignment)?,
|
||||
VerticalAlignment: (Stateful<Enum.VerticalAlignment> | Enum.VerticalAlignment)?,
|
||||
SortOrder: (Stateful<Enum.SortOrder> | Enum.SortOrder)?,
|
||||
Appearance: (Stateful<Enum.Appearance> | Enum.Appearance)?,
|
||||
}
|
||||
|
||||
export type UIGridLayoutProperties = {
|
||||
CellSize: (Stateful<UDim2> | UDim2)?,
|
||||
CellPadding: (Stateful<UDim2> | UDim2)?,
|
||||
StartCorner: (Stateful<Enum.StartCorner> | Enum.StartCorner)?,
|
||||
FillDirection: (Stateful<Enum.FillDirection> | Enum.FillDirection)?,
|
||||
HorizontalAlignment: (Stateful<Enum.HorizontalAlignment> | Enum.HorizontalAlignment)?,
|
||||
VerticalAlignment: (Stateful<Enum.VerticalAlignment> | Enum.VerticalAlignment)?,
|
||||
SortOrder: (Stateful<Enum.SortOrder> | Enum.SortOrder)?,
|
||||
}
|
||||
|
||||
-- Style Elements
|
||||
export type UICornerProperties = {
|
||||
CornerRadius: (Stateful<UDim> | UDim)?,
|
||||
}
|
||||
|
||||
export type UIStrokeProperties = {
|
||||
Color: (Stateful<Color3> | Color3)?,
|
||||
Thickness: (Stateful<number> | number)?,
|
||||
Transparency: (Stateful<number> | number)?,
|
||||
Enabled: (Stateful<boolean> | boolean)?,
|
||||
ApplyStrokeMode: (Stateful<Enum.ApplyStrokeMode> | Enum.ApplyStrokeMode)?,
|
||||
LineJoinMode: (Stateful<Enum.LineJoinMode> | Enum.LineJoinMode)?,
|
||||
}
|
||||
|
||||
export type UIGradientProperties = {
|
||||
Color: (Stateful<ColorSequence> | ColorSequence)?,
|
||||
Transparency: (Stateful<NumberSequence> | NumberSequence)?,
|
||||
Offset: (Stateful<Vector2> | Vector2)?,
|
||||
Rotation: (Stateful<number> | number)?,
|
||||
Enabled: (Stateful<boolean> | boolean)?,
|
||||
}
|
||||
|
||||
export type UIPaddingProperties = {
|
||||
PaddingTop: (Stateful<UDim> | UDim)?,
|
||||
PaddingBottom: (Stateful<UDim> | UDim)?,
|
||||
PaddingLeft: (Stateful<UDim> | UDim)?,
|
||||
PaddingRight: (Stateful<UDim> | UDim)?,
|
||||
}
|
||||
|
||||
export type UIScaleProperties = {
|
||||
Scale: (Stateful<number> | number)?,
|
||||
}
|
||||
|
||||
|
||||
type CanvasMouseEvents = GuiBaseEvents & {
|
||||
MouseWheel: (direction: Enum.MouseWheelDirection, delta: number) -> (),
|
||||
}
|
||||
|
||||
export type CanvasGroupProperties = {
|
||||
GroupTransparency: (Stateful<number> | number)?,
|
||||
GroupColor3: (Stateful<Color3> | Color3)?,
|
||||
} & CanvasMouseEvents
|
||||
|
||||
-- Constraints
|
||||
export type UIAspectRatioConstraintProperties = {
|
||||
AspectRatio: (Stateful<number> | number)?,
|
||||
AspectType: (Stateful<Enum.AspectType> | Enum.AspectType)?,
|
||||
DominantAxis: (Stateful<Enum.DominantAxis> | Enum.DominantAxis)?,
|
||||
}
|
||||
|
||||
export type UISizeConstraintProperties = {
|
||||
MinSize: (Stateful<Vector2> | Vector2)?,
|
||||
MaxSize: (Stateful<Vector2> | Vector2)?,
|
||||
}
|
||||
|
||||
-- Specialized
|
||||
export type BillboardGuiProperties = GuiBaseProperties & {
|
||||
Active: (Stateful<boolean> | boolean)?,
|
||||
AlwaysOnTop: (Stateful<boolean> | boolean)?,
|
||||
LightInfluence: (Stateful<number> | number)?,
|
||||
MaxDistance: (Stateful<number> | number)?,
|
||||
SizeOffset: (Stateful<Vector2> | Vector2)?,
|
||||
StudsOffset: (Stateful<Vector3> | Vector3)?,
|
||||
ExtentsOffset: (Stateful<Vector3> | Vector3)?,
|
||||
}
|
||||
|
||||
export type SurfaceGuiProperties = GuiBaseProperties & {
|
||||
Active: (Stateful<boolean> | boolean)?,
|
||||
AlwaysOnTop: (Stateful<boolean> | boolean)?,
|
||||
Brightness: (Stateful<number> | number)?,
|
||||
CanvasSize: (Stateful<Vector2> | Vector2)?,
|
||||
Face: (Stateful<Enum.NormalId> | Enum.NormalId)?,
|
||||
LightInfluence: (Stateful<number> | number)?,
|
||||
PixelsPerStud: (Stateful<number> | number)?,
|
||||
SizingMode: (Stateful<Enum.SurfaceGuiSizingMode> | Enum.SurfaceGuiSizingMode)?,
|
||||
ToolPunchThroughDistance: (Stateful<number> | number)?,
|
||||
}
|
||||
|
||||
export type ScreenGuiProperties = GuiBaseProperties & {
|
||||
Active: (Stateful<boolean> | boolean)?,
|
||||
AlwaysOnTop: (Stateful<boolean> | boolean)?,
|
||||
Brightness: (Stateful<number> | number)?,
|
||||
DisplayOrder: (Stateful<number> | number)?,
|
||||
IgnoreGuiInset: (Stateful<boolean> | boolean)?,
|
||||
OnTopOfCoreBlur: (Stateful<boolean> | boolean)?,
|
||||
ScreenInsets: (Stateful<Enum.ScreenInsets> | Enum.ScreenInsets)?,
|
||||
ZIndexBehavior: (Stateful<Enum.ZIndexBehavior> | Enum.ZIndexBehavior)?,
|
||||
}
|
||||
|
||||
export type EventNames = (
|
||||
"InputBegan" | "InputEnded" | "InputChanged" |
|
||||
"MouseEnter" | "MouseLeave" | "MouseMoved" |
|
||||
"MouseButton1Down" | "MouseButton1Up" |
|
||||
"MouseWheelForward" | "MouseWheelBackward" |
|
||||
|
||||
"TouchTap" | "TouchPinch" | "TouchPan" |
|
||||
"TouchSwipe" | "TouchRotate" | "TouchLongPress" |
|
||||
|
||||
"SelectionGained" | "SelectionLost" | "SelectionChanged" |
|
||||
|
||||
"Activated" | "MouseButton1Click" | "MouseButton2Click" |
|
||||
"MouseButton2Down" | "MouseButton2Up" |
|
||||
|
||||
"FocusLost" | "FocusGained" | "TextChanged" |
|
||||
|
||||
"Scrolled" |
|
||||
|
||||
"ViewportResized" | "CameraChanged" |
|
||||
|
||||
"BillboardTransformed" |
|
||||
|
||||
"SurfaceChanged" |
|
||||
|
||||
"GroupTransparencyChanged" |
|
||||
|
||||
"StrokeUpdated" |
|
||||
|
||||
"GradientOffsetChanged" |
|
||||
|
||||
"ChildAdded" | "ChildRemoved" | "AncestryChanged"
|
||||
)
|
||||
|
||||
export type PropertyNames = (
|
||||
"Name" | "Visible" | "Active" | "AnchorPoint" | "Position" | "Size" |
|
||||
"Rotation" | "ZIndex" | "LayoutOrder" | "BackgroundTransparency" |
|
||||
"BackgroundColor3" | "BorderSizePixel" | "BorderColor3" |
|
||||
"ClipsDescendants" | "Selectable" |
|
||||
|
||||
"Image" | "ImageColor3" | "ImageTransparency" | "ScaleType" |
|
||||
"SliceCenter" | "TileSize" | "ResampleMode" |
|
||||
|
||||
"Text" | "TextColor3" | "TextTransparency" | "TextStrokeColor3" |
|
||||
"TextStrokeTransparency" | "TextScaled" | "TextSize" | "TextWrapped" |
|
||||
"FontFace" | "LineHeight" | "RichText" | "TextXAlignment" |
|
||||
"TextYAlignment" | "TextTruncate" |
|
||||
|
||||
"AutoButtonColor" | "Modal" | "Selected" | "ButtonHoverStyle" |
|
||||
"ButtonPressStyle" | "ActivationBehavior" | "SelectionGroup" |
|
||||
"SelectionBehaviorUp" | "SelectionBehaviorDown" |
|
||||
"SelectionBehaviorLeft" | "SelectionBehaviorRight" | "GamepadPriority" |
|
||||
|
||||
"ClearTextOnFocus" | "MultiLine" | "PlaceholderText" |
|
||||
"PlaceholderColor3" | "CursorPosition" | "SelectionStart" |
|
||||
"ShowNativeInput" | "TextInputType" |
|
||||
|
||||
"ScrollBarImageColor3" | "ScrollBarThickness" | "ScrollingDirection" |
|
||||
"CanvasSize" | "CanvasPosition" | "AutomaticCanvasSize" |
|
||||
"VerticalScrollBarInset" | "HorizontalScrollBarInset" |
|
||||
"ScrollBarImageTransparency" | "ElasticBehavior" | "VerticalScrollBarPosition" |
|
||||
|
||||
"CurrentCamera" | "LightColor" | "LightDirection" | "Ambient" |
|
||||
"LightAngularInfluence" |
|
||||
|
||||
"Padding" | "FillDirection" | "HorizontalAlignment" | "VerticalAlignment" |
|
||||
"SortOrder" | "Appearance" | "CellSize" | "CellPadding" | "StartCorner" |
|
||||
|
||||
"CornerRadius" | "Color" | "Thickness" | "Transparency" | "Enabled" |
|
||||
"ApplyStrokeMode" | "LineJoinMode" | "Offset" | "Rotation" |
|
||||
"PaddingTop" | "PaddingBottom" | "PaddingLeft" | "PaddingRight" | "Scale" |
|
||||
|
||||
"GroupTransparency" | "GroupColor3" |
|
||||
|
||||
"AspectRatio" | "AspectType" | "DominantAxis" | "MinSize" | "MaxSize" |
|
||||
|
||||
"AlwaysOnTop" | "LightInfluence" | "MaxDistance" | "SizeOffset" |
|
||||
"StudsOffset" | "ExtentsOffset" |
|
||||
|
||||
"Brightness" | "Face" | "PixelsPerStud" | "SizingMode" | "ToolPunchThroughDistance" |
|
||||
|
||||
"Parent" | "Children"
|
||||
)
|
||||
|
||||
|
||||
return {}
|
66
src/Chemical/Types/Overrides.lua
Normal file
66
src/Chemical/Types/Overrides.lua
Normal file
|
@ -0,0 +1,66 @@
|
|||
--!strict
|
||||
local Gui = require(script.Parent.Gui)
|
||||
|
||||
-- Define the custom method we're adding.
|
||||
type CompositionHandle = {
|
||||
Destroy: (self: CompositionHandle) -> ()
|
||||
}
|
||||
|
||||
-- The factory function now returns an intersection type.
|
||||
-- It tells Luau "this object has all the properties of P AND all the properties of CompositionHandle".
|
||||
type ComposerFactory<P> = (blueprint: P) -> (P & CompositionHandle)
|
||||
|
||||
-- The overloads remain the same, but their return type is now more powerful.
|
||||
export type ComposeFunction = (
|
||||
-- Overloads for creating new instances via class name strings
|
||||
((target: "Frame") -> ComposerFactory<Gui.FrameProperties>) &
|
||||
((target: "TextLabel") -> ComposerFactory<Gui.TextLabelProperties>) &
|
||||
((target: "ImageLabel") -> ComposerFactory<Gui.ImageLabelProperties>) &
|
||||
((target: "TextButton") -> ComposerFactory<Gui.TextButtonProperties>) &
|
||||
((target: "ImageButton") -> ComposerFactory<Gui.ImageButtonProperties>) &
|
||||
((target: "TextBox") -> ComposerFactory<Gui.TextBoxProperties>) &
|
||||
((target: "ScrollingFrame") -> ComposerFactory<Gui.ScrollingFrameProperties>) &
|
||||
((target: "ViewportFrame") -> ComposerFactory<Gui.ViewportFrameProperties>) &
|
||||
((target: "CanvasGroup") -> ComposerFactory<Gui.CanvasGroupProperties>) &
|
||||
((target: "UIListLayout") -> ComposerFactory<Gui.UIListLayoutProperties>) &
|
||||
((target: "UIGridLayout") -> ComposerFactory<Gui.UIGridLayoutProperties>) &
|
||||
((target: "UICorner") -> ComposerFactory<Gui.UICornerProperties>) &
|
||||
((target: "UIStroke") -> ComposerFactory<Gui.UIStrokeProperties>) &
|
||||
((target: "UIGradient") -> ComposerFactory<Gui.UIGradientProperties>) &
|
||||
((target: "UIPadding") -> ComposerFactory<Gui.UIPaddingProperties>) &
|
||||
((target: "UIScale") -> ComposerFactory<Gui.UIScaleProperties>) &
|
||||
((target: "UIAspectRatioConstraint") -> ComposerFactory<Gui.UIAspectRatioConstraintProperties>) &
|
||||
((target: "UISizeConstraint") -> ComposerFactory<Gui.UISizeConstraintProperties>) &
|
||||
((target: "BillboardGui") -> ComposerFactory<Gui.BillboardGuiProperties>) &
|
||||
((target: "SurfaceGui") -> ComposerFactory<Gui.SurfaceGuiProperties>) &
|
||||
((target: "ScreenGui") -> ComposerFactory<Gui.ScreenGuiProperties>) &
|
||||
|
||||
-- Overloads for adopting existing instances
|
||||
((target: Frame) -> ComposerFactory<Gui.FrameProperties>) &
|
||||
((target: TextLabel) -> ComposerFactory<Gui.TextLabelProperties>) &
|
||||
((target: ImageLabel) -> ComposerFactory<Gui.ImageLabelProperties>) &
|
||||
((target: TextButton) -> ComposerFactory<Gui.TextButtonProperties>) &
|
||||
((target: ImageButton) -> ComposerFactory<Gui.ImageButtonProperties>) &
|
||||
((target: TextBox) -> ComposerFactory<Gui.TextBoxProperties>) &
|
||||
((target: ScrollingFrame) -> ComposerFactory<Gui.ScrollingFrameProperties>) &
|
||||
((target: ViewportFrame) -> ComposerFactory<Gui.ViewportFrameProperties>) &
|
||||
((target: CanvasGroup) -> ComposerFactory<Gui.CanvasGroupProperties>) &
|
||||
((target: UIListLayout) -> ComposerFactory<Gui.UIListLayoutProperties>) &
|
||||
((target: UIGridLayout) -> ComposerFactory<Gui.UIGridLayoutProperties>) &
|
||||
((target: UICorner) -> ComposerFactory<Gui.UICornerProperties>) &
|
||||
((target: UIStroke) -> ComposerFactory<Gui.UIStrokeProperties>) &
|
||||
((target: UIGradient) -> ComposerFactory<Gui.UIGradientProperties>) &
|
||||
((target: UIPadding) -> ComposerFactory<Gui.UIPaddingProperties>) &
|
||||
((target: UIScale) -> ComposerFactory<Gui.UIScaleProperties>) &
|
||||
((target: UIAspectRatioConstraint) -> ComposerFactory<Gui.UIAspectRatioConstraintProperties>) &
|
||||
((target: UISizeConstraint) -> ComposerFactory<Gui.UISizeConstraintProperties>) &
|
||||
((target: BillboardGui) -> ComposerFactory<Gui.BillboardGuiProperties>) &
|
||||
((target: SurfaceGui) -> ComposerFactory<Gui.SurfaceGuiProperties>) &
|
||||
((target: ScreenGui) -> ComposerFactory<Gui.ScreenGuiProperties>) &
|
||||
|
||||
-- Fallback overloads for generic/unspecified types
|
||||
((target: string) -> ComposerFactory<Gui.GuiBaseProperties>) &
|
||||
((target: GuiObject) -> ComposerFactory<Gui.GuiBaseProperties>)
|
||||
)
|
||||
|
||||
return {}
|
17
src/Chemical/Types/init.lua
Normal file
17
src/Chemical/Types/init.lua
Normal file
|
@ -0,0 +1,17 @@
|
|||
local ECS = require(script.Parent.Packages.JECS)
|
||||
|
||||
local module = {}
|
||||
|
||||
export type HasEntity = {
|
||||
entity: ECS.Entity
|
||||
}
|
||||
|
||||
export type MaybeDestroyable = {
|
||||
__internalDestroy: (MaybeDestroyable) -> (),
|
||||
}
|
||||
|
||||
export type MaybeCleanable = {
|
||||
clean: (MaybeCleanable) -> ()
|
||||
}
|
||||
|
||||
return module
|
66
src/Chemical/init.lua
Normal file
66
src/Chemical/init.lua
Normal file
|
@ -0,0 +1,66 @@
|
|||
--!strict
|
||||
|
||||
--version 0.2.5 == Sovereignty
|
||||
|
||||
--TODO:
|
||||
-- Reactors and Reactions: Tables and Maps change networking, rather than full value networking on change.
|
||||
-- Export Types
|
||||
-- Improve file organization
|
||||
-- Templating
|
||||
-- Entity recycling - if possible
|
||||
|
||||
local ECS = require(script.ECS)
|
||||
local Overrides = require(script.Types.Overrides)
|
||||
|
||||
local Value = require(script.Factories.Value)
|
||||
local Table = require(script.Factories.Table)
|
||||
local Map = require(script.Factories.Map)
|
||||
local Computed = require(script.Factories.Computed)
|
||||
local Observer = require(script.Factories.Observer)
|
||||
local Watch = require(script.Factories.Watch)
|
||||
local Effect = require(script.Factories.Effect)
|
||||
local Reaction = require(script.Factories.Reaction)
|
||||
|
||||
|
||||
local Compose = require(script.Functions.Compose)
|
||||
|
||||
|
||||
local Is = require(script.Functions.Is)
|
||||
local Peek = require(script.Functions.Peek)
|
||||
local Array = require(script.Functions.Array)
|
||||
local Alive = require(script.Functions.Alive)
|
||||
local Destroy = require(script.Functions:FindFirstChild("Destroy"))
|
||||
local Blueprint = require(script.Functions.Blueprint)
|
||||
|
||||
local Symbols = require(script.Symbols)
|
||||
|
||||
local Scheduler = require(script.Singletons.Scheduler)
|
||||
local Reactor = require(script.Singletons.Reactor)
|
||||
|
||||
|
||||
return {
|
||||
Value = (Value :: any) :: Value.ValueFactory,
|
||||
Table = Table,
|
||||
Map = Map,
|
||||
Computed = Computed,
|
||||
Observer = Observer,
|
||||
Watch = Watch,
|
||||
Effect = Effect,
|
||||
Reaction = Reaction,
|
||||
|
||||
|
||||
Compose = (Compose :: any) :: Overrides.ComposeFunction,
|
||||
Reactor = Reactor,
|
||||
|
||||
Is = Is,
|
||||
Peek = Peek,
|
||||
Array = Array,
|
||||
Alive = Alive,
|
||||
Destroy = Destroy,
|
||||
Blueprint = Blueprint,
|
||||
|
||||
|
||||
OnEvent = Symbols.OnEvent,
|
||||
OnChange = Symbols.OnChange,
|
||||
Children = Symbols.Children
|
||||
}
|
Loading…
Reference in a new issue