mirror of
https://github.com/Sovvie/Chemical.git
synced 2025-08-04 14:09:17 +00:00
Compare commits
No commits in common. "6eff46964f8c811b5d32f9ff78f32659644afec2" and "031a4899e4f8541a66f7d86d6c123b7540f5a8e8" have entirely different histories.
6eff46964f
...
031a4899e4
75 changed files with 813 additions and 10829 deletions
22
.gitignore
vendored
22
.gitignore
vendored
|
@ -1,8 +1,14 @@
|
|||
# Project place file
|
||||
/*.rbxl
|
||||
sourcemap.json*
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
/.vscode
|
||||
/.png
|
||||
# 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
|
||||
|
|
BIN
Chemical.rbxm
Normal file
BIN
Chemical.rbxm
Normal file
Binary file not shown.
31
LICENSE
Normal file
31
LICENSE
Normal file
|
@ -0,0 +1,31 @@
|
|||
╔════════════════════════════════════════════════════╗
|
||||
║ Library Created by @Sovereignty ║
|
||||
╚════════════════════════════════════════════════════╝
|
||||
|
||||
Copyright © 2025 @Sovereignty
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
||||
of this script and associated documentation files (the “Script”), to use, copy,
|
||||
|
||||
modify, merge, publish, and distribute the Script, provided that the following
|
||||
|
||||
conditions are met:
|
||||
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
|
||||
all copies or substantial portions of the Script.
|
||||
|
||||
The Script may not be sold, sublicensed, or used for commercial purposes
|
||||
|
||||
without prior written permission from @Sovereignty.
|
||||
|
||||
The Script is provided “as is”, without warranty of any kind, express or
|
||||
|
||||
implied, including but not limited to the warranties of merchantability,
|
||||
|
||||
fitness for a particular purpose, and noninfringement.
|
||||
|
||||
|
||||
For inquiries, contact: @sov_dev / On Discord
|
771
README.md
771
README.md
|
@ -1,4 +1,769 @@
|
|||

|
||||
# Chemical - Reactive State & UI Framework for Roblox Luau
|
||||
|
||||
### Reactive state management & replication and declarative UI composition library.
|
||||
#### Latest Release: https://github.com/Sovvie/Chemical/releases/tag/v0.2.5
|
||||
**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()`.
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
# 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"
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"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 +0,0 @@
|
|||
std = "roblox"
|
|
@ -1,123 +0,0 @@
|
|||
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
|
|
@ -1,41 +0,0 @@
|
|||
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
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
--!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
|
|
@ -1,46 +0,0 @@
|
|||
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
|
|
@ -1,43 +0,0 @@
|
|||
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
|
|
@ -1,44 +0,0 @@
|
|||
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
|
|
@ -1,70 +0,0 @@
|
|||
--!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
|
|
@ -1,53 +0,0 @@
|
|||
--!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
|
|
@ -1,48 +0,0 @@
|
|||
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
|
|
@ -1,65 +0,0 @@
|
|||
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
|
|
@ -1,47 +0,0 @@
|
|||
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
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
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
|
|
@ -1,80 +0,0 @@
|
|||
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
|
|
@ -1,78 +0,0 @@
|
|||
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
|
|
@ -1,57 +0,0 @@
|
|||
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
|
|
@ -1,147 +0,0 @@
|
|||
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
|
|
@ -1,38 +0,0 @@
|
|||
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
|
|
@ -1,13 +0,0 @@
|
|||
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
|
|
@ -1,20 +0,0 @@
|
|||
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
|
|
@ -1,14 +0,0 @@
|
|||
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
|
|
@ -1,12 +0,0 @@
|
|||
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
|
|
@ -1,83 +0,0 @@
|
|||
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
|
|
@ -1,15 +0,0 @@
|
|||
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
|
|
@ -1,21 +0,0 @@
|
|||
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
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
--!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,
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
--!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,
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
--!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
|
|
@ -1,33 +0,0 @@
|
|||
--!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,
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
--!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,
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
--!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,
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
--!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,
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
--!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,
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
--!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,
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
--!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,
|
||||
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
--!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,
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
-- 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
|
|
@ -1,219 +0,0 @@
|
|||
-- 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
|
|
@ -1,266 +0,0 @@
|
|||
-- 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
|
|
@ -1,734 +0,0 @@
|
|||
-- 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
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,160 +0,0 @@
|
|||
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
|
|
@ -1,101 +0,0 @@
|
|||
--!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
|
|
@ -1,46 +0,0 @@
|
|||
--!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
|
|
@ -1,6 +0,0 @@
|
|||
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",
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
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"
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
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"
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
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"
|
||||
}
|
|
@ -1,705 +0,0 @@
|
|||
--!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,
|
||||
}
|
|
@ -1,368 +0,0 @@
|
|||
--!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
|
@ -1,53 +0,0 @@
|
|||
--!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
|
|
@ -1,113 +0,0 @@
|
|||
--- 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
|
|
@ -1,237 +0,0 @@
|
|||
-- 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
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
|
@ -1,323 +0,0 @@
|
|||
--[[
|
||||
@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
|
|
@ -1,95 +0,0 @@
|
|||
--!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
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
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
|
|
@ -1,360 +0,0 @@
|
|||
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 {}
|
|
@ -1,66 +0,0 @@
|
|||
--!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 {}
|
|
@ -1,17 +0,0 @@
|
|||
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
|
|
@ -1,66 +0,0 @@
|
|||
--!strict
|
||||
|
||||
--version 0.2.5 == Sovereignty
|
||||
|
||||
--TODO:
|
||||
-- Reactors and Reactions: Tables and Maps change networking, rather than full value networking on change.
|
||||
-- Export Types
|
||||
-- Improve file organization
|
||||
-- Templating
|
||||
-- Entity recycling - if possible
|
||||
|
||||
local ECS = require(script.ECS)
|
||||
local Overrides = require(script.Types.Overrides)
|
||||
|
||||
local Value = require(script.Factories.Value)
|
||||
local Table = require(script.Factories.Table)
|
||||
local Map = require(script.Factories.Map)
|
||||
local Computed = require(script.Factories.Computed)
|
||||
local Observer = require(script.Factories.Observer)
|
||||
local Watch = require(script.Factories.Watch)
|
||||
local Effect = require(script.Factories.Effect)
|
||||
local Reaction = require(script.Factories.Reaction)
|
||||
|
||||
|
||||
local Compose = require(script.Functions.Compose)
|
||||
|
||||
|
||||
local Is = require(script.Functions.Is)
|
||||
local Peek = require(script.Functions.Peek)
|
||||
local Array = require(script.Functions.Array)
|
||||
local Alive = require(script.Functions.Alive)
|
||||
local Destroy = require(script.Functions:FindFirstChild("Destroy"))
|
||||
local Blueprint = require(script.Functions.Blueprint)
|
||||
|
||||
local Symbols = require(script.Symbols)
|
||||
|
||||
local Scheduler = require(script.Singletons.Scheduler)
|
||||
local Reactor = require(script.Singletons.Reactor)
|
||||
|
||||
|
||||
return {
|
||||
Value = (Value :: any) :: Value.ValueFactory,
|
||||
Table = Table,
|
||||
Map = Map,
|
||||
Computed = Computed,
|
||||
Observer = Observer,
|
||||
Watch = Watch,
|
||||
Effect = Effect,
|
||||
Reaction = Reaction,
|
||||
|
||||
|
||||
Compose = (Compose :: any) :: Overrides.ComposeFunction,
|
||||
Reactor = Reactor,
|
||||
|
||||
Is = Is,
|
||||
Peek = Peek,
|
||||
Array = Array,
|
||||
Alive = Alive,
|
||||
Destroy = Destroy,
|
||||
Blueprint = Blueprint,
|
||||
|
||||
|
||||
OnEvent = Symbols.OnEvent,
|
||||
OnChange = Symbols.OnChange,
|
||||
Children = Symbols.Children
|
||||
}
|
Loading…
Reference in a new issue