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

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

Binary file not shown.

674
LICENSE
View file

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

@ -1,769 +1,4 @@
# Chemical - Reactive State & UI Framework for Roblox Luau
![455053047-da1869cc-d7ed-4f89-b4cb-9ff2adeca48a](https://github.com/user-attachments/assets/585d3f1d-cc0b-4d79-953f-d4e681152bb3)
**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
View 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
View 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
View file

@ -0,0 +1 @@
std = "roblox"

123
src/Chemical/Cache.lua Normal file
View 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

View 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

View file

@ -0,0 +1,3 @@
{
"ignoreUnknownInstances": true
}

109
src/Chemical/ECS.lua Normal file
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -0,0 +1,3 @@
{
"ignoreUnknownInstances": true
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -0,0 +1,3 @@
{
"ignoreUnknownInstances": true
}

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

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

View 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

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

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

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

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

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

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

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

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

View file

@ -0,0 +1,3 @@
{
"ignoreUnknownInstances": true
}

View 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

View 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

View 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

View 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

View file

@ -0,0 +1,3 @@
{
"ignoreUnknownInstances": true
}

File diff suppressed because it is too large Load diff

View 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

View 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

View 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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load diff

View 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

View 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

View 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

View file

@ -0,0 +1,3 @@
{
"ignoreUnknownInstances": true
}

View 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

View 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

View file

@ -0,0 +1,3 @@
{
"ignoreUnknownInstances": true
}

26
src/Chemical/Symbols.lua Normal file
View 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
View 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 {}

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

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