Chemical v0.2.5

This commit is contained in:
Sovereignty 2025-06-13 22:52:16 +02:00
commit 172a63f664
72 changed files with 10826 additions and 0 deletions

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
# Project place file
/*.rbxl
sourcemap.json*
# Roblox Studio lock files
/*.rbxlx.lock
/*.rbxl.lock
/.vscode
/.png

7
aftman.toml Normal file
View file

@ -0,0 +1,7 @@
# This file lists tools managed by Aftman, a cross-platform toolchain manager.
# For more information, see https://github.com/LPGhatguy/aftman
# To add a new tool, add an entry to this table.
[tools]
rojo = "rojo-rbx/rojo@7.5.1"
# rojo = "rojo-rbx/rojo@6.2.0"

30
default.project.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "project",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"$ignoreUnknownInstances": true,
"$path": "src/ReplicatedStorage"
},
"ServerScriptService": {
"$className": "ServerScriptService",
"$ignoreUnknownInstances": true,
"$path": "src/ServerScriptService"
},
"StarterPlayer": {
"$className": "StarterPlayer",
"StarterPlayerScripts": {
"$className": "StarterPlayerScripts",
"$ignoreUnknownInstances": true,
"$path": "src/StarterPlayer/StarterPlayerScripts"
},
"$ignoreUnknownInstances": true
},
"TestService": {
"$className": "TestService",
"$ignoreUnknownInstances": true,
"$path": "src/TestService"
}
}
}

1
selene.toml Normal file
View file

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

123
src/Chemical/Cache.lua Normal file
View file

@ -0,0 +1,123 @@
local module = {}
module.Tokens = {}
module.Stack = {}
module.Queues = {}
do
local tokenCount = 0
local stringsToTokens = {}
local tokensToSrings = {}
local tokenMap = {}
function module:Tokenize(key: string): number
if stringsToTokens[key] then return stringsToTokens[key] end
tokenCount += 1
stringsToTokens[key] = tokenCount
tokensToSrings[tokenCount] = key
return tokenCount
end
function module:FromToken(token: number): string
return tokensToSrings[token]
end
function module:MapAToken(key: string, token: number)
tokenMap[token] = key
end
function module:FromAMap(token: number): string
return tokenMap[token]
end
function module:TokenClear(key: string | number)
if typeof(key) == "string" then
local token = stringsToTokens[key]
stringsToTokens[key] = nil
tokensToSrings[token] = nil
elseif typeof(key) == "number" then
local str = tokensToSrings[key]
stringsToTokens[str] = nil
tokensToSrings[key] = nil
end
end
end
function module.Tokens.new()
local tokenCount = 0
local stringsToTokens = {}
local tokensToSrings = {}
return {
ToToken = function(self: {}, key: string): number
if stringsToTokens[key] then return stringsToTokens[key] end
tokenCount += 1
stringsToTokens[key] = tokenCount
tokensToSrings[tokenCount] = key
return tokenCount
end,
ToTokenPath = function(self: {}, keys: {string}): { number }
local tokens = {}
for _, key in keys do
table.insert(tokens, self:ToToken(key))
end
return tokens
end,
Is = function(self: {}, key: string): boolean
return stringsToTokens[key] ~= nil
end,
From = function(self: {}, token: number): string
return tokensToSrings[token]
end,
FromPath = function(self: {}, tokens: { number }): { string }
local strings = {}
for _, token in tokens do
table.insert(strings, tokensToSrings[token])
end
return strings
end,
Map = function(self: {}, stringsToTokens: { [string]: number })
for key, value in stringsToTokens do
if typeof(value) == "table" then
self:Map(value)
continue
end
stringsToTokens[key] = value
tokensToSrings[value] = key
end
end,
}
end
local stack = {}
function module.Stack:Push(entry: any): number
table.insert(stack, entry)
return #stack
end
function module.Stack:Top(): any
return stack[#stack]
end
function module.Stack:Pop(index: number?)
stack[index or #stack] = nil
end
return module

View file

@ -0,0 +1,41 @@
local RootFolder = script.Parent.Parent
local Types = require(RootFolder.Types)
local ECS = require(RootFolder.ECS)
local module = {}
type Object = Types.HasEntity & {
use: (self: Object, ...{}) -> (),
}
function module.new(metamethods: {}?): Object
local inherits = {}
metamethods = metamethods or {}
metamethods.__index = function(self, index)
local has = rawget(self, index)
if has then return has
else if inherits[index] then return inherits[index] end
end
end
local object = setmetatable({
entity = ECS.World:entity()
}, metamethods)
object.use = function(self, ...: {})
local classes = { ... }
for _, class in classes do
for key, value in class do
inherits[key] = value
end
end
object.use = nil
end
return object
end
return module

View file

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

109
src/Chemical/ECS.lua Normal file
View file

@ -0,0 +1,109 @@
--!nonstrict
local Packages = script.Parent.Packages
local ECS = require(Packages.JECS)
export type Entity<T = nil> = ECS.Entity<T>
local Tags = {
-- State Management Tags
IsStatic = ECS.tag(),
IsPrimitive = ECS.tag(),
IsSettable = ECS.tag(),
IsStateful = ECS.tag(),
IsStatefulTable = ECS.tag(),
IsStatefulDictionary = ECS.tag(),
IsComputed = ECS.tag(),
IsEffect = ECS.tag(),
IsDirty = ECS.tag(),
IsDeepComparable = ECS.tag(),
-- Relationship Tags
SubscribesTo = ECS.tag(),
HasSubscriber = ECS.tag(),
InScope = ECS.tag(),
ChildOf = ECS.ChildOf,
-- UI-specific Tags TODO
IsHost = ECS.tag(),
ManagedBy = ECS.tag(),
UIParent = ECS.tag(),
}
local World = ECS.world()
local Components = {
Name = ECS.Name,
Object = World:component(),
Value = World:component(),
PrevValue = World:component(),
Callback = World:component(),
CallbackList = World:component(),
OnChangeCallbacks = World:component(),
OnKVChangeCallbacks = World:component(),
Connection = World:component() :: ECS.Entity<RBXScriptConnection>,
ConnectionList = World:component() :: ECS.Entity<{RBXScriptConnection}>,
Instance = World:component(),
ManagedItems = World:component(),
LoopType = World:component(),
ComputeFn = World:component(),
EffectFn = World:component(),
CleanupFn = World:component(),
}
World:set(Components.Connection, ECS.OnRemove, function(entity)
local connection = World:get(entity, Components.Connection)
if connection then
connection:Disconnect()
end
end)
World:set(Components.ConnectionList, ECS.OnRemove, function(entity)
local connections = World:get(entity, Components.ConnectionList)
if connections then
for _, conn in ipairs(connections) do
conn:Disconnect()
end
end
end)
World:set(Components.Instance, ECS.OnRemove, function(entity)
if World:has(entity, Tags.IsHost) then
for effectEntity in World:each(ECS.pair(Tags.InScope, entity)) do
World:delete(effectEntity)
end
if World:has(entity, Components.ConnectionList) then
World:remove(entity, Components.ConnectionList)
end
end
end)
World:set(Components.Value, ECS.OnChange, function(entity, id, data)
if World:has(entity, Tags.IsSettable) then
World:add(entity, Tags.IsDirty)
end
end)
World:set(Components.Object, ECS.OnRemove, function(entity: ECS.Entity<any>, id: ECS.Id<any>)
local object = World:get(entity, Components.Object)
if object and object.__internalDestroy then
object:__internalDestroy()
end
end)
World:add(Tags.SubscribesTo, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
World:add(Tags.HasSubscriber, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
World:add(Tags.InScope, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
World:add(Tags.ManagedBy, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
World:add(Tags.UIParent, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
local module = {
Components = Components,
Tags = Tags,
JECS = ECS,
World = World,
}
return module

View file

@ -0,0 +1,46 @@
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local Cache = require(RootFolder.Cache)
local Object = require(RootFolder.Classes.Object)
local Stateful = require(RootFolder.Mixins.Stateful)
local Destroyable = require(RootFolder.Mixins.Destroyable)
local Cleanable = require(RootFolder.Mixins.Cleanable)
local Computable = require(RootFolder.Mixins.Computable)
export type Computed<T> = Stateful.Stateful<T> & Computable.Computable<T> & Destroyable.Destroyable<T> & Cleanable.Cleanable<T>
return function<T>(computeFn: () -> T, cleanupFn: (T) -> ()?): Computed<T>
local obj = Object.new({
__tostring = function(self)
local rawValue = ECS.World:get(self.entity, ECS.Components.Value)
return `Computed<{tostring(rawValue)}>`
end,
})
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
ECS.World:add(obj.entity, ECS.Tags.IsComputed)
ECS.World:set(obj.entity, ECS.Components.ComputeFn, computeFn)
if cleanupFn then ECS.World:set(obj.entity, ECS.Components.CleanupFn, cleanupFn) end
obj:use(
Computable,
Stateful,
Destroyable,
Cleanable
)
obj:compute()
ECS.World:set(obj.entity, ECS.Components.Object, obj)
return obj
end

View file

@ -0,0 +1,43 @@
local RootFolder = script.Parent.Parent
local Classes = RootFolder.Classes
local Mixins = RootFolder.Mixins
local ECS = require(RootFolder.ECS)
local Object = require(Classes.Object)
local Effectable = require(Mixins.Effectable)
local Destroyable = require(Mixins.Destroyable)
local Cleanable = require(Mixins.Cleanable)
export type Effect = Effectable.Effectable & Destroyable.Destroyable & Cleanable.Cleanable
type CleanUp = () -> ()
--- Effect
-- Effects will fire after the batch of stateful object changes are propogated.
-- The optional cleanup function will fire first, and then the effect's function.
-- The effect function can optionally return a cleanup function.
-- Effects will be deleted when any one of its dependent objects are destroyed.
return function(effectFn: () -> ( CleanUp | nil )): Effect
local obj = Object.new()
ECS.World:add(obj.entity, ECS.Tags.IsEffect)
ECS.World:set(obj.entity, ECS.Components.EffectFn, effectFn)
obj:use(
Cleanable,
Destroyable,
Effectable
)
obj:run()
ECS.World:set(obj.entity, ECS.Components.Object, obj)
return obj
end

View file

@ -0,0 +1,44 @@
local RootFolder = script.Parent.Parent
local Classes = RootFolder.Classes
local Mixins = RootFolder.Mixins
local ECS = require(RootFolder.ECS)
local Object = require(Classes.Object)
local Stateful = require(Mixins.Stateful)
local StatefulTable = require(Mixins.StatefulTable)
local StatefulDictionary = require(Mixins.StatefulDictionary)
local Settable = require(Mixins.Settable)
local Numerical = require(Mixins.Numerical)
local Destroyable = require(Mixins.Destroyable)
export type Value<T> = Stateful.Stateful<T> & Settable.Settable<T> & StatefulDictionary.StatefulDictionary<T> & Destroyable.Destroyable
return function<T>(value: T): Value<T>
local obj = Object.new({
__tostring = function(self)
return `Map<{self.entity}>`
end,
})
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
ECS.World:add(obj.entity, ECS.Tags.IsStatefulDictionary)
ECS.World:add(obj.entity, ECS.Tags.IsSettable)
obj:use(
Stateful,
Settable,
StatefulDictionary,
Destroyable
)
ECS.World:set(obj.entity, ECS.Components.Value, value)
ECS.World:set(obj.entity, ECS.Components.Object, obj)
return obj
end

View file

@ -0,0 +1,70 @@
--!strict
local RootFolder = script.Parent.Parent
local LinkedList = require(RootFolder.Packages.LinkedList)
local Classes = RootFolder.Classes
local Mixins = RootFolder.Mixins
local Functions = RootFolder.Functions
local ECS = require(RootFolder.ECS)
local Is = require(Functions.Is)
local Object = require(Classes.Object)
local Stateful = require(Mixins.Stateful)
local Observable = require(Mixins.Observable)
local Destroyable = require(Mixins.Destroyable)
export type Observer<T> = Observable.Observable<T> & Destroyable.Destroyable
export type ObserverTable<T> = Observable.ObservableTable<T> & Destroyable.Destroyable
export type ObserverFactory = (<T>(sourceObject: Stateful.Stateful<{T}>) -> ObserverTable<T>)
& (<T>(sourceObject: Stateful.Stateful<T>) -> Observer<T>)
--- Observer
-- Creates an observer that reacts to changes in a stateful source.
-- If the subject's value is a table, upon first creation of Observer, onKVChange callbacks will be supported.
-- @param sourceObject The stateful object to observe.
-- @return A new observer object.
local function createObserver<T>(sourceObject: Stateful.Stateful<T>)
if not Is.Stateful(sourceObject) then
error("The first argument of an Observer must be a stateful object.", 2)
end
local obj = Object.new()
ECS.World:add(obj.entity, ECS.Tags.IsEffect)
ECS.World:set(obj.entity, ECS.Components.OnChangeCallbacks, LinkedList.new())
if typeof(sourceObject:get()) == "table" then
ECS.World:add(sourceObject.entity, ECS.Tags.IsDeepComparable)
ECS.World:set(obj.entity, ECS.Components.OnKVChangeCallbacks, LinkedList.new())
end
ECS.World:add(obj.entity, ECS.JECS.pair(ECS.Tags.SubscribesTo, sourceObject.entity))
ECS.World:add(sourceObject.entity, ECS.JECS.pair(ECS.Tags.HasSubscriber, obj.entity))
obj:use(
Destroyable,
Observable
)
ECS.World:set(obj.entity, ECS.Components.Object, obj)
return obj
end
return (createObserver :: any) :: ObserverFactory

View file

@ -0,0 +1,53 @@
--!strict
local RootFolder = script.Parent.Parent
local LinkedList = require(RootFolder.Packages.LinkedList)
local Classes = RootFolder.Classes
local Mixins = RootFolder.Mixins
local Functions = RootFolder.Functions
local ECS = require(RootFolder.ECS)
local Is = require(Functions.Is)
local SetInScope = require(Functions.SetInScope)
local Object = require(Classes.Object)
local Stateful = require(Mixins.Stateful)
local Destroyable = require(Mixins.Destroyable)
local Serializable = require(Mixins.Serializable)
export type Reaction<T> = Stateful.Stateful<T> & Destroyable.Destroyable & Serializable.Serializable & T
--- Reaction
-- A Stateful container with helper methods for converting data into different formats.
local function createReaction<T>(name: string, key: string, container: T): Reaction<T>
local obj = Object.new({
__tostring = function(self)
local isAlive = Is.Dead(self) and "Dead" or "Alive"
return `Reaction<{name}/{key}> - {isAlive}`
end,
})
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
SetInScope(container :: any, obj.entity)
obj:use(
Stateful,
Destroyable,
Serializable,
(container :: any)
)
ECS.World:set(obj.entity, ECS.Components.Value, container)
ECS.World:set(obj.entity, ECS.Components.Object, obj)
return obj :: Reaction<T>
end
return createReaction

View file

@ -0,0 +1,48 @@
local RootFolder = script.Parent.Parent
local Classes = RootFolder.Classes
local Mixins = RootFolder.Mixins
local ECS = require(RootFolder.ECS)
local Object = require(Classes.Object)
local Stateful = require(Mixins.Stateful)
local StatefulTable = require(Mixins.StatefulTable)
local StatefulDictionary = require(Mixins.StatefulDictionary)
local Settable = require(Mixins.Settable)
local Numerical = require(Mixins.Numerical)
local Destroyable = require(Mixins.Destroyable)
export type Value<T> = Stateful.Stateful<T> & Settable.Settable<T> & StatefulTable.StatefulTable<T> & Destroyable.Destroyable
return function<T>(value: T): Value<T>
local obj = Object.new({
__len = function(self)
return #ECS.World:get(self.entity, ECS.Components.Value)
end,
__tostring = function(self)
return `Table<{ tostring(self.entity) }>`
end,
})
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
ECS.World:add(obj.entity, ECS.Tags.IsStatefulTable)
ECS.World:add(obj.entity, ECS.Tags.IsSettable)
obj:use(
Stateful,
Settable,
StatefulTable,
Destroyable
)
ECS.World:set(obj.entity, ECS.Components.Value, value)
ECS.World:set(obj.entity, ECS.Components.Object, obj)
return obj
end

View file

@ -0,0 +1,65 @@
local RootFolder = script.Parent.Parent
local Classes = RootFolder.Classes
local Mixins = RootFolder.Mixins
local ECS = require(RootFolder.ECS)
local Object = require(Classes.Object)
local Stateful = require(Mixins.Stateful)
local StatefulTable = require(Mixins.StatefulTable)
local StatefulDictionary = require(Mixins.StatefulDictionary)
local Settable = require(Mixins.Settable)
local Numerical = require(Mixins.Numerical)
local Destroyable = require(Mixins.Destroyable)
type Value<T> = Stateful.Stateful<T> & Settable.Settable<T> & Destroyable.Destroyable
export type ValueFactory = (
((value: number) -> Value<number> & Numerical.Numerical) &
(<T>(value: T) -> Value<T>)
)
--- Value
-- Stateful value container which enables reactivity.
-- Depending on the type of the initial value, certain methods are exposed correlating to the value type.
-- @param value any -- The initial value to set.
-- @return The Value object.
return function<T>(value: T): Value<T>
local obj = Object.new({
__len = typeof(value) == "table" and function(self)
return #ECS.World:get(self.entity, ECS.Components.Value)
end or nil,
__tostring = function(self)
local rawValue = ECS.World:get(self.entity, ECS.Components.Value)
return `Value<{tostring(rawValue)}>`
end,
})
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
ECS.World:add(obj.entity, ECS.Tags.IsSettable)
local mtMethods: { {} } = {
Stateful,
Settable,
Destroyable,
}
if typeof(value) == "number" then
table.insert(mtMethods, Numerical)
end
obj:use(table.unpack(mtMethods))
ECS.World:set(obj.entity, ECS.Components.Value, value)
ECS.World:set(obj.entity, ECS.Components.Object, obj)
return obj
end

View file

@ -0,0 +1,47 @@
local RootFolder = script.Parent.Parent
local Types = require(RootFolder.Types)
local Observer = require(RootFolder.Factories.Observer)
local Stateful = require(RootFolder.Mixins.Stateful)
local Computable = require(RootFolder.Mixins.Computable)
export type WatchHandle = {
destroy: (self: WatchHandle) -> ()
}
type Watchable<T> = Stateful.Stateful<T> | Computable.Computable<T>
--- Creates a watcher that runs a callback function whenever a reactive source changes.
-- @param source The Value or Computed object to watch.
-- @param watchCallback A function that will be called with (newValue, oldValue).
-- @returns A handle with a :destroy() method to stop watching.
return function<T>(source: Watchable<T>, watchCallback: (new: T, old: T) -> ()): WatchHandle
if not source or not source.entity then
error("Chemical.Watch requires a valid Value or Computed object as its first argument.", 2)
end
if typeof(watchCallback) ~= "function" then
error("Chemical.Watch requires a function as its second argument.", 2)
end
local obs = Observer(source)
obs:onChange(function(newValue, oldValue)
local success, err = pcall(watchCallback, newValue, oldValue)
if not success then
warn("Chemical Watch Error: ", err)
end
end)
local handle: WatchHandle = {
destroy = function()
obs:destroy()
end,
}
return handle
end

View file

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

View file

@ -0,0 +1,8 @@
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local Types = require(RootFolder.Types)
return function(obj: Types.HasEntity): boolean
return ECS.World:contains(obj.entity)
end

View file

@ -0,0 +1,80 @@
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local Cache = require(RootFolder.Cache)
local Types = require(RootFolder.Types)
local Is = require(RootFolder.Functions.Is)
local module = {}
function module.Transform<K, V, R>(tbl: { [K]: V }, doFn: (k: K, v: V) -> R): { [K]: R }
local newTbl = {}
for key, value in tbl do
if Is.Array(value) then
newTbl[key] = module.Transform(value, doFn)
else
newTbl[key] = doFn(key, value)
end
end
return newTbl
end
function module.ShallowTransform<K, V, R>(tbl: { [K]: V }, doFn: (k: K, v: V) -> R): { [K]: R }
local newTbl = {}
for key, value in tbl do
newTbl[key] = doFn(key, value)
end
return newTbl
end
function module.Traverse(tbl: {}, doFn: (k: any, v: any) -> ())
for key, value in tbl do
if Is.Array(value) then
module.Traverse(value, doFn)
else
doFn(key, value)
end
end
end
--[[
Recursively walks a table, calling a visitor function for every
value encountered. The visitor receives the path (an array of keys)
and the value at that path.
@param target The table to walk.
@param visitor The function to call, with signature: (path: {any}, value: any) -> ()
--]]
function module.Walk(target: {any}, visitor: (path: {any}, value: any) -> ())
local function _walk(currentValue: any, currentPath: {any})
visitor(currentPath, currentValue)
if Is.Array(currentValue) then
for key, childValue in pairs(currentValue) do
local childPath = table.clone(currentPath)
table.insert(childPath, key)
_walk(childValue, childPath)
end
end
end
_walk(target, {})
end
function module.FindOnPath<K, V>(tbl: {[K]: V}, path: { number | string }): V
local current = tbl
for _, key in path do
current = current[key]
end
return current
end
return module

View file

@ -0,0 +1,78 @@
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local Cache = require(RootFolder.Cache)
local Types = require(RootFolder.Types)
local Value = require(RootFolder.Factories.Value)
local Table = require(RootFolder.Factories.Table)
local Map = require(RootFolder.Factories.Map)
local Is = require(RootFolder.Functions.Is)
local Peek = require(RootFolder.Functions.Peek)
local Array = require(RootFolder.Functions.Array)
type Blueprint = { T: ECS.Entity, V: any }
local module = {}
local function blueprintTreeFromValue(value: any): Blueprint
if Is.Stateful(value) then
if Is.StatefulTable(value) then
return { T = ECS.Tags.IsStatefulTable, V = Peek(value) }
elseif Is.StatefulDictionary(value) then
return { T = ECS.Tags.IsStatefulDictionary, V = Peek(value) }
else
return { T = ECS.Tags.IsStateful, V = Peek(value) }
end
elseif typeof(value) == "table" then
local childrenAsBlueprints = Array.ShallowTransform(value, function(k, v)
return blueprintTreeFromValue(v)
end)
return { T = ECS.Tags.IsStatic, V = childrenAsBlueprints }
else
return { T = ECS.Tags.IsStatic, V = value }
end
end
function module:From(value: any): Blueprint
return blueprintTreeFromValue(value)
end
local buildFromBlueprintTree
local function buildFromABlueprint(blueprint: Blueprint)
if blueprint.T == ECS.Tags.IsStateful then
return Value(blueprint.V)
elseif blueprint.T == ECS.Tags.IsStatefulTable then
return Table(blueprint.V)
elseif blueprint.T == ECS.Tags.IsStatefulDictionary then
return Map(blueprint.V)
elseif blueprint.T == ECS.Tags.IsStatic then
if typeof(blueprint.V) == "table" then
return buildFromBlueprintTree(blueprint.V)
else
return blueprint.V
end
end
return nil
end
buildFromBlueprintTree = function(blueprintTable: {any})
return Array.ShallowTransform(blueprintTable, function(key, value)
return buildFromABlueprint(value)
end)
end
function module:Read(rootBlueprint: Blueprint)
return buildFromABlueprint(rootBlueprint)
end
return module

View file

@ -0,0 +1,57 @@
export type Change = {
Path: {string | number},
OldValue: any,
NewValue: any,
}
local DeepCompare = {}
local function compare(oldTable, newTable, path, changes)
path = path or {}
changes = changes or {}
for key, newValue in pairs(newTable) do
local oldValue = oldTable and oldTable[key]
local currentPath = table.clone(path)
table.insert(currentPath, key)
if oldValue ~= newValue then
if typeof(newValue) == "table" and typeof(oldValue) == "table" then
compare(oldValue, newValue, currentPath, changes)
else
table.insert(changes, {
Path = currentPath,
OldValue = oldValue,
NewValue = newValue,
})
end
end
end
if oldTable then
for key, oldValue in pairs(oldTable) do
if newTable[key] == nil then
local currentPath = table.clone(path)
table.insert(currentPath, key)
table.insert(changes, {
Path = currentPath,
OldValue = oldValue,
NewValue = nil,
})
end
end
end
return changes
end
--- Compares two tables deeply and returns an array of changes.
-- Each change object contains a `Path`, `OldValue`, and `NewValue`.
return function(oldTable: {}, newTable: {}): {Change}
if typeof(oldTable) ~= "table" or typeof(newTable) ~= "table" then
return {}
end
return compare(oldTable, newTable)
end

View file

@ -0,0 +1,147 @@
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local Symbols = require(RootFolder.Symbols)
local Effect = require(RootFolder.Factories.Effect)
local Is = require(RootFolder.Functions.Is)
local Has = require(RootFolder.Functions.Has)
local GetInScope = require(RootFolder.Functions.GetInScope)
local JECS = ECS.JECS
local World = ECS.World
local Components = ECS.Components
local Tags = ECS.Tags
local RESERVED_KEYS = { "Children", "Parent" }
local INSTANCE_TO_ENTITY = setmetatable({}, { __mode = "k" })
local function applyProperty(instance, prop, value)
instance[prop] = value
end
local function bindEvent(instance: Instance, instanceEntity: ECS.Entity, event, callback)
local connection = instance[event]:Connect(callback)
local connectionList = World:get(instanceEntity, Components.ConnectionList)
table.insert(connectionList, connection)
World:set(instanceEntity, Components.ConnectionList, connectionList)
end
local function bindChange(instance: Instance, instanceEntity: ECS.Entity, prop, action)
local connection
if Is.Settable(action) then
connection = instance:GetPropertyChangedSignal(prop):Connect(function(...: any) action:set(instance[prop]) end)
else
connection = instance:GetPropertyChangedSignal(prop):Connect(action)
end
local connectionList = World:get(instanceEntity, Components.ConnectionList)
table.insert(connectionList, connection)
World:set(instanceEntity, Components.ConnectionList, connectionList)
end
local function bindReactive(instance: Instance, instanceEntity: ECS.Entity, prop, value): Effect.Effect
local propType = typeof(instance[prop])
local propIsString = propType == "string"
local propEffect = Effect(function()
local currentValue = value:get()
if propIsString and typeof(currentValue) ~= "string" then
instance[prop] = tostring(currentValue)
else
instance[prop] = currentValue
end
end)
applyProperty(instance, prop, value:get())
World:add(propEffect.entity, JECS.pair(Tags.InScope, instanceEntity))
end
local function applyVirtualNode(instance: Instance, instanceEntity: ECS.Entity, properties: {})
for key, value in properties do
if table.find(RESERVED_KEYS, key) then continue end
if Is.Symbol(key) then
if Has.Symbol("Event", key) then
if Is.Stateful(value) then error("Chemical OnEvent Error: Chemical does not currently support Stateful values.") end
if typeof(value) ~= "function" then error("Chemical OnEvent Error: can only be bound to a callback", 2) end
bindEvent(instance, instanceEntity, key.Symbol, value)
elseif Has.Symbol("Change", key) then
if typeof(value) ~= "function"
and not Is.Settable(value) then error("Chemical OnChange Error: can only be bound to a callback or settable Stateful object.", 2) end
bindChange(instance, instanceEntity, key.Symbol, value)
elseif Has.Symbol("Children", key) then
for _, child in value do
child.Parent = instance
end
end
elseif Is.Stateful(value) then
bindReactive(instance, instanceEntity, key, value)
elseif Is.Literal(value) then
applyProperty(instance, key, value)
end
end
end
local function Compose(target: string | Instance)
return function(properties: {})
local instance: Instance
local instanceEntity: ECS.Entity
if typeof(target) == "string" then
instance = Instance.new(target)
instanceEntity = World:entity()
World:add(instanceEntity, Tags.IsHost)
World:set(instanceEntity, Components.Instance, instance)
World:set(instanceEntity, Components.ConnectionList, {})
World:set(instanceEntity, Components.Connection, instance.Destroying:Once(function()
INSTANCE_TO_ENTITY[instance] = nil
if World:contains(instanceEntity) then
World:delete(instanceEntity)
end
end))
else
instance = target
instanceEntity = INSTANCE_TO_ENTITY[instance]
if not instanceEntity or not World:contains(instanceEntity) then
instanceEntity = World:entity()
INSTANCE_TO_ENTITY[instance] = instanceEntity
World:add(instanceEntity, Tags.IsHost)
World:set(instanceEntity, Components.Instance, instance)
World:set(instanceEntity, Components.ConnectionList, {})
World:set(instanceEntity, Components.Connection, instance.Destroying:Once(function()
INSTANCE_TO_ENTITY[instance] = nil
if World:contains(instanceEntity) then
World:delete(instanceEntity)
end
end))
end
end
applyVirtualNode(instance, instanceEntity, properties)
if properties.Parent and not instance.Parent then
instance.Parent = properties.Parent
end
return instance
end
end
return Compose

View file

@ -0,0 +1,38 @@
export type Destroyable = Computed | Value | Observer | { destroy: (self: {}) -> () } | Instance | RBXScriptConnection | { Destroyable } | () -> () | thread
local function Destroy(subject: Destroyable )
if typeof(subject) == "table" then
if subject.destroy then
subject:destroy()
return
end
if subject.Destroy then
subject:Destroy()
return
end
if getmetatable(subject) then
setmetatable(subject, nil)
table.clear(subject)
return
end
for _, value in subject do
Destroy(value)
end
elseif typeof(subject) == "Instance" then
subject:Destroy()
elseif typeof(subject) == "RBXScriptConnection" then
subject:Disconnect()
elseif typeof(subject) == "function" then
subject()
elseif typeof(subject) == "thread" then
task.cancel(subject)
end
end
return Destroy

View file

@ -0,0 +1,13 @@
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local function getInScope(entity: ECS.Entity)
local scoped = {}
for scopedEntity in ECS.World:each(ECS.JECS.pair(ECS.Tags.InScope, entity)) do
table.insert(scoped, scopedEntity)
end
return scoped
end
return getInScope

View file

@ -0,0 +1,20 @@
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local function getSubscribers(entity: ECS.Entity)
local subscribers = {}
local subscriberQuery = ECS.World:query(ECS.Components.Object)
:with(ECS.JECS.pair(ECS.Tags.SubscribesTo, entity))
for subscriberEntity, _ in subscriberQuery:iter() do
table.insert(subscribers, subscriberEntity)
end
return subscribers
end
return getSubscribers

View file

@ -0,0 +1,14 @@
local RootFolder = script.Parent.Parent
local symbolMap = {}
export type Symbol<S, T> = { Symbol: S, Type: T }
return function<S, T>(symbolName: S, symbolType: T): Symbol<S, T>
if symbolMap[symbolName] then return symbolMap[symbolName] end
local symbol = { Symbol = symbolName, Type = symbolType }
symbolMap[symbolName] = symbol
return symbol
end

View file

@ -0,0 +1,12 @@
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local module = {}
function module.Symbol(typeOf: string, obj: {}): boolean
return obj.Type == typeOf
end
return module

View file

@ -0,0 +1,83 @@
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local module = {}
function module.Stateful(obj: any): boolean
if typeof(obj) == "table" and obj.entity then
return ECS.World:has(obj.entity, ECS.Tags.IsStateful)
end
return false
end
function module.Settable(obj: any): boolean
if typeof(obj) == "table" and obj.entity then
return ECS.World:has(obj.entity, ECS.Tags.IsSettable)
end
return false
end
function module.Primitive(obj: any): boolean
local typeA = typeof(obj)
return typeA ~= "table" and typeA ~= "userdata"
end
function module.Literal(obj: any): boolean
local typeA = typeof(obj)
return typeA ~= "table" and typeA ~= "userdata" and typeA ~= "thread" and typeA ~= "function" and typeA ~= "Instance"
end
function module.Symbol(obj: any, typeOf: string?): boolean
local is = typeof(obj) == "table" and obj.Type and obj.Symbol
return typeOf == nil and is or is and obj.Type == typeOf
end
function module.Array(obj: any): boolean
return typeof(obj) == "table" and obj.entity == nil
end
function module.StatefulTable(obj: any): boolean
if typeof(obj) == "table" and obj.entity then
return ECS.World:has(obj.entity, ECS.Tags.IsStateful) and ECS.World:has(obj.entity, ECS.Tags.IsStatefulTable)
end
return false
end
function module.StatefulDictionary(obj: any): boolean
if typeof(obj) == "table" and obj.entity then
return ECS.World:has(obj.entity, ECS.Tags.IsStateful) and ECS.World:has(obj.entity, ECS.Tags.IsStatefulDictionary)
end
return false
end
function module.Blueprint(obj: any): boolean
if typeof(obj) == "table" and obj.T and obj.V then
return true
end
return false
end
function module.Dead(obj: any)
return typeof(obj) == "table" and obj.__destroyed
end
function module.Destroyed(obj: Instance): boolean
if obj.Parent == nil then
local Success, Error = pcall(function()
obj.Parent = UserSettings() :: any
end)
return Error ~= "Not allowed to add that under settings"
end
return false
end
return module

View file

@ -0,0 +1,15 @@
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local Is = require(RootFolder.Functions.Is)
--- Peek
-- View a stateful's value without triggering and scoped dependencies/subscriptions.
return function(obj: any): any?
if Is.Stateful(obj) then
return ECS.World:get(obj.entity, ECS.Components.Value)
end
return nil
end

View file

@ -0,0 +1,21 @@
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local Array = require(RootFolder.Functions.Array)
local Is = require(RootFolder.Functions.Is)
local function setInScope(scopable: {ECS.Entity} | ECS.Entity, entity: ECS.Entity)
if Is.Stateful(scopable) then
ECS.World:add(scopable.entity, ECS.JECS.pair(ECS.Tags.InScope, entity))
return
end
Array.Traverse(scopable, function(k, v)
if Is.Stateful(v) then
ECS.World:add(v.entity, ECS.JECS.pair(ECS.Tags.InScope, entity))
end
end)
end
return setInScope

View file

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

View file

@ -0,0 +1,19 @@
--!strict
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local Types = require(RootFolder.Types)
export type Cleanable = Types.HasEntity & {
clean: (self: Cleanable) -> (),
}
return {
clean = function(self: Cleanable)
local cleanupFn = ECS.World:get(self.entity, ECS.Components.CleanupFn)
if cleanupFn then
cleanupFn()
end
end,
}

View file

@ -0,0 +1,53 @@
--!strict
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local Cache = require(RootFolder.Cache)
local Types = require(RootFolder.Types)
local GetSubscribers = require(RootFolder.Functions.GetSubscribers)
export type Computable<T = any> = Types.HasEntity & {
compute: (self: Computable<T>) -> (),
}
type MaybeCleanable = {
clean: (self: MaybeCleanable) -> ()
}
return {
compute = function(self: Computable & MaybeCleanable)
local computeFn = ECS.World:get(self.entity, ECS.Components.ComputeFn)
if not computeFn then return end
local oldValue = ECS.World:get(self.entity, ECS.Components.Value)
local cleanupFn = ECS.World:get(self.entity, ECS.Components.CleanupFn)
if oldValue and cleanupFn then
cleanupFn(oldValue)
end
Cache.Stack:Push(self.entity)
local s, result = pcall(computeFn)
Cache.Stack:Pop()
if not s then
warn("Chemical Computed Error: ", result)
return
end
if result ~= oldValue then
ECS.World:set(self.entity, ECS.Components.PrevValue, oldValue)
ECS.World:set(self.entity, ECS.Components.Value, result)
local subscribers = GetSubscribers(self.entity)
for _, subscriberEntity in ipairs(subscribers) do
if not ECS.World:has(subscriberEntity, ECS.Tags.IsDirty) then
ECS.World:add(subscriberEntity, ECS.Tags.IsDirty)
end
end
end
end,
}

View file

@ -0,0 +1,37 @@
--!strict
local RootFolder = script.Parent.Parent
local Types = require(RootFolder.Types)
local ECS = require(RootFolder.ECS)
export type Destroyable = Types.HasEntity & {
__destroyed: boolean,
destroy: (self: Destroyable) -> (),
--__internalDestroy: (self: Destroyable) -> (),
}
local methods = {}
function methods:__internalDestroy()
if self.__destroyed then return end
self.__destroyed = true
local cleanupFn = ECS.World:get(self.entity, ECS.Components.CleanupFn)
if cleanupFn then
cleanupFn()
ECS.World:remove(self.entity, ECS.Components.CleanupFn)
end
end
function methods:destroy()
if self.__destroyed then return end
self:__internalDestroy()
ECS.World:delete(self.entity)
end
return methods

View file

@ -0,0 +1,33 @@
--!strict
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local Cache = require(RootFolder.Cache)
local Types = require(RootFolder.Types)
export type Effectable = Types.HasEntity & {
run: (self: Effectable) -> (),
}
return {
run = function(self: Effectable)
local effectFn = ECS.World:get(self.entity, ECS.Components.EffectFn)
if not effectFn then return end
local oldCleanupFn = ECS.World:get(self.entity, ECS.Components.CleanupFn)
if oldCleanupFn then
oldCleanupFn()
ECS.World:remove(self.entity, ECS.Components.CleanupFn)
end
Cache.Stack:Push(self.entity)
local newCleanupFn = effectFn()
Cache.Stack:Pop()
if newCleanupFn then
ECS.World:set(self.entity, ECS.Components.CleanupFn, newCleanupFn)
end
end,
}

View file

@ -0,0 +1,28 @@
--!strict
local RootFolder = script.Parent.Parent
local Cache = require(RootFolder.Cache)
local ECS = require(RootFolder.ECS)
local Types = require(RootFolder.Types)
export type Numerical = Types.HasEntity & {
increment: (self: Numerical, n: number) -> (),
decrement: (self: Numerical, n: number) -> ()
}
return {
increment = function(self: Numerical, n: number)
local cachedValue = ECS.World:get(self.entity, ECS.Components.Value)
ECS.World:set(self.entity, ECS.Components.PrevValue, cachedValue)
ECS.World:set(self.entity, ECS.Components.Value, cachedValue + n)
end,
decrement = function(self: Numerical, n: number)
local cachedValue = ECS.World:get(self.entity, ECS.Components.Value)
ECS.World:set(self.entity, ECS.Components.PrevValue, cachedValue)
ECS.World:set(self.entity, ECS.Components.Value, cachedValue - n)
end,
}

View file

@ -0,0 +1,103 @@
--!strict
local RootFolder = script.Parent.Parent
local LinkedList = require(RootFolder.Packages.LinkedList)
local Types = require(RootFolder.Types)
local ECS = require(RootFolder.ECS)
local DeepCompare = require(RootFolder.Functions.Compare)
export type Observable<T = any> = Types.HasEntity & {
__destroyed: boolean,
onChange: (self: Observable<T>, callback: (new: T, old: T) -> ()) -> { disconnect: () -> () },
run: (self: Observable<T>) -> (),
destroy: (self: Observable<T>) -> (),
}
export type ObservableTable<T = {}> = Observable<T> & {
onKVChange: (self: ObservableTable<T>, callback: (path: {string|number}, new: any, old: any) -> ()) -> { disconnect: () -> () },
}
return {
onChange = function(self: Observable, callback: (any, any) -> ())
local callbackList = ECS.World:get(self.entity, ECS.Components.OnChangeCallbacks)
callbackList:InsertBack(callback)
return {
disconnect = function()
callbackList:Remove(callback)
end,
}
end,
onKVChange = function(self: Observable, callback: (path: {any}, any, any) -> ())
local kvCallbackList = ECS.World:get(self.entity, ECS.Components.OnKVChangeCallbacks)
kvCallbackList:InsertBack(callback)
return {
disconnect = function()
kvCallbackList:Remove(callback)
end,
}
end,
run = function(self: Observable)
local sourceEntity = ECS.World:target(self.entity, ECS.Tags.SubscribesTo)
if not sourceEntity then return end
local newValue = ECS.World:get(sourceEntity, ECS.Components.Value)
local oldValue = ECS.World:get(sourceEntity, ECS.Components.PrevValue)
local callbacksList = ECS.World:get(self.entity, ECS.Components.OnChangeCallbacks)
if callbacksList then
for link, callback in callbacksList:IterateForward() do
local s, err = pcall(callback, newValue, oldValue)
if not s then warn("Chemical Observer Error: onChange: ", err) end
end
end
if ECS.World:has(sourceEntity, ECS.Tags.IsDeepComparable) then
local kvCallbackList = ECS.World:get(self.entity, ECS.Components.OnKVChangeCallbacks)
if kvCallbackList then
local changes = DeepCompare(oldValue, newValue)
for _, change in ipairs(changes) do
for link, callback in kvCallbackList:IterateForward() do
local s, err = pcall(callback, change.Path, change.NewValue, change.OldValue)
if not s then warn("Chemical Observer Error: onKVChange: ", err) end
end
end
end
end
end,
__internalDestroy = function(self: Observable & Types.MaybeCleanable)
if self.__destroyed then return end
self.__destroyed = true
local callbacksList = ECS.World:get(self.entity, ECS.Components.OnChangeCallbacks)
if callbacksList then callbacksList:Destroy() end
local kvCallbackList = ECS.World:get(self.entity, ECS.Components.OnKVChangeCallbacks)
if kvCallbackList then kvCallbackList:Destroy() end
if self.clean then self:clean() end
setmetatable(self, nil)
end,
destroy = function(self: Observable & Types.MaybeCleanable & Types.MaybeDestroyable)
if self.__destroyed then return end
self:__internalDestroy()
ECS.World:delete(self.entity)
end,
}

View file

@ -0,0 +1,73 @@
--!strict
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local Cache = require(RootFolder.Cache)
local Types = require(RootFolder.Types)
local Is = require(RootFolder.Functions.Is)
local Peek = require(RootFolder.Functions.Peek)
local Array = require(RootFolder.Functions.Array)
local Blueprint = require(RootFolder.Functions.Blueprint)
export type Serializable = Types.HasEntity & {
serialize: (self: Serializable) -> (any),
snapshot: (self: Serializable) -> (any),
blueprint: (self: Serializable) -> (any | { any }),
}
return {
serialize = function(self: Serializable): any
local value = ECS.World:get(self.entity, ECS.Components.Value)
local serialized
if Is.Stateful(value) then
local theValue = Peek(value)
serialized = Is.Primitive(theValue) and theValue
elseif Is.Array(value) then
serialized = Array.Transform(value, function(k, v)
if Is.Stateful(v) then
local theValue = Peek(v)
return Is.Primitive(theValue) and theValue or nil
elseif Is.Primitive(v) then
return v
end
return nil
end)
elseif Is.Primitive(value) then
serialized = value
end
return serialized, if not serialized then warn("There was nothing to serialize, or the value was unserializable.") else nil
end,
snapshot = function(self: Serializable): any
local value = ECS.World:get(self.entity, ECS.Components.Value)
if Is.Stateful(value) then
return Peek(value)
elseif Is.Array(value) then
return Array.Transform(value, function(k, v)
if Is.Stateful(v) then
local theValue = Peek(v)
return Peek(v)
elseif Is.Primitive(v) then
return v
end
return nil
end)
else
return value
end
end,
blueprint = function(self: Serializable): any | { any }
local value = ECS.World:get(self.entity, ECS.Components.Value)
return Blueprint:From(value)
end,
}

View file

@ -0,0 +1,24 @@
--!strict
local RootFolder = script.Parent.Parent
local Cache = require(RootFolder.Cache)
local ECS = require(RootFolder.ECS)
local Types = require(RootFolder.Types)
export type Settable<T = any> = Types.HasEntity & {
set: (self: Settable<T>, T) -> ()
}
return {
set = function(self: Settable, value: any)
local cachedValue = ECS.World:get(self.entity, ECS.Components.Value)
if value == cachedValue then
return
end
ECS.World:set(self.entity, ECS.Components.PrevValue, cachedValue)
ECS.World:set(self.entity, ECS.Components.Value, value)
end,
}

View file

@ -0,0 +1,23 @@
--!strict
local RootFolder = script.Parent.Parent
local Cache = require(RootFolder.Cache)
local ECS = require(RootFolder.ECS)
local Types = require(RootFolder.Types)
export type Stateful<T = any> = Types.HasEntity & {
get: (self: Stateful<T>) -> (T)
}
return {
get = function(self: Stateful)
local withinEntity = Cache.Stack:Top()
if withinEntity then
ECS.World:add(withinEntity, ECS.JECS.pair(ECS.Tags.SubscribesTo, self.entity))
ECS.World:add(self.entity, ECS.JECS.pair(ECS.Tags.HasSubscriber, withinEntity))
end
return ECS.World:get(self.entity, ECS.Components.Value)
end,
}

View file

@ -0,0 +1,52 @@
--!strict
local RootFolder = script.Parent.Parent
local Cache = require(RootFolder.Cache)
local ECS = require(RootFolder.ECS)
local Types = require(RootFolder.Types)
local Destroy = require(RootFolder.Functions:FindFirstChild("Destroy"))
export type StatefulDictionary<T = {}> = Types.HasEntity & {
key: <K, V>(self: StatefulDictionary<T>, key: K, value: V?) -> (),
clear: <V>(self: StatefulDictionary<T>, cleanup: (value: V) -> ()?) -> (any?),
}
local function recursive(tbl, func)
for key, value in tbl do
if typeof(value) == "table" and not value.type then
recursive(value, func)
continue
end
func(value)
end
end
return {
key = function<T, V>(self: StatefulDictionary, key: T, value: V?)
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
local newTbl = table.clone(tbl)
newTbl[key] = value
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
end,
clear = function(self: StatefulDictionary, cleanup: (any) -> ())
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
local newTbl = table.clone(tbl)
if cleanup then
recursive(newTbl, cleanup)
end
newTbl = {}
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
end,
}

View file

@ -0,0 +1,71 @@
--!strict
local RootFolder = script.Parent.Parent
local Cache = require(RootFolder.Cache)
local ECS = require(RootFolder.ECS)
local Types = require(RootFolder.Types)
export type StatefulTable<T = {}> = Types.HasEntity & {
insert: <V>(self: StatefulTable<T>, value: V) -> (),
remove: <V>(self: StatefulTable<T>, value: V) -> (),
find: <V>(self: StatefulTable<T>, value: V) -> (number)?,
setAt: <V>(self: StatefulTable<T>, index: number, value: V) -> (),
getAt: (self: StatefulTable<T>, index: number) -> (any?),
clear: (self: StatefulTable<T>) -> (),
}
return {
insert = function<T>(self: StatefulTable, value: T)
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
local newTbl = table.clone(tbl)
table.insert(newTbl, value)
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
end,
remove = function<T>(self: StatefulTable, value: T)
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
local newTbl = table.clone(tbl)
local index = table.find(newTbl, value)
local poppedValue = table.remove(newTbl, index)
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
return poppedValue
end,
find = function<T>(self: StatefulTable, value: T)
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
local found = table.find(tbl, value)
return found
end,
setAt = function<T>(self: StatefulTable, index: number, value: T)
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
local newTbl = table.clone(tbl)
newTbl[index] = value
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
end,
getAt = function(self: StatefulTable, index: number)
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
return tbl[index]
end,
clear = function(self: StatefulTable)
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
local newTbl = table.clone(tbl)
table.clear(newTbl)
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
end,
}

View file

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

View file

@ -0,0 +1,52 @@
-- Variables
local Constructor = {}
local Index, NewIndex
-- Types
export type Constructor = {
new: (data: {[any]: any}, public: {[any]: any}?) -> ({}, {[any]: any}),
}
-- Constructor
Constructor.new = function(data, public)
local proxy = newproxy(true)
local metatable = getmetatable(proxy)
for index, value in data do metatable[index] = value end
metatable.__index = Index
metatable.__newindex = NewIndex
metatable.__public = public or {}
return proxy, metatable
end
-- Functions
Index = function(proxy, index)
local metatable = getmetatable(proxy)
local public = metatable.__public[index]
return if public == nil then metatable.__shared[index] else public
end
NewIndex = function(proxy, index, value)
local metatable = getmetatable(proxy)
local set = metatable.__set[index]
if set == nil then
metatable.__public[index] = value
elseif set == false then
error("Attempt to modify a readonly value", 2)
else
set(proxy, metatable, value)
end
end
return table.freeze(Constructor) :: Constructor

View file

@ -0,0 +1,219 @@
-- Variables
local Proxy = require(script.Parent.Proxy)
local Constructor, Signal, Connection = {}, {}, {}
local Thread, Call
local threads = {}
-- Types
export type Constructor = {
new: () -> Signal,
}
export type Signal = {
[any]: any,
Connections: number,
Connected: (connected: boolean, signal: Signal) -> ()?,
Connect: (self: Signal, func: (...any) -> (), ...any) -> Connection,
Once: (self: Signal, func: (...any) -> (), ...any) -> Connection,
Wait: (self: Signal, ...any) -> ...any,
Fire: (self: Signal, ...any) -> (),
FastFire: (self: Signal, ...any) -> (),
DisconnectAll: (self: Signal) -> (),
}
export type Connection = {
[any]: any,
Signal: Signal?,
Disconnect: (self: Connection) -> (),
}
-- Constructor
Constructor.new = function()
local proxy, signal = Proxy.new(Signal, {Connections = 0})
return proxy
end
-- Signal
Signal.__tostring = function(proxy)
return "Signal"
end
Signal.__shared = {
Connect = function(proxy, func, ...)
local signal = getmetatable(proxy)
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to Connect failed: Passed value is not a Signal", 3) end
if type(func) ~= "function" then error("Attempt to Connect failed: Passed value is not a function", 3) end
signal.__public.Connections += 1
local connectionProxy, connection = Proxy.new(Connection, {Signal = proxy})
connection.FunctionOrThread = func
connection.Parameters = if ... == nil then nil else {...}
if signal.Last == nil then signal.First, signal.Last = connection, connection else connection.Previous, signal.Last.Next, signal.Last = signal.Last, connection, connection end
if signal.__public.Connections == 1 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, true, proxy) end
return connectionProxy
end,
Once = function(proxy, func, ...)
local signal = getmetatable(proxy)
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to Connect failed: Passed value is not a Signal", 3) end
if type(func) ~= "function" then error("Attempt to Connect failed: Passed value is not a function", 3) end
signal.__public.Connections += 1
local connectionProxy, connection = Proxy.new(Connection, {Signal = proxy})
connection.FunctionOrThread = func
connection.Once = true
connection.Parameters = if ... == nil then nil else {...}
if signal.Last == nil then signal.First, signal.Last = connection, connection else connection.Previous, signal.Last.Next, signal.Last = signal.Last, connection, connection end
if signal.__public.Connections == 1 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, true, proxy) end
return connectionProxy
end,
Wait = function(proxy, ...)
local signal = getmetatable(proxy)
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to Connect failed: Passed value is not a Signal", 3) end
signal.__public.Connections += 1
local connectionProxy, connection = Proxy.new(Connection, {Signal = proxy})
connection.FunctionOrThread = coroutine.running()
connection.Once = true
connection.Parameters = if ... == nil then nil else {...}
if signal.Last == nil then signal.First, signal.Last = connection, connection else connection.Previous, signal.Last.Next, signal.Last = signal.Last, connection, connection end
if signal.__public.Connections == 1 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, true, proxy) end
return coroutine.yield()
end,
Fire = function(proxy, ...)
local signal = getmetatable(proxy)
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to connect failed: Passed value is not a Signal", 3) end
local connection = signal.First
while connection ~= nil do
if connection.Once == true then
signal.__public.Connections -= 1
connection.__public.Signal = nil
if signal.First == connection then signal.First = connection.Next end
if signal.Last == connection then signal.Last = connection.Previous end
if connection.Previous ~= nil then connection.Previous.Next = connection.Next end
if connection.Next ~= nil then connection.Next.Previous = connection.Previous end
if signal.__public.Connections == 0 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, false, proxy) end
end
if type(connection.FunctionOrThread) == "thread" then
if connection.Parameters == nil then
task.spawn(connection.FunctionOrThread, ...)
else
local parameters = {...}
task.spawn(connection.FunctionOrThread, table.unpack(table.move(connection.Parameters, 1, #connection.Parameters, #parameters + 1, parameters)))
end
else
local thread = table.remove(threads)
if thread == nil then thread = coroutine.create(Thread) coroutine.resume(thread) end
if connection.Parameters == nil then
task.spawn(thread, thread, connection.FunctionOrThread, ...)
else
local parameters = {...}
task.spawn(thread, thread, connection.FunctionOrThread, table.unpack(table.move(connection.Parameters, 1, #connection.Parameters, #parameters + 1, parameters)))
end
end
connection = connection.Next
end
end,
FastFire = function(proxy, ...)
local signal = getmetatable(proxy)
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to connect failed: Passed value is not a Signal", 3) end
local connection = signal.First
while connection ~= nil do
if connection.Once == true then
signal.__public.Connections -= 1
connection.__public.Signal = nil
if signal.First == connection then signal.First = connection.Next end
if signal.Last == connection then signal.Last = connection.Previous end
if connection.Previous ~= nil then connection.Previous.Next = connection.Next end
if connection.Next ~= nil then connection.Next.Previous = connection.Previous end
if signal.__public.Connections == 0 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, false, proxy) end
end
if type(connection.FunctionOrThread) == "thread" then
if connection.Parameters == nil then
coroutine.resume(connection.FunctionOrThread, ...)
else
local parameters = {...}
coroutine.resume(connection.FunctionOrThread, table.unpack(table.move(connection.Parameters, 1, #connection.Parameters, #parameters + 1, parameters)))
end
else
if connection.Parameters == nil then
connection.FunctionOrThread(...)
else
local parameters = {...}
connection.FunctionOrThread(table.unpack(table.move(connection.Parameters, 1, #connection.Parameters, #parameters + 1, parameters)))
end
end
connection = connection.Next
end
end,
DisconnectAll = function(proxy)
local signal = getmetatable(proxy)
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to Connect failed: Passed value is not a Signal", 3) end
local connection = signal.First
if connection == nil then return end
while connection ~= nil do
connection.__public.Signal = nil
if type(connection.FunctionOrThread) == "thread" then task.cancel(connection.FunctionOrThread) end
connection = connection.Next
end
if signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, false, proxy) end
signal.__public.Connections, signal.First, signal.Last = 0, nil, nil
end,
}
Signal.__set = {
Connections = false,
}
-- Connection
Connection.__tostring = function(proxy)
return "Connection"
end
Connection.__shared = {
Disconnect = function(proxy)
local connection = getmetatable(proxy)
if type(connection) ~= "table" or connection.__shared ~= Connection.__shared then error("Attempt to Disconnect failed: Passed value is not a Connection", 3) end
local signal = getmetatable(connection.__public.Signal)
if signal == nil then return end
signal.__public.Connections -= 1
connection.__public.Signal = nil
if signal.First == connection then signal.First = connection.Next end
if signal.Last == connection then signal.Last = connection.Previous end
if connection.Previous ~= nil then connection.Previous.Next = connection.Next end
if connection.Next ~= nil then connection.Next.Previous = connection.Previous end
if type(connection.FunctionOrThread) == "thread" then task.cancel(connection.FunctionOrThread) end
if signal.__public.Connections == 0 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, false, proxy) end
end,
}
Connection.__set = {
Signal = false,
}
-- Functions
Thread = function()
while true do Call(coroutine.yield()) end
end
Call = function(thread, func, ...)
func(...)
if #threads >= 16 then return end
table.insert(threads, thread)
end
return table.freeze(Constructor) :: Constructor

View file

@ -0,0 +1,266 @@
-- Variables
local Proxy = require(script.Parent.Proxy)
local Constructor, TaskManager, SynchronousTask, RunningTask = {}, {}, {}, {}
local Run
-- Types
export type Constructor = {
new: () -> TaskManager,
}
export type TaskManager = {
[any]: any,
Enabled: boolean,
Tasks: number,
Running: SynchronousTask?,
InsertFront: (self: TaskManager, func: (RunningTask, ...any) -> (), ...any) -> SynchronousTask,
InsertBack: (self: TaskManager, func: (RunningTask, ...any) -> (), ...any) -> SynchronousTask,
FindFirst: (self: TaskManager, func: (RunningTask, ...any) -> ()) -> (SynchronousTask?, number?),
FindLast: (self: TaskManager, func: (RunningTask, ...any) -> ()) -> (SynchronousTask?, number?),
CancelAll: (self: TaskManager, func: (RunningTask, ...any) -> ()?) -> (),
}
export type SynchronousTask = {
[any]: any,
TaskManager: TaskManager?,
Running: boolean,
Wait: (self: SynchronousTask, ...any) -> ...any,
Cancel: (self: SynchronousTask) -> (),
}
export type RunningTask = {
Next: (self: RunningTask) -> (thread, ...any),
Iterate: (self: RunningTask) -> ((self: RunningTask) -> (thread, ...any), RunningTask),
End: (self: RunningTask) -> (),
}
-- Constructor
Constructor.new = function()
local proxy, taskManager = Proxy.new(TaskManager, {Enabled = true, Tasks = 0})
taskManager.Active = false
return proxy
end
-- TaskManager
TaskManager.__tostring = function(proxy)
return "Task Manager"
end
TaskManager.__shared = {
InsertFront = function(proxy, func, ...)
local taskManager = getmetatable(proxy)
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to InsertFront failed: Passed value is not a Task Manager", 3) end
if type(func) ~= "function" then error("Attempt to InsertFront failed: Passed value is not a function", 3) end
taskManager.__public.Tasks += 1
local proxy, synchronousTask = Proxy.new(SynchronousTask, {TaskManager = proxy, Running = false})
synchronousTask.Active = true
synchronousTask.Function = func
synchronousTask.Parameters = if ... == nil then nil else {...}
if taskManager.First == nil then taskManager.First, taskManager.Last = proxy, proxy else synchronousTask.Next, getmetatable(taskManager.First).Previous, taskManager.First = taskManager.First, proxy, proxy end
if taskManager.Active == false and taskManager.__public.Enabled == true then taskManager.Active = true task.defer(Run, taskManager) end
return proxy
end,
InsertBack = function(proxy, func, ...)
local taskManager = getmetatable(proxy)
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to InsertBack failed: Passed value is not a Task Manager", 3) end
if type(func) ~= "function" then error("Attempt to InsertBack failed: Passed value is not a function", 3) end
taskManager.__public.Tasks += 1
local proxy, synchronousTask = Proxy.new(SynchronousTask, {TaskManager = proxy, Running = false})
synchronousTask.Active = true
synchronousTask.Function = func
synchronousTask.Parameters = if ... == nil then nil else {...}
if taskManager.Last == nil then taskManager.First, taskManager.Last = proxy, proxy else synchronousTask.Previous, getmetatable(taskManager.Last).Next, taskManager.Last = taskManager.Last, proxy, proxy end
if taskManager.Active == false and taskManager.__public.Enabled == true then taskManager.Active = true task.defer(Run, taskManager) end
return proxy
end,
FindFirst = function(proxy, func)
local taskManager = getmetatable(proxy)
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to FindFirst failed: Passed value is not a Task Manager", 3) end
if type(func) ~= "function" then error("Attempt to FindFirst failed: Passed value is not a function", 3) end
proxy = taskManager.__public.Running
if proxy ~= nil then
local synchronousTask = getmetatable(proxy)
if synchronousTask.Active == true and synchronousTask.Function == func then return proxy, 0 end
end
local index = 1
proxy = taskManager.First
while proxy ~= nil do
local synchronousTask = getmetatable(proxy)
if synchronousTask.Function == func then return proxy, index end
proxy = synchronousTask.Next
index += 1
end
end,
FindLast = function(proxy, func)
local taskManager = getmetatable(proxy)
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to FindLast failed: Passed value is not a Task Manager", 3) end
if type(func) ~= "function" then error("Attempt to FindFirst failed: Passed value is not a function", 3) end
local index = if taskManager.__public.Running == nil then taskManager.__public.Tasks else taskManager.__public.Tasks - 1
proxy = taskManager.Last
while proxy ~= nil do
local synchronousTask = getmetatable(proxy)
if synchronousTask.Function == func then return proxy, index end
proxy = synchronousTask.Previous
index -= 1
end
proxy = taskManager.__public.Running
if proxy ~= nil then
local synchronousTask = getmetatable(proxy)
if synchronousTask.Active == true and synchronousTask.Function == func then return proxy, 0 end
end
end,
CancelAll = function(proxy, func)
local taskManager = getmetatable(proxy)
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to FindLast failed: Passed value is not a Task Manager", 3) end
if func == nil then
local proxy = taskManager.First
taskManager.First = nil
taskManager.Last = nil
if taskManager.__public.Running == nil then taskManager.__public.Tasks = 0 else taskManager.__public.Tasks = 1 end
while proxy ~= nil do
local synchronousTask = getmetatable(proxy)
proxy, synchronousTask.Active, synchronousTask.__public.TaskManager, synchronousTask.Previous, synchronousTask.Next = synchronousTask.Next, false, nil, nil, nil
end
else
if type(func) ~= "function" then error("Attempt to CancelAll failed: Passed value is not nil or function", 3) end
local proxy = taskManager.First
while proxy ~= nil do
local synchronousTask = getmetatable(proxy)
if synchronousTask.Function == func then
taskManager.__public.Tasks -= 1
if taskManager.First == proxy then taskManager.First = synchronousTask.Next end
if taskManager.Last == proxy then taskManager.Last = synchronousTask.Previous end
if synchronousTask.Previous ~= nil then getmetatable(synchronousTask.Previous).Next = synchronousTask.Next end
if synchronousTask.Next ~= nil then getmetatable(synchronousTask.Next).Previous = synchronousTask.Previous end
proxy, synchronousTask.Active, synchronousTask.__public.TaskManager, synchronousTask.Previous, synchronousTask.Next = synchronousTask.Next, false, nil, nil, nil
else
proxy = synchronousTask.Next
end
end
end
end,
}
TaskManager.__set = {
Enabled = function(proxy, taskManager, value)
if type(value) ~= "boolean" then error("Attempt to set Enabled failed: Passed value is not a boolean", 3) end
taskManager.__public.Enabled = value
if value == false or taskManager.First == nil or taskManager.Active == true then return end
taskManager.Active = true
task.defer(Run, taskManager)
end,
Tasks = false,
Running = false,
}
-- SynchronousTask
SynchronousTask.__tostring = function(proxy)
return "Synchronous Task"
end
SynchronousTask.__shared = {
Wait = function(proxy, ...)
local synchronousTask = getmetatable(proxy)
if type(synchronousTask) ~= "table" or synchronousTask.__shared ~= SynchronousTask.__shared then error("Attempt to Wait failed: Passed value is not a Synchronous Task", 3) end
if synchronousTask.Active == false then return end
local waiter = {coroutine.running(), ...}
if synchronousTask.Last == nil then synchronousTask.First, synchronousTask.Last = waiter, waiter else synchronousTask.Last.Next, synchronousTask.Last = waiter, waiter end
return coroutine.yield()
end,
Cancel = function(proxy)
local synchronousTask = getmetatable(proxy)
if type(synchronousTask) ~= "table" or synchronousTask.__shared ~= SynchronousTask.__shared then error("Attempt to Cancel failed: Passed value is not a Synchronous Task", 3) end
if synchronousTask.__public.Running == true then return false end
local taskManager = synchronousTask.__public.TaskManager
if taskManager == nil then return false end
taskManager = getmetatable(taskManager)
taskManager.__public.Tasks -= 1
if taskManager.First == proxy then taskManager.First = synchronousTask.Next end
if taskManager.Last == proxy then taskManager.Last = synchronousTask.Previous end
if synchronousTask.Previous ~= nil then getmetatable(synchronousTask.Previous).Next = synchronousTask.Next end
if synchronousTask.Next ~= nil then getmetatable(synchronousTask.Next).Previous = synchronousTask.Previous end
synchronousTask.Active, synchronousTask.__public.TaskManager, synchronousTask.Previous, synchronousTask.Next = false, nil, nil, nil
return true
end,
}
SynchronousTask.__set = {
TaskManager = false,
Running = false,
}
-- RunningTask
RunningTask.__tostring = function(proxy)
return "Running Task"
end
RunningTask.__shared = {
Next = function(proxy)
local runningTask = getmetatable(proxy)
if type(runningTask) ~= "table" or runningTask.__shared ~= RunningTask.__shared then error("Attempt to Next failed: Passed value is not a Running Task", 3) end
local synchronousTask = runningTask.SynchronousTask
local waiter = synchronousTask.First
if waiter == nil then return end
synchronousTask.First = waiter.Next
if synchronousTask.Last == waiter then synchronousTask.Last = nil end
return table.unpack(waiter)
end,
Iterate = function(proxy)
local runningTask = getmetatable(proxy)
if type(runningTask) ~= "table" or runningTask.__shared ~= RunningTask.__shared then error("Attempt to Iterate failed: Passed value is not a Running Task", 3) end
return runningTask.__shared.Next, proxy
end,
End = function(proxy)
local runningTask = getmetatable(proxy)
if type(runningTask) ~= "table" or runningTask.__shared ~= RunningTask.__shared then error("Attempt to End failed: Passed value is not a Running Task", 3) end
runningTask.SynchronousTask.Active = false
end,
}
RunningTask.__set = {
}
-- Functions
Run = function(taskManager)
if taskManager.__public.Enabled == false then taskManager.Active = false return end
local proxy = taskManager.First
if proxy == nil then taskManager.Active = false return end
local synchronousTask = getmetatable(proxy)
taskManager.__public.Running = proxy
taskManager.First = synchronousTask.Next
synchronousTask.__public.Running = true
if synchronousTask.Next == nil then taskManager.Last = nil else getmetatable(synchronousTask.Next).Previous = nil synchronousTask.Next = nil end
local proxy, runningTask = Proxy.new(RunningTask)
runningTask.SynchronousTask = synchronousTask
if synchronousTask.Parameters == nil then synchronousTask.Function(proxy) else synchronousTask.Function(proxy, table.unpack(synchronousTask.Parameters)) end
taskManager.__public.Tasks -= 1
taskManager.__public.Running = nil
synchronousTask.Active = false
synchronousTask.__public.TaskManager = nil
synchronousTask.__public.Running = false
if taskManager.__public.Enabled == false or taskManager.First == nil then taskManager.Active = false else task.defer(Run, taskManager) end
end
return table.freeze(Constructor) :: Constructor

View file

@ -0,0 +1,734 @@
-- Variables
local Proxy = require(script.Proxy)
local Signal = require(script.Signal)
local SynchronousTaskManager = require(script.SynchronousTaskManager)
local dataStoreService, memoryStoreService, httpService = game:GetService("DataStoreService"), game:GetService("MemoryStoreService"), game:GetService("HttpService")
local Constructor, DataStore = {}, {}
local OpenTask, ReadTask, LockTask, SaveTask, CloseTask, DestroyTask, Lock, Unlock, Load, Save, StartSaveTimer, StopSaveTimer, SaveTimerEnded, StartLockTimer, StopLockTimer, LockTimerEnded, ProcessQueue, SignalConnected, Clone, Reconcile, Compress, Decompress, Encode, Decode, BindToClose
local dataStores, bindToClose, active = {}, {}, true
local characters = {[0] = "0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","!","$","%","&","'",",",".","/",":",";","=","?","@","[","]","^","_","`","{","}","~"}
local bytes = {} for i = (0), #characters do bytes[string.byte(characters[i])] = i end
local base = #characters + 1
-- Types
export type Constructor = {
new: (name: string, scope: string, key: string?) -> DataStore,
hidden: (name: string, scope: string, key: string?) -> DataStore,
find: (name: string, scope: string, key: string?) -> DataStore?,
Response: {Success: string, Saved: string, Locked: string, State: string, Error: string},
}
export type DataStore = {
Value: any,
Metadata: {[string]: any},
UserIds: {any},
SaveInterval: number,
SaveDelay: number,
LockInterval: number,
LockAttempts: number,
SaveOnClose: boolean,
Id: string,
UniqueId: string,
Key: string,
State: boolean?,
Hidden: boolean,
AttemptsRemaining: number,
CreatedTime: number,
UpdatedTime: number,
Version: string,
CompressedValue: string,
StateChanged: Signal.Signal,
Saving: Signal.Signal,
Saved: Signal.Signal,
AttemptsChanged: Signal.Signal,
ProcessQueue: Signal.Signal,
Open: (self: DataStore, template: any?) -> (string, any),
Read: (self: DataStore, template: any?) -> (string, any),
Save: (self: DataStore) -> (string, any),
Close: (self: DataStore) -> (string, any),
Destroy: (self: DataStore) -> (string, any),
Queue: (self: DataStore, value: any, expiration: number?, priority: number?) -> (string, any),
Remove: (self: DataStore, id: string) -> (string, any),
Clone: (self: DataStore) -> any,
Reconcile: (self: DataStore, template: any) -> (),
Usage: (self: DataStore) -> (number, number),
}
-- Constructor
Constructor.new = function(name, scope, key)
if key == nil then key, scope = scope, "global" end
local id = name .. "/" .. scope .. "/" .. key
if dataStores[id] ~= nil then return dataStores[id] end
local proxy, dataStore = Proxy.new(DataStore, {
Metadata = {},
UserIds = {},
SaveInterval = 30,
SaveDelay = 0,
LockInterval = 60,
LockAttempts = 5,
SaveOnClose = true,
Id = id,
UniqueId = httpService:GenerateGUID(false),
Key = key,
State = false,
Hidden = false,
AttemptsRemaining = 0,
CreatedTime = 0,
UpdatedTime = 0,
Version = "",
CompressedValue = "",
StateChanged = Signal.new(),
Saving = Signal.new(),
Saved = Signal.new(),
AttemptsChanged = Signal.new(),
ProcessQueue = Signal.new(),
})
dataStore.TaskManager = SynchronousTaskManager.new()
dataStore.LockTime = -math.huge
dataStore.SaveTime = -math.huge
dataStore.ActiveLockInterval = 0
dataStore.ProcessingQueue = false
dataStore.DataStore = dataStoreService:GetDataStore(name, scope)
dataStore.MemoryStore = memoryStoreService:GetSortedMap(id)
dataStore.Queue = memoryStoreService:GetQueue(id)
dataStore.Options = Instance.new("DataStoreSetOptions")
dataStore.__public.ProcessQueue.DataStore = proxy
dataStore.__public.ProcessQueue.Connected = SignalConnected
dataStores[id] = proxy
if active == true then bindToClose[dataStore.__public.UniqueId] = proxy end
return proxy
end
Constructor.hidden = function(name, scope, key)
if key == nil then key, scope = scope, "global" end
local id = name .. "/" .. scope .. "/" .. key
local proxy, dataStore = Proxy.new(DataStore, {
Metadata = {},
UserIds = {},
SaveInterval = 30,
SaveDelay = 0,
LockInterval = 60,
LockAttempts = 5,
SaveOnClose = true,
Id = id,
UniqueId = httpService:GenerateGUID(false),
Key = key,
State = false,
Hidden = true,
AttemptsRemaining = 0,
CreatedTime = 0,
UpdatedTime = 0,
Version = "",
CompressedValue = "",
StateChanged = Signal.new(),
Saving = Signal.new(),
Saved = Signal.new(),
AttemptsChanged = Signal.new(),
ProcessQueue = Signal.new(),
})
dataStore.TaskManager = SynchronousTaskManager.new()
dataStore.LockTime = -math.huge
dataStore.SaveTime = -math.huge
dataStore.ActiveLockInterval = 0
dataStore.ProcessingQueue = false
dataStore.DataStore = dataStoreService:GetDataStore(name, scope)
dataStore.MemoryStore = memoryStoreService:GetSortedMap(id)
dataStore.Queue = memoryStoreService:GetQueue(id)
dataStore.Options = Instance.new("DataStoreSetOptions")
dataStore.__public.ProcessQueue.DataStore = proxy
dataStore.__public.ProcessQueue.Connected = SignalConnected
if active == true then bindToClose[dataStore.__public.UniqueId] = proxy end
return proxy
end
Constructor.find = function(name, scope, key)
if key == nil then key, scope = scope, "global" end
local id = name .. "/" .. scope .. "/" .. key
return dataStores[id]
end
Constructor.Response = {Success = "Success", Saved = "Saved", Locked = "Locked", State = "State", Error = "Error"}
-- DataStore
DataStore.__tostring = function(proxy)
return "DataStore"
end
DataStore.__shared = {
Open = function(proxy, template)
local dataStore = getmetatable(proxy)
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Open failed: Passed value is not a DataStore", 3) end
if dataStore.__public.State == nil then return "State", "Destroyed" end
local synchronousTask = dataStore.TaskManager:FindFirst(OpenTask)
if synchronousTask ~= nil then return synchronousTask:Wait(template) end
if dataStore.TaskManager:FindLast(DestroyTask) ~= nil then return "State", "Destroying" end
if dataStore.__public.State == true and dataStore.TaskManager:FindLast(CloseTask) == nil then
if dataStore.__public.Value == nil then
dataStore.__public.Value = Clone(template)
elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
Reconcile(dataStore.__public.Value, template)
end
return "Success"
end
return dataStore.TaskManager:InsertBack(OpenTask, proxy):Wait(template)
end,
Read = function(proxy, template)
local dataStore = getmetatable(proxy)
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Read failed: Passed value is not a DataStore", 3) end
local synchronousTask = dataStore.TaskManager:FindFirst(ReadTask)
if synchronousTask ~= nil then return synchronousTask:Wait(template) end
if dataStore.__public.State == true and dataStore.TaskManager:FindLast(CloseTask) == nil then return "State", "Open" end
return dataStore.TaskManager:InsertBack(ReadTask, proxy):Wait(template)
end,
Save = function(proxy)
local dataStore = getmetatable(proxy)
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Save failed: Passed value is not a DataStore", 3) end
if dataStore.__public.State == false then return "State", "Closed" end
if dataStore.__public.State == nil then return "State", "Destroyed" end
local synchronousTask = dataStore.TaskManager:FindFirst(SaveTask)
if synchronousTask ~= nil then return synchronousTask:Wait() end
if dataStore.TaskManager:FindLast(CloseTask) ~= nil then return "State", "Closing" end
if dataStore.TaskManager:FindLast(DestroyTask) ~= nil then return "State", "Destroying" end
return dataStore.TaskManager:InsertBack(SaveTask, proxy):Wait()
end,
Close = function(proxy)
local dataStore = getmetatable(proxy)
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Close failed: Passed value is not a DataStore", 3) end
if dataStore.__public.State == nil then return "Success" end
local synchronousTask = dataStore.TaskManager:FindFirst(CloseTask)
if synchronousTask ~= nil then return synchronousTask:Wait() end
if dataStore.__public.State == false and dataStore.TaskManager:FindLast(OpenTask) == nil then return "Success" end
local synchronousTask = dataStore.TaskManager:FindFirst(DestroyTask)
if synchronousTask ~= nil then return synchronousTask:Wait() end
StopLockTimer(dataStore)
StopSaveTimer(dataStore)
return dataStore.TaskManager:InsertBack(CloseTask, proxy):Wait()
end,
Destroy = function(proxy)
local dataStore = getmetatable(proxy)
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Destroy failed: Passed value is not a DataStore", 3) end
if dataStore.__public.State == nil then return "Success" end
dataStores[dataStore.__public.Id] = nil
StopLockTimer(dataStore)
StopSaveTimer(dataStore)
return (dataStore.TaskManager:FindFirst(DestroyTask) or dataStore.TaskManager:InsertBack(DestroyTask, proxy)):Wait()
end,
Queue = function(proxy, value, expiration, priority)
local dataStore = getmetatable(proxy)
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Queue failed: Passed value is not a DataStore", 3) end
if expiration ~= nil and type(expiration) ~= "number" then error("Attempt to Queue failed: Passed value is not nil or number", 3) end
if priority ~= nil and type(priority) ~= "number" then error("Attempt to Queue failed: Passed value is not nil or number", 3) end
local success, errorMessage
for i = 1, 3 do
if i > 1 then task.wait(1) end
success, errorMessage = pcall(dataStore.Queue.AddAsync, dataStore.Queue, value, expiration or 604800, priority)
if success == true then return "Success" end
end
return "Error", errorMessage
end,
Remove = function(proxy, id)
local dataStore = getmetatable(proxy)
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Remove failed: Passed value is not a DataStore", 3) end
if type(id) ~= "string" then error("Attempt to RemoveQueue failed: Passed value is not a string", 3) end
local success, errorMessage
for i = 1, 3 do
if i > 1 then task.wait(1) end
success, errorMessage = pcall(dataStore.Queue.RemoveAsync, dataStore.Queue, id)
if success == true then return "Success" end
end
return "Error", errorMessage
end,
Clone = function(proxy)
local dataStore = getmetatable(proxy)
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Clone failed: Passed value is not a DataStore", 3) end
return Clone(dataStore.__public.Value)
end,
Reconcile = function(proxy, template)
local dataStore = getmetatable(proxy)
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Reconcile failed: Passed value is not a DataStore", 3) end
if dataStore.__public.Value == nil then
dataStore.__public.Value = Clone(template)
elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
Reconcile(dataStore.__public.Value, template)
end
end,
Usage = function(proxy)
local dataStore = getmetatable(proxy)
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Usage failed: Passed value is not a DataStore", 3) end
if dataStore.__public.Value == nil then return 0, 0 end
if type(dataStore.__public.Metadata.Compress) ~= "table" then
local characters = #httpService:JSONEncode(dataStore.__public.Value)
return characters, characters / 4194303
else
local level = dataStore.__public.Metadata.Compress.Level or 2
local decimals = 10 ^ (dataStore.__public.Metadata.Compress.Decimals or 3)
local safety = if dataStore.__public.Metadata.Compress.Safety == nil then true else dataStore.__public.Metadata.Compress.Safety
dataStore.__public.CompressedValue = Compress(dataStore.__public.Value, level, decimals, safety)
local characters = #httpService:JSONEncode(dataStore.__public.CompressedValue)
return characters, characters / 4194303
end
end,
}
DataStore.__set = {
Metadata = function(proxy, dataStore, value)
if type(value) ~= "table" then error("Attempt to set Metadata failed: Passed value is not a table", 3) end
dataStore.__public.Metadata = value
end,
UserIds = function(proxy, dataStore, value)
if type(value) ~= "table" then error("Attempt to set UserIds failed: Passed value is not a table", 3) end
dataStore.__public.UserIds = value
end,
SaveInterval = function(proxy, dataStore, value)
if type(value) ~= "number" then error("Attempt to set SaveInterval failed: Passed value is not a number", 3) end
if value < 10 and value ~= 0 then error("Attempt to set SaveInterval failed: Passed value is less then 10 and not 0", 3) end
if value > 1000 then error("Attempt to set SaveInterval failed: Passed value is more then 1000", 3) end
if value == dataStore.__public.SaveInterval then return end
dataStore.__public.SaveInterval = value
if dataStore.__public.State ~= true then return end
if value == 0 then
StopSaveTimer(dataStore)
elseif dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then
StartSaveTimer(proxy)
end
end,
SaveDelay = function(proxy, dataStore, value)
if type(value) ~= "number" then error("Attempt to set SaveDelay failed: Passed value is not a number", 3) end
if value < 0 then error("Attempt to set SaveDelay failed: Passed value is less then 0", 3) end
if value > 10 then error("Attempt to set SaveDelay failed: Passed value is more then 10", 3) end
dataStore.__public.SaveDelay = value
end,
LockInterval = function(proxy, dataStore, value)
if type(value) ~= "number" then error("Attempt to set LockInterval failed: Passed value is not a number", 3) end
if value < 10 then error("Attempt to set LockInterval failed: Passed value is less then 10", 3) end
if value > 1000 then error("Attempt to set LockInterval failed: Passed value is more then 1000", 3) end
dataStore.__public.LockInterval = value
end,
LockAttempts = function(proxy, dataStore, value)
if type(value) ~= "number" then error("Attempt to set LockAttempts failed: Passed value is not a number", 3) end
if value < 1 then error("Attempt to set LockAttempts failed: Passed value is less then 1", 3) end
if value > 100 then error("Attempt to set LockAttempts failed: Passed value is more then 100", 3) end
dataStore.__public.LockAttempts = value
end,
SaveOnClose = function(proxy, dataStore, value)
if type(value) ~= "boolean" then error("Attempt to set SaveOnClose failed: Passed value is not a boolean", 3) end
dataStore.__public.SaveOnClose = value
end,
Id = false,
UniqueId = false,
Key = false,
State = false,
Hidden = false,
AttemptsRemaining = false,
CreatedTime = false,
UpdatedTime = false,
Version = false,
CompressedValue = false,
StateChanged = false,
Saving = false,
Saved = false,
AttemptsChanged = false,
ProcessQueue = false,
}
-- Functions
OpenTask = function(runningTask, proxy)
local dataStore = getmetatable(proxy)
local response, responseData = Lock(dataStore, 3)
if response ~= "Success" then for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end return end
local response, responseData = Load(dataStore, 3)
if response ~= "Success" then Unlock(dataStore, 3) for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end return end
dataStore.__public.State = true
if dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then
StartSaveTimer(proxy)
StartLockTimer(proxy)
end
for thread, template in runningTask:Iterate() do
if dataStore.__public.Value == nil then
dataStore.__public.Value = Clone(template)
elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
Reconcile(dataStore.__public.Value, template)
end
task.defer(thread, response)
end
if dataStore.ProcessingQueue == false and dataStore.__public.ProcessQueue.Connections > 0 then task.defer(ProcessQueue, proxy) end
dataStore.__public.StateChanged:Fire(true, proxy)
end
ReadTask = function(runningTask, proxy)
local dataStore = getmetatable(proxy)
if dataStore.__public.State == true then for thread in runningTask:Iterate() do task.defer(thread, "State", "Open") end return end
local response, responseData = Load(dataStore, 3)
if response ~= "Success" then for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end return end
for thread, template in runningTask:Iterate() do
if dataStore.__public.Value == nil then
dataStore.__public.Value = Clone(template)
elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
Reconcile(dataStore.__public.Value, template)
end
task.defer(thread, response)
end
end
LockTask = function(runningTask, proxy)
local dataStore = getmetatable(proxy)
local attemptsRemaining = dataStore.__public.AttemptsRemaining
local response, responseData = Lock(dataStore, 3)
if response ~= "Success" then dataStore.__public.AttemptsRemaining -= 1 end
if dataStore.__public.AttemptsRemaining ~= attemptsRemaining then dataStore.__public.AttemptsChanged:Fire(dataStore.__public.AttemptsRemaining, proxy) end
if dataStore.__public.AttemptsRemaining > 0 then
if dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then StartLockTimer(proxy) end
else
dataStore.__public.State = false
StopLockTimer(dataStore)
StopSaveTimer(dataStore)
if dataStore.__public.SaveOnClose == true then Save(proxy, 3) end
Unlock(dataStore, 3)
dataStore.__public.StateChanged:Fire(false, proxy)
end
for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
end
SaveTask = function(runningTask, proxy)
local dataStore = getmetatable(proxy)
if dataStore.__public.State == false then for thread in runningTask:Iterate() do task.defer(thread, "State", "Closed") end return end
StopSaveTimer(dataStore)
runningTask:End()
local response, responseData = Save(proxy, 3)
if dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then StartSaveTimer(proxy) end
for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
end
CloseTask = function(runningTask, proxy)
local dataStore = getmetatable(proxy)
if dataStore.__public.State == false then for thread in runningTask:Iterate() do task.defer(thread, "Success") end return end
dataStore.__public.State = false
local response, responseData = nil, nil
if dataStore.__public.SaveOnClose == true then response, responseData = Save(proxy, 3) end
Unlock(dataStore, 3)
dataStore.__public.StateChanged:Fire(false, proxy)
if response == "Saved" then
for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
else
for thread in runningTask:Iterate() do task.defer(thread, "Success") end
end
end
DestroyTask = function(runningTask, proxy)
local dataStore = getmetatable(proxy)
local response, responseData = nil, nil
if dataStore.__public.State == false then
dataStore.__public.State = nil
else
dataStore.__public.State = nil
if dataStore.__public.SaveOnClose == true then response, responseData = Save(proxy, 3) end
Unlock(dataStore, 3)
end
dataStore.__public.StateChanged:Fire(nil, proxy)
dataStore.__public.StateChanged:DisconnectAll()
dataStore.__public.Saving:DisconnectAll()
dataStore.__public.Saved:DisconnectAll()
dataStore.__public.AttemptsChanged:DisconnectAll()
dataStore.__public.ProcessQueue:DisconnectAll()
bindToClose[dataStore.__public.UniqueId] = nil
if response == "Saved" then
for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
else
for thread in runningTask:Iterate() do task.defer(thread, "Success") end
end
end
Lock = function(dataStore, attempts)
local success, value, id, lockTime, lockInterval, lockAttempts = nil, nil, nil, nil, dataStore.__public.LockInterval, dataStore.__public.LockAttempts
for i = 1, attempts do
if i > 1 then task.wait(1) end
lockTime = os.clock()
success, value = pcall(dataStore.MemoryStore.UpdateAsync, dataStore.MemoryStore, "Id", function(value) id = value return if id == nil or id == dataStore.__public.UniqueId then dataStore.__public.UniqueId else nil end, lockInterval * lockAttempts + 30)
if success == true then break end
end
if success == false then return "Error", value end
if value == nil then return "Locked", id end
dataStore.LockTime = lockTime + lockInterval * lockAttempts
dataStore.ActiveLockInterval = lockInterval
dataStore.__public.AttemptsRemaining = lockAttempts
return "Success"
end
Unlock = function(dataStore, attempts)
local success, value, id = nil, nil, nil
for i = 1, attempts do
if i > 1 then task.wait(1) end
success, value = pcall(dataStore.MemoryStore.UpdateAsync, dataStore.MemoryStore, "Id", function(value) id = value return if id == dataStore.__public.UniqueId then dataStore.__public.UniqueId else nil end, 0)
if success == true then break end
end
if success == false then return "Error", value end
if value == nil and id ~= nil then return "Locked", id end
return "Success"
end
Load = function(dataStore, attempts)
local success, value, info = nil, nil, nil
for i = 1, attempts do
if i > 1 then task.wait(1) end
success, value, info = pcall(dataStore.DataStore.GetAsync, dataStore.DataStore, dataStore.__public.Key)
if success == true then break end
end
if success == false then return "Error", value end
if info == nil then
dataStore.__public.Metadata, dataStore.__public.UserIds, dataStore.__public.CreatedTime, dataStore.__public.UpdatedTime, dataStore.__public.Version = {}, {}, 0, 0, ""
else
dataStore.__public.Metadata, dataStore.__public.UserIds, dataStore.__public.CreatedTime, dataStore.__public.UpdatedTime, dataStore.__public.Version = info:GetMetadata(), info:GetUserIds(), info.CreatedTime, info.UpdatedTime, info.Version
end
if type(dataStore.__public.Metadata.Compress) ~= "table" then
dataStore.__public.Value = value
else
dataStore.__public.CompressedValue = value
local decimals = 10 ^ (dataStore.__public.Metadata.Compress.Decimals or 3)
dataStore.__public.Value = Decompress(dataStore.__public.CompressedValue, decimals)
end
return "Success"
end
Save = function(proxy, attempts)
local dataStore = getmetatable(proxy)
local deltaTime = os.clock() - dataStore.SaveTime
if deltaTime < dataStore.__public.SaveDelay then task.wait(dataStore.__public.SaveDelay - deltaTime) end
dataStore.__public.Saving:Fire(dataStore.__public.Value, proxy)
local success, value, info = nil, nil, nil
if dataStore.__public.Value == nil then
for i = 1, attempts do
if i > 1 then task.wait(1) end
success, value, info = pcall(dataStore.DataStore.RemoveAsync, dataStore.DataStore, dataStore.__public.Key)
if success == true then break end
end
if success == false then dataStore.__public.Saved:Fire("Error", value, proxy) return "Error", value end
dataStore.__public.Metadata, dataStore.__public.UserIds, dataStore.__public.CreatedTime, dataStore.__public.UpdatedTime, dataStore.__public.Version = {}, {}, 0, 0, ""
elseif type(dataStore.__public.Metadata.Compress) ~= "table" then
dataStore.Options:SetMetadata(dataStore.__public.Metadata)
for i = 1, attempts do
if i > 1 then task.wait(1) end
success, value = pcall(dataStore.DataStore.SetAsync, dataStore.DataStore, dataStore.__public.Key, dataStore.__public.Value, dataStore.__public.UserIds, dataStore.Options)
if success == true then break end
end
if success == false then dataStore.__public.Saved:Fire("Error", value, proxy) return "Error", value end
dataStore.__public.Version = value
else
local level = dataStore.__public.Metadata.Compress.Level or 2
local decimals = 10 ^ (dataStore.__public.Metadata.Compress.Decimals or 3)
local safety = if dataStore.__public.Metadata.Compress.Safety == nil then true else dataStore.__public.Metadata.Compress.Safety
dataStore.__public.CompressedValue = Compress(dataStore.__public.Value, level, decimals, safety)
dataStore.Options:SetMetadata(dataStore.__public.Metadata)
for i = 1, attempts do
if i > 1 then task.wait(1) end
success, value = pcall(dataStore.DataStore.SetAsync, dataStore.DataStore, dataStore.__public.Key, dataStore.__public.CompressedValue, dataStore.__public.UserIds, dataStore.Options)
if success == true then break end
end
if success == false then dataStore.__public.Saved:Fire("Error", value, proxy) return "Error", value end
dataStore.Version = value
end
dataStore.SaveTime = os.clock()
dataStore.__public.Saved:Fire("Saved", dataStore.__public.Value, proxy)
return "Saved", dataStore.__public.Value
end
StartSaveTimer = function(proxy)
local dataStore = getmetatable(proxy)
if dataStore.SaveThread ~= nil then task.cancel(dataStore.SaveThread) end
if dataStore.__public.SaveInterval == 0 then return end
dataStore.SaveThread = task.delay(dataStore.__public.SaveInterval, SaveTimerEnded, proxy)
end
StopSaveTimer = function(dataStore)
if dataStore.SaveThread == nil then return end
task.cancel(dataStore.SaveThread)
dataStore.SaveThread = nil
end
SaveTimerEnded = function(proxy)
local dataStore = getmetatable(proxy)
dataStore.SaveThread = nil
if dataStore.TaskManager:FindLast(SaveTask) ~= nil then return end
dataStore.TaskManager:InsertBack(SaveTask, proxy)
end
StartLockTimer = function(proxy)
local dataStore = getmetatable(proxy)
if dataStore.LockThread ~= nil then task.cancel(dataStore.LockThread) end
local startTime = dataStore.LockTime - dataStore.__public.AttemptsRemaining * dataStore.ActiveLockInterval
dataStore.LockThread = task.delay(startTime - os.clock() + dataStore.ActiveLockInterval, LockTimerEnded, proxy)
end
StopLockTimer = function(dataStore)
if dataStore.LockThread == nil then return end
task.cancel(dataStore.LockThread)
dataStore.LockThread = nil
end
LockTimerEnded = function(proxy)
local dataStore = getmetatable(proxy)
dataStore.LockThread = nil
if dataStore.TaskManager:FindFirst(LockTask) ~= nil then return end
dataStore.TaskManager:InsertBack(LockTask, proxy)
end
ProcessQueue = function(proxy)
local dataStore = getmetatable(proxy)
if dataStore.__public.State ~= true then return end
if dataStore.__public.ProcessQueue.Connections == 0 then return end
if dataStore.ProcessingQueue == true then return end
dataStore.ProcessingQueue = true
while true do
local success, values, id = pcall(dataStore.Queue.ReadAsync, dataStore.Queue, 100, false, 30)
if dataStore.__public.State ~= true then break end
if dataStore.__public.ProcessQueue.Connections == 0 then break end
if success == true and id ~= nil then dataStore.__public.ProcessQueue:Fire(id, values, proxy) end
end
dataStore.ProcessingQueue = false
end
SignalConnected = function(connected, signal)
if connected == false then return end
ProcessQueue(signal.DataStore)
end
Clone = function(original)
if type(original) ~= "table" then return original end
local clone = {}
for index, value in original do clone[index] = Clone(value) end
return clone
end
Reconcile = function(target, template)
for index, value in template do
if type(index) == "number" then continue end
if target[index] == nil then
target[index] = Clone(value)
elseif type(target[index]) == "table" and type(value) == "table" then
Reconcile(target[index], value)
end
end
end
Compress = function(value, level, decimals, safety)
local data = {}
if type(value) == "boolean" then
table.insert(data, if value == false then "-" else "+")
elseif type(value) == "number" then
if value % 1 == 0 then
table.insert(data, if value < 0 then "<" .. Encode(-value) else ">" .. Encode(value))
else
table.insert(data, if value < 0 then "(" .. Encode(math.round(-value * decimals)) else ")" .. Encode(math.round(value * decimals)))
end
elseif type(value) == "string" then
if safety == true then value = value:gsub("", " ") end
table.insert(data, "#" .. value .. "")
elseif type(value) == "table" then
if #value > 0 and level == 2 then
table.insert(data, "|")
for i = 1, #value do table.insert(data, Compress(value[i], level, decimals, safety)) end
table.insert(data, "")
else
table.insert(data, "*")
for key, tableValue in value do table.insert(data, Compress(key, level, decimals, safety)) table.insert(data, Compress(tableValue, level, decimals, safety)) end
table.insert(data, "")
end
end
return table.concat(data)
end
Decompress = function(value, decimals, index)
local i1, i2, dataType, data = value:find("([-+<>()#|*])", index or 1)
if dataType == "-" then
return false, i2
elseif dataType == "+" then
return true, i2
elseif dataType == "<" then
i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
return -Decode(data), i2
elseif dataType == ">" then
i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
return Decode(data), i2
elseif dataType == "(" then
i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
return -Decode(data) / decimals, i2
elseif dataType == ")" then
i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
return Decode(data) / decimals, i2
elseif dataType == "#" then
i1, i2, data = value:find("(.-)", i2 + 1)
return data, i2
elseif dataType == "|" then
local array = {}
while true do
data, i2 = Decompress(value, decimals, i2 + 1)
if data == nil then break end
table.insert(array, data)
end
return array, i2
elseif dataType == "*" then
local dictionary, key = {}, nil
while true do
key, i2 = Decompress(value, decimals, i2 + 1)
if key == nil then break end
data, i2 = Decompress(value, decimals, i2 + 1)
dictionary[key] = data
end
return dictionary, i2
end
return nil, i2
end
Encode = function(value)
if value == 0 then return "0" end
local data = {}
while value > 0 do
table.insert(data, characters[value % base])
value = math.floor(value / base)
end
return table.concat(data)
end
Decode = function(value)
local number, power, data = 0, 1, {string.byte(value, 1, #value)}
for i, code in data do
number += bytes[code] * power
power *= base
end
return number
end
BindToClose = function()
active = false
for uniqueId, proxy in bindToClose do
local dataStore = getmetatable(proxy)
if dataStore.__public.State == nil then continue end
dataStores[dataStore.__public.Id] = nil
StopLockTimer(dataStore)
StopSaveTimer(dataStore)
if dataStore.TaskManager:FindFirst(DestroyTask) == nil then dataStore.TaskManager:InsertBack(DestroyTask, proxy) end
end
while next(bindToClose) ~= nil do task.wait() end
end
-- Events
game:BindToClose(BindToClose)
return table.freeze(Constructor) :: Constructor

View file

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,160 @@
local listClass, linkClass = {}, {}
listClass.__index, linkClass.__index = listClass, linkClass
export type List<T> = typeof(listClass.new((1 :: any) :: T))
function listClass.new()
local self = setmetatable({}, listClass)
self.List = self
self.Links = {}
self.Length = 0
self.Next = self
self.Previous = self
return self
end
function listClass:InsertFront(value)
assert(self.Links[value] == nil, "Value already exists")
local link = {Value = value, List = self, Previous = self, Next = self.Next}
self.Links[value] = link
self.Length += 1
self.Next.Previous = link
self.Next = link
return setmetatable(link, linkClass)
end
function listClass:InsertAfter(value)
assert(self.Links[value] == nil, "Value already exists")
local link = {Value = value, List = self, Previous = self, Next = self.Next}
self.Links[value] = link
self.Length += 1
self.Next.Previous = link
self.Next = link
return setmetatable(link, linkClass)
end
function listClass:InsertBack(value)
assert(self.Links[value] == nil, "Value already exists")
local link = {Value = value, List = self, Next = self, Previous = self.Previous}
self.Links[value] = link
self.Length += 1
self.Previous.Next = link
self.Previous = link
return setmetatable(link, linkClass)
end
function listClass:InsertBefore(value)
assert(self.Links[value] == nil, "Value already exists")
local link = {Value = value, List = self, Next = self, Previous = self.Previous}
self.Links[value] = link
self.Length += 1
self.Previous.Next = link
self.Previous = link
return setmetatable(link, linkClass)
end
function listClass:GetNext(link)
link = (link or self).Next
if link ~= self then return link, link.Value end
end
function listClass:GetPrevious(link)
link = (link or self).Previous
if link ~= self then return link, link.Value end
end
function listClass:IterateForward(link)
return listClass.GetNext, self, link
end
function listClass:IterateBackward(link)
return listClass.GetPrevious, self, link
end
function listClass:Remove(value, clean: boolean?)
local link = self.Links[value]
if link ~= nil then
self.Links[value] = nil
self.Length -= 1
link.List = nil
link.Previous.Next = link.Next
link.Next.Previous = link.Previous
if clean then setmetatable(link, nil) return end
return link
end
end
function listClass:RemoveFirst()
local link = self.Next
if link ~= self then
self.Links[link.Value] = nil
self.Length -= 1
link.List = nil
link.Previous.Next = link.Next
link.Next.Previous = link.Previous
return link.Value, link
end
end
function listClass:RemoveLast()
local link = self.Previous
if link ~= self then
self.Links[link.Value] = nil
self.Length -= 1
link.List = nil
link.Previous.Next = link.Next
link.Next.Previous = link.Previous
return link.Value, link
end
end
function listClass:Destroy()
local l = self.List.Length
for link, value in self:IterateForward() do
self:Remove(value, true)
end
setmetatable(self, nil)
table.clear(self)
end
function linkClass:InsertAfter(value)
assert(self.List.Links[value] == nil, "Value already exists")
local link = {Value = value, List = self.List, Previous = self, Next = self.Next}
self.List.Links[value] = link
self.List.Length += 1
self.Next.Previous = link
self.Next = link
return setmetatable(link, linkClass)
end
function linkClass:InsertBefore(value)
assert(self.List.Links[value] == nil, "Value already exists")
local link = {Value = value, List = self.List, Next = self, Previous = self.Previous}
self.List.Links[value] = link
self.List.Length += 1
self.Previous.Next = link
self.Previous = link
return setmetatable(link, linkClass)
end
function linkClass:GetNext()
local link = self.Next
if link ~= link.List then return link end
end
function linkClass:GetPrevious()
local link = self.Previous
if link ~= link.List then return link end
end
function linkClass:Remove()
assert(self.List ~= nil, "Link is not in a list")
self.List.Links[self.Value] = nil
self.List.Length -= 1
self.List = nil
self.Previous.Next = self.Next
self.Next.Previous = self.Previous
end
return listClass

View file

@ -0,0 +1,101 @@
--!strict
-- Requires
local Task = require(script.Parent.Task)
-- Types
export type Signal<A... = ()> = {
Type: "Signal",
Previous: Connection<A...>,
Next: Connection<A...>,
Fire: (self: Signal<A...>, A...) -> (),
Connect: (self: Signal<A...>, func: (A...) -> ()) -> Connection<A...>,
Once: (self: Signal<A...>, func: (A...) -> ()) -> Connection<A...>,
Wait: (self: Signal<A...>) -> A...,
}
export type Connection<A... = ()> = {
Type: "Connection",
Previous: Connection<A...>,
Next: Connection<A...>,
Once: boolean,
Function: (player: Player, A...) -> (),
Thread: thread,
Disconnect: (self: Connection<A...>) -> (),
}
-- Varables
local Signal = {} :: Signal<...any>
local Connection = {} :: Connection<...any>
-- Constructor
local function Constructor<A...>()
local signal = (setmetatable({}, Signal) :: any) :: Signal<A...>
signal.Previous = signal :: any
signal.Next = signal :: any
return signal
end
-- Signal
Signal["__index"] = Signal
Signal.Type = "Signal"
function Signal:Connect(func)
local connection = (setmetatable({}, Connection) :: any) :: Connection
connection.Previous = self.Previous
connection.Next = self :: any
connection.Once = false
connection.Function = func
self.Previous.Next = connection
self.Previous = connection
return connection
end
function Signal:Once(func)
local connection = (setmetatable({}, Connection) :: any) :: Connection
connection.Previous = self.Previous
connection.Next = self :: any
connection.Once = true
connection.Function = func
self.Previous.Next = connection
self.Previous = connection
return connection
end
function Signal:Wait()
local connection = (setmetatable({}, Connection) :: any) :: Connection
connection.Previous = self.Previous
connection.Next = self :: any
connection.Once = true
connection.Thread = coroutine.running()
self.Previous.Next = connection
self.Previous = connection
return coroutine.yield()
end
function Signal:Fire(...)
local connection = self.Next
while connection.Type == "Connection" do
if connection.Function then Task:Defer(connection.Function, ...) else task.defer(connection.Thread, ...) end
if connection.Once then connection.Previous.Next = connection.Next connection.Next.Previous = connection.Previous end
connection = connection.Next
end
end
-- Connection
Connection["__index"] = Connection
Connection.Type = "Connection"
function Connection:Disconnect()
self.Previous.Next = self.Next
self.Next.Previous = self.Previous
end
return Constructor

View file

@ -0,0 +1,46 @@
--!strict
-- Types
export type Task = {
Type: "Task",
Spawn: (self: Task, func: (...any) -> (), ...any) -> thread,
Defer: (self: Task, func: (...any) -> (), ...any) -> thread,
Delay: (self: Task, duration: number, func: (...any) -> (), ...any) -> thread,
}
-- Varables
local Call, Thread
local Task = {} :: Task
local threads = {} :: {thread}
-- Task
Task.Type = "Task"
function Task:Spawn(func, ...)
return task.spawn(table.remove(threads) or task.spawn(Thread), func, ...)
end
function Task:Defer(func, ...)
return task.defer(table.remove(threads) or task.spawn(Thread), func, ...)
end
function Task:Delay(duration, func, ...)
return task.delay(duration, table.remove(threads) or task.spawn(Thread), func, ...)
end
-- Functions
function Call(func: (...any) -> (), ...)
func(...)
table.insert(threads, coroutine.running())
end
function Thread()
while true do Call(coroutine.yield()) end
end
return Task

View file

@ -0,0 +1,6 @@
return {[0] = -- Recommended character array lengths: 2, 4, 8, 16, 32, 64, 128, 256
" ", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D",
"E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
"U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
}

View file

@ -0,0 +1,12 @@
return { -- Add any enum [Max: 255]
Enum.AccessoryType,
Enum.Axis,
Enum.BodyPart,
Enum.BodyPartR15,
Enum.EasingDirection,
Enum.EasingStyle,
Enum.KeyCode,
Enum.Material,
Enum.NormalId,
Enum.HumanoidStateType
}

View file

@ -0,0 +1,8 @@
return {
"DataStore Failed To Load",
"Another Static String",
math.pi,
123456789,
Vector3.new(1, 2, 3),
"You can have upto 255 static values of any type"
}

View file

@ -0,0 +1,8 @@
return {
"DataStore Failed To Load",
"Another Static String",
math.pi,
123456789,
Vector3.new(1, 2, 3),
"You can have upto 255 static values of any type"
}

View file

@ -0,0 +1,8 @@
return {
"DataStore Failed To Load",
"Another Static String",
math.pi,
123456789,
Vector3.new(1, 2, 3),
"You can have upto 255 static values of any type"
}

View file

@ -0,0 +1,705 @@
--!strict
--!optimize 2
--[[
S8 Minimum: -128 Maximum: 127
S16 Minimum: -32768 Maximum: 32767
S24 Minimum: -8388608 Maximum: 8388607
S32 Minimum: -2147483648 Maximum: 2147483647
U8 Minimum: 0 Maximum: 255
U16 Minimum: 0 Maximum: 65535
U24 Minimum: 0 Maximum: 16777215
U32 Minimum: 0 Maximum: 4294967295
F16 ±2048 [65520]
F24 ±262144 [4294959104]
F32 ±16777216 [170141183460469231731687303715884105728]
F64 ±9007199254740992 [huge]
]]
-- Types
export type Cursor = {
Buffer: buffer,
BufferLength: number,
BufferOffset: number,
Instances: {Instance},
InstancesOffset: number,
}
-- Varables
local activeCursor : Cursor
local activeBuffer : buffer
local bufferLength : number
local bufferOffset : number
local instances : {Instance}
local instancesOffset : number
local types = {}
local reads = {}
local writes = {}
local anyReads = {} :: {[any]: () -> any}
local anyWrites = {} :: {[any]: (any) -> ()}
-- Functions
local function Allocate(bytes: number)
local targetLength = bufferOffset + bytes
if bufferLength < targetLength then
while bufferLength < targetLength do bufferLength *= 2 end
local newBuffer = buffer.create(bufferLength)
buffer.copy(newBuffer, 0, activeBuffer, 0, bufferOffset)
activeCursor.Buffer = newBuffer
activeBuffer = newBuffer
end
end
local function ReadS8(): number local value = buffer.readi8(activeBuffer, bufferOffset) bufferOffset += 1 return value end
local function WriteS8(value: number) buffer.writei8(activeBuffer, bufferOffset, value) bufferOffset += 1 end
local function ReadS16(): number local value = buffer.readi16(activeBuffer, bufferOffset) bufferOffset += 2 return value end
local function WriteS16(value: number) buffer.writei16(activeBuffer, bufferOffset, value) bufferOffset += 2 end
local function ReadS24(): number local value = buffer.readbits(activeBuffer, bufferOffset * 8, 24) - 8388608 bufferOffset += 3 return value end
local function WriteS24(value: number) buffer.writebits(activeBuffer, bufferOffset * 8, 24, value + 8388608) bufferOffset += 3 end
local function ReadS32(): number local value = buffer.readi32(activeBuffer, bufferOffset) bufferOffset += 4 return value end
local function WriteS32(value: number) buffer.writei32(activeBuffer, bufferOffset, value) bufferOffset += 4 end
local function ReadU8(): number local value = buffer.readu8(activeBuffer, bufferOffset) bufferOffset += 1 return value end
local function WriteU8(value: number) buffer.writeu8(activeBuffer, bufferOffset, value) bufferOffset += 1 end
local function ReadU16(): number local value = buffer.readu16(activeBuffer, bufferOffset) bufferOffset += 2 return value end
local function WriteU16(value: number) buffer.writeu16(activeBuffer, bufferOffset, value) bufferOffset += 2 end
local function ReadU24(): number local value = buffer.readbits(activeBuffer, bufferOffset * 8, 24) bufferOffset += 3 return value end
local function WriteU24(value: number) buffer.writebits(activeBuffer, bufferOffset * 8, 24, value) bufferOffset += 3 end
local function ReadU32(): number local value = buffer.readu32(activeBuffer, bufferOffset) bufferOffset += 4 return value end
local function WriteU32(value: number) buffer.writeu32(activeBuffer, bufferOffset, value) bufferOffset += 4 end
local function ReadF32(): number local value = buffer.readf32(activeBuffer, bufferOffset) bufferOffset += 4 return value end
local function WriteF32(value: number) buffer.writef32(activeBuffer, bufferOffset, value) bufferOffset += 4 end
local function ReadF64(): number local value = buffer.readf64(activeBuffer, bufferOffset) bufferOffset += 8 return value end
local function WriteF64(value: number) buffer.writef64(activeBuffer, bufferOffset, value) bufferOffset += 8 end
local function ReadString(length: number) local value = buffer.readstring(activeBuffer, bufferOffset, length) bufferOffset += length return value end
local function WriteString(value: string) buffer.writestring(activeBuffer, bufferOffset, value) bufferOffset += #value end
local function ReadBuffer(length: number) local value = buffer.create(length) buffer.copy(value, 0, activeBuffer, bufferOffset, length) bufferOffset += length return value end
local function WriteBuffer(value: buffer) buffer.copy(activeBuffer, bufferOffset, value) bufferOffset += buffer.len(value) end
local function ReadInstance() instancesOffset += 1 return instances[instancesOffset] end
local function WriteInstance(value) instancesOffset += 1 instances[instancesOffset] = value end
local function ReadF16(): number
local bitOffset = bufferOffset * 8
bufferOffset += 2
local mantissa = buffer.readbits(activeBuffer, bitOffset + 0, 10)
local exponent = buffer.readbits(activeBuffer, bitOffset + 10, 5)
local sign = buffer.readbits(activeBuffer, bitOffset + 15, 1)
if mantissa == 0b0000000000 then
if exponent == 0b00000 then return 0 end
if exponent == 0b11111 then return if sign == 0 then math.huge else -math.huge end
elseif exponent == 0b11111 then return 0/0 end
if sign == 0 then
return (mantissa / 1024 + 1) * 2 ^ (exponent - 15)
else
return -(mantissa / 1024 + 1) * 2 ^ (exponent - 15)
end
end
local function WriteF16(value: number)
local bitOffset = bufferOffset * 8
bufferOffset += 2
if value == 0 then
buffer.writebits(activeBuffer, bitOffset, 16, 0b0_00000_0000000000)
elseif value >= 65520 then
buffer.writebits(activeBuffer, bitOffset, 16, 0b0_11111_0000000000)
elseif value <= -65520 then
buffer.writebits(activeBuffer, bitOffset, 16, 0b1_11111_0000000000)
elseif value ~= value then
buffer.writebits(activeBuffer, bitOffset, 16, 0b0_11111_0000000001)
else
local sign = 0
if value < 0 then sign = 1 value = -value end
local mantissa, exponent = math.frexp(value)
buffer.writebits(activeBuffer, bitOffset + 0, 10, mantissa * 2048 - 1023.5)
buffer.writebits(activeBuffer, bitOffset + 10, 5, exponent + 14)
buffer.writebits(activeBuffer, bitOffset + 15, 1, sign)
end
end
local function ReadF24(): number
local bitOffset = bufferOffset * 8
bufferOffset += 3
local mantissa = buffer.readbits(activeBuffer, bitOffset + 0, 17)
local exponent = buffer.readbits(activeBuffer, bitOffset + 17, 6)
local sign = buffer.readbits(activeBuffer, bitOffset + 23, 1)
if mantissa == 0b00000000000000000 then
if exponent == 0b000000 then return 0 end
if exponent == 0b111111 then return if sign == 0 then math.huge else -math.huge end
elseif exponent == 0b111111 then return 0/0 end
if sign == 0 then
return (mantissa / 131072 + 1) * 2 ^ (exponent - 31)
else
return -(mantissa / 131072 + 1) * 2 ^ (exponent - 31)
end
end
local function WriteF24(value: number)
local bitOffset = bufferOffset * 8
bufferOffset += 3
if value == 0 then
buffer.writebits(activeBuffer, bitOffset, 24, 0b0_000000_00000000000000000)
elseif value >= 4294959104 then
buffer.writebits(activeBuffer, bitOffset, 24, 0b0_111111_00000000000000000)
elseif value <= -4294959104 then
buffer.writebits(activeBuffer, bitOffset, 24, 0b1_111111_00000000000000000)
elseif value ~= value then
buffer.writebits(activeBuffer, bitOffset, 24, 0b0_111111_00000000000000001)
else
local sign = 0
if value < 0 then sign = 1 value = -value end
local mantissa, exponent = math.frexp(value)
buffer.writebits(activeBuffer, bitOffset + 0, 17, mantissa * 262144 - 131071.5)
buffer.writebits(activeBuffer, bitOffset + 17, 6, exponent + 30)
buffer.writebits(activeBuffer, bitOffset + 23, 1, sign)
end
end
-- Types
types.Any = "Any" :: any
reads.Any = function() return anyReads[ReadU8()]() end
writes.Any = function(value: any) anyWrites[typeof(value)](value) end
types.Nil = ("Nil" :: any) :: nil
reads.Nil = function() return nil end
writes.Nil = function(value: nil) end
types.NumberS8 = ("NumberS8" :: any) :: number
reads.NumberS8 = function() return ReadS8() end
writes.NumberS8 = function(value: number) Allocate(1) WriteS8(value) end
types.NumberS16 = ("NumberS16" :: any) :: number
reads.NumberS16 = function() return ReadS16() end
writes.NumberS16 = function(value: number) Allocate(2) WriteS16(value) end
types.NumberS24 = ("NumberS24" :: any) :: number
reads.NumberS24 = function() return ReadS24() end
writes.NumberS24 = function(value: number) Allocate(3) WriteS24(value) end
types.NumberS32 = ("NumberS32" :: any) :: number
reads.NumberS32 = function() return ReadS32() end
writes.NumberS32 = function(value: number) Allocate(4) WriteS32(value) end
types.NumberU8 = ("NumberU8" :: any) :: number
reads.NumberU8 = function() return ReadU8() end
writes.NumberU8 = function(value: number) Allocate(1) WriteU8(value) end
types.NumberU16 = ("NumberU16" :: any) :: number
reads.NumberU16 = function() return ReadU16() end
writes.NumberU16 = function(value: number) Allocate(2) WriteU16(value) end
types.NumberU24 = ("NumberU24" :: any) :: number
reads.NumberU24 = function() return ReadU24() end
writes.NumberU24 = function(value: number) Allocate(3) WriteU24(value) end
types.NumberU32 = ("NumberU32" :: any) :: number
reads.NumberU32 = function() return ReadU32() end
writes.NumberU32 = function(value: number) Allocate(4) WriteU32(value) end
types.NumberF16 = ("NumberF16" :: any) :: number
reads.NumberF16 = function() return ReadF16() end
writes.NumberF16 = function(value: number) Allocate(2) WriteF16(value) end
types.NumberF24 = ("NumberF24" :: any) :: number
reads.NumberF24 = function() return ReadF24() end
writes.NumberF24 = function(value: number) Allocate(3) WriteF24(value) end
types.NumberF32 = ("NumberF32" :: any) :: number
reads.NumberF32 = function() return ReadF32() end
writes.NumberF32 = function(value: number) Allocate(4) WriteF32(value) end
types.NumberF64 = ("NumberF64" :: any) :: number
reads.NumberF64 = function() return ReadF64() end
writes.NumberF64 = function(value: number) Allocate(8) WriteF64(value) end
types.String = ("String" :: any) :: string
reads.String = function() return ReadString(ReadU8()) end
writes.String = function(value: string) local length = #value Allocate(1 + length) WriteU8(length) WriteString(value) end
types.StringLong = ("StringLong" :: any) :: string
reads.StringLong = function() return ReadString(ReadU16()) end
writes.StringLong = function(value: string) local length = #value Allocate(2 + length) WriteU16(length) WriteString(value) end
types.Buffer = ("Buffer" :: any) :: buffer
reads.Buffer = function() return ReadBuffer(ReadU8()) end
writes.Buffer = function(value: buffer) local length = buffer.len(value) Allocate(1 + length) WriteU8(length) WriteBuffer(value) end
types.BufferLong = ("BufferLong" :: any) :: buffer
reads.BufferLong = function() return ReadBuffer(ReadU16()) end
writes.BufferLong = function(value: buffer) local length = buffer.len(value) Allocate(2 + length) WriteU16(length) WriteBuffer(value) end
types.Instance = ("Instance" :: any) :: Instance
reads.Instance = function() return ReadInstance() end
writes.Instance = function(value: Instance) WriteInstance(value) end
types.Boolean8 = ("Boolean8" :: any) :: boolean
reads.Boolean8 = function() return ReadU8() == 1 end
writes.Boolean8 = function(value: boolean) Allocate(1) WriteU8(if value then 1 else 0) end
types.NumberRange = ("NumberRange" :: any) :: NumberRange
reads.NumberRange = function() return NumberRange.new(ReadF32(), ReadF32()) end
writes.NumberRange = function(value: NumberRange) Allocate(8) WriteF32(value.Min) WriteF32(value.Max) end
types.BrickColor = ("BrickColor" :: any) :: BrickColor
reads.BrickColor = function() return BrickColor.new(ReadU16()) end
writes.BrickColor = function(value: BrickColor) Allocate(2) WriteU16(value.Number) end
types.Color3 = ("Color3" :: any) :: Color3
reads.Color3 = function() return Color3.fromRGB(ReadU8(), ReadU8(), ReadU8()) end
writes.Color3 = function(value: Color3) Allocate(3) WriteU8(value.R * 255 + 0.5) WriteU8(value.G * 255 + 0.5) WriteU8(value.B * 255 + 0.5) end
types.UDim = ("UDim" :: any) :: UDim
reads.UDim = function() return UDim.new(ReadS16() / 1000, ReadS16()) end
writes.UDim = function(value: UDim) Allocate(4) WriteS16(value.Scale * 1000) WriteS16(value.Offset) end
types.UDim2 = ("UDim2" :: any) :: UDim2
reads.UDim2 = function() return UDim2.new(ReadS16() / 1000, ReadS16(), ReadS16() / 1000, ReadS16()) end
writes.UDim2 = function(value: UDim2) Allocate(8) WriteS16(value.X.Scale * 1000) WriteS16(value.X.Offset) WriteS16(value.Y.Scale * 1000) WriteS16(value.Y.Offset) end
types.Rect = ("Rect" :: any) :: Rect
reads.Rect = function() return Rect.new(ReadF32(), ReadF32(), ReadF32(), ReadF32()) end
writes.Rect = function(value: Rect) Allocate(16) WriteF32(value.Min.X) WriteF32(value.Min.Y) WriteF32(value.Max.X) WriteF32(value.Max.Y) end
types.Vector2S16 = ("Vector2S16" :: any) :: Vector2
reads.Vector2S16 = function() return Vector2.new(ReadS16(), ReadS16()) end
writes.Vector2S16 = function(value: Vector2) Allocate(4) WriteS16(value.X) WriteS16(value.Y) end
types.Vector2F24 = ("Vector2F24" :: any) :: Vector2
reads.Vector2F24 = function() return Vector2.new(ReadF24(), ReadF24()) end
writes.Vector2F24 = function(value: Vector2) Allocate(6) WriteF24(value.X) WriteF24(value.Y) end
types.Vector2F32 = ("Vector2F32" :: any) :: Vector2
reads.Vector2F32 = function() return Vector2.new(ReadF32(), ReadF32()) end
writes.Vector2F32 = function(value: Vector2) Allocate(8) WriteF32(value.X) WriteF32(value.Y) end
types.Vector3S16 = ("Vector3S16" :: any) :: Vector3
reads.Vector3S16 = function() return Vector3.new(ReadS16(), ReadS16(), ReadS16()) end
writes.Vector3S16 = function(value: Vector3) Allocate(6) WriteS16(value.X) WriteS16(value.Y) WriteS16(value.Z) end
types.Vector3F24 = ("Vector3F24" :: any) :: Vector3
reads.Vector3F24 = function() return Vector3.new(ReadF24(), ReadF24(), ReadF24()) end
writes.Vector3F24 = function(value: Vector3) Allocate(9) WriteF24(value.X) WriteF24(value.Y) WriteF24(value.Z) end
types.Vector3F32 = ("Vector3F32" :: any) :: Vector3
reads.Vector3F32 = function() return Vector3.new(ReadF32(), ReadF32(), ReadF32()) end
writes.Vector3F32 = function(value: Vector3) Allocate(12) WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z) end
types.NumberU4 = ("NumberU4" :: any) :: {number}
reads.NumberU4 = function()
local bitOffset = bufferOffset * 8
bufferOffset += 1
return {
buffer.readbits(activeBuffer, bitOffset + 0, 4),
buffer.readbits(activeBuffer, bitOffset + 4, 4)
}
end
writes.NumberU4 = function(value: {number})
Allocate(1)
local bitOffset = bufferOffset * 8
bufferOffset += 1
buffer.writebits(activeBuffer, bitOffset + 0, 4, value[1])
buffer.writebits(activeBuffer, bitOffset + 4, 4, value[2])
end
types.BooleanNumber = ("BooleanNumber" :: any) :: {Boolean: boolean, Number: number}
reads.BooleanNumber = function()
local bitOffset = bufferOffset * 8
bufferOffset += 1
return {
Boolean = buffer.readbits(activeBuffer, bitOffset + 0, 1) == 1,
Number = buffer.readbits(activeBuffer, bitOffset + 1, 7),
}
end
writes.BooleanNumber = function(value: {Boolean: boolean, Number: number})
Allocate(1)
local bitOffset = bufferOffset * 8
bufferOffset += 1
buffer.writebits(activeBuffer, bitOffset + 0, 1, if value.Boolean then 1 else 0)
buffer.writebits(activeBuffer, bitOffset + 1, 7, value.Number)
end
types.Boolean1 = ("Boolean1" :: any) :: {boolean}
reads.Boolean1 = function()
local bitOffset = bufferOffset * 8
bufferOffset += 1
return {
buffer.readbits(activeBuffer, bitOffset + 0, 1) == 1,
buffer.readbits(activeBuffer, bitOffset + 1, 1) == 1,
buffer.readbits(activeBuffer, bitOffset + 2, 1) == 1,
buffer.readbits(activeBuffer, bitOffset + 3, 1) == 1,
buffer.readbits(activeBuffer, bitOffset + 4, 1) == 1,
buffer.readbits(activeBuffer, bitOffset + 5, 1) == 1,
buffer.readbits(activeBuffer, bitOffset + 6, 1) == 1,
buffer.readbits(activeBuffer, bitOffset + 7, 1) == 1,
}
end
writes.Boolean1 = function(value: {boolean})
Allocate(1)
local bitOffset = bufferOffset * 8
bufferOffset += 1
buffer.writebits(activeBuffer, bitOffset + 0, 1, if value[1] then 1 else 0)
buffer.writebits(activeBuffer, bitOffset + 1, 1, if value[2] then 1 else 0)
buffer.writebits(activeBuffer, bitOffset + 2, 1, if value[3] then 1 else 0)
buffer.writebits(activeBuffer, bitOffset + 3, 1, if value[4] then 1 else 0)
buffer.writebits(activeBuffer, bitOffset + 4, 1, if value[5] then 1 else 0)
buffer.writebits(activeBuffer, bitOffset + 5, 1, if value[6] then 1 else 0)
buffer.writebits(activeBuffer, bitOffset + 6, 1, if value[7] then 1 else 0)
buffer.writebits(activeBuffer, bitOffset + 7, 1, if value[8] then 1 else 0)
end
types.CFrameF24U8 = ("CFrameF24U8" :: any) :: CFrame
reads.CFrameF24U8 = function()
return CFrame.fromEulerAnglesXYZ(ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331)
+ Vector3.new(ReadF24(), ReadF24(), ReadF24())
end
writes.CFrameF24U8 = function(value: CFrame)
local rx, ry, rz = value:ToEulerAnglesXYZ()
Allocate(12)
WriteU8(rx * 40.58451048843331 + 0.5) WriteU8(ry * 40.58451048843331 + 0.5) WriteU8(rz * 40.58451048843331 + 0.5)
WriteF24(value.X) WriteF24(value.Y) WriteF24(value.Z)
end
types.CFrameF32U8 = ("CFrameF32U8" :: any) :: CFrame
reads.CFrameF32U8 = function()
return CFrame.fromEulerAnglesXYZ(ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331)
+ Vector3.new(ReadF32(), ReadF32(), ReadF32())
end
writes.CFrameF32U8 = function(value: CFrame)
local rx, ry, rz = value:ToEulerAnglesXYZ()
Allocate(15)
WriteU8(rx * 40.58451048843331 + 0.5) WriteU8(ry * 40.58451048843331 + 0.5) WriteU8(rz * 40.58451048843331 + 0.5)
WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z)
end
types.CFrameF32U16 = ("CFrameF32U16" :: any) :: CFrame
reads.CFrameF32U16 = function()
return CFrame.fromEulerAnglesXYZ(ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361)
+ Vector3.new(ReadF32(), ReadF32(), ReadF32())
end
writes.CFrameF32U16 = function(value: CFrame)
local rx, ry, rz = value:ToEulerAnglesXYZ()
Allocate(18)
WriteU16(rx * 10430.219195527361 + 0.5) WriteU16(ry * 10430.219195527361 + 0.5) WriteU16(rz * 10430.219195527361 + 0.5)
WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z)
end
types.Region3 = ("Region3" :: any) :: Region3
reads.Region3 = function()
return Region3.new(
Vector3.new(ReadF32(), ReadF32(), ReadF32()),
Vector3.new(ReadF32(), ReadF32(), ReadF32())
)
end
writes.Region3 = function(value: Region3)
local halfSize = value.Size / 2
local minimum = value.CFrame.Position - halfSize
local maximum = value.CFrame.Position + halfSize
Allocate(24)
WriteF32(minimum.X) WriteF32(minimum.Y) WriteF32(minimum.Z)
WriteF32(maximum.X) WriteF32(maximum.Y) WriteF32(maximum.Z)
end
types.NumberSequence = ("NumberSequence" :: any) :: NumberSequence
reads.NumberSequence = function()
local length = ReadU8()
local keypoints = table.create(length)
for index = 1, length do
table.insert(keypoints, NumberSequenceKeypoint.new(ReadU8() / 255, ReadU8() / 255, ReadU8() / 255))
end
return NumberSequence.new(keypoints)
end
writes.NumberSequence = function(value: NumberSequence)
local length = #value.Keypoints
Allocate(1 + length * 3)
WriteU8(length)
for index, keypoint in value.Keypoints do
WriteU8(keypoint.Time * 255 + 0.5) WriteU8(keypoint.Value * 255 + 0.5) WriteU8(keypoint.Envelope * 255 + 0.5)
end
end
types.ColorSequence = ("ColorSequence" :: any) :: ColorSequence
reads.ColorSequence = function()
local length = ReadU8()
local keypoints = table.create(length)
for index = 1, length do
table.insert(keypoints, ColorSequenceKeypoint.new(ReadU8() / 255, Color3.fromRGB(ReadU8(), ReadU8(), ReadU8())))
end
return ColorSequence.new(keypoints)
end
writes.ColorSequence = function(value: ColorSequence)
local length = #value.Keypoints
Allocate(1 + length * 4)
WriteU8(length)
for index, keypoint in value.Keypoints do
WriteU8(keypoint.Time * 255 + 0.5)
WriteU8(keypoint.Value.R * 255 + 0.5) WriteU8(keypoint.Value.G * 255 + 0.5) WriteU8(keypoint.Value.B * 255 + 0.5)
end
end
local characterIndices = {}
local characters = require(script.Characters)
for index, value in characters do characterIndices[value] = index end
local characterBits = math.ceil(math.log(#characters + 1, 2))
local characterBytes = characterBits / 8
types.Characters = ("Characters" :: any) :: string
reads.Characters = function()
local length = ReadU8()
local characterArray = table.create(length)
local bitOffset = bufferOffset * 8
bufferOffset += math.ceil(length * characterBytes)
for index = 1, length do
table.insert(characterArray, characters[buffer.readbits(activeBuffer, bitOffset, characterBits)])
bitOffset += characterBits
end
return table.concat(characterArray)
end
writes.Characters = function(value: string)
local length = #value
local bytes = math.ceil(length * characterBytes)
Allocate(1 + bytes)
WriteU8(length)
local bitOffset = bufferOffset * 8
for index = 1, length do
buffer.writebits(activeBuffer, bitOffset, characterBits, characterIndices[value:sub(index, index)])
bitOffset += characterBits
end
bufferOffset += bytes
end
local enumIndices = {}
local enums = require(script.Enums)
for index, static in enums do enumIndices[static] = index end
types.EnumItem = ("EnumItem" :: any) :: EnumItem
reads.EnumItem = function() return enums[ReadU8()]:FromValue(ReadU16()) end
writes.EnumItem = function(value: EnumItem) Allocate(3) WriteU8(enumIndices[value.EnumType]) WriteU16(value.Value) end
local staticIndices = {}
local statics = require(script.Static1)
for index, static in statics do staticIndices[static] = index end
types.Static1 = ("Static1" :: any) :: any
reads.Static1 = function() return statics[ReadU8()] end
writes.Static1 = function(value: any) Allocate(1) WriteU8(staticIndices[value] or 0) end
local staticIndices = {}
local statics = require(script.Static2)
for index, static in statics do staticIndices[static] = index end
types.Static2 = ("Static2" :: any) :: any
reads.Static2 = function() return statics[ReadU8()] end
writes.Static2 = function(value: any) Allocate(1) WriteU8(staticIndices[value] or 0) end
local staticIndices = {}
local statics = require(script.Static3)
for index, static in statics do staticIndices[static] = index end
types.Static3 = ("Static3" :: any) :: any
reads.Static3 = function() return statics[ReadU8()] end
writes.Static3 = function(value: any) Allocate(1) WriteU8(staticIndices[value] or 0) end
-- Any Types
anyReads[0] = function() return nil end
anyWrites["nil"] = function(value: nil) Allocate(1) WriteU8(0) end
anyReads[1] = function() return -ReadU8() end
anyReads[2] = function() return -ReadU16() end
anyReads[3] = function() return -ReadU24() end
anyReads[4] = function() return -ReadU32() end
anyReads[5] = function() return ReadU8() end
anyReads[6] = function() return ReadU16() end
anyReads[7] = function() return ReadU24() end
anyReads[8] = function() return ReadU32() end
anyReads[9] = function() return ReadF32() end
anyReads[10] = function() return ReadF64() end
anyWrites.number = function(value: number)
if value % 1 == 0 then
if value < 0 then
if value > -256 then
Allocate(2) WriteU8(1) WriteU8(-value)
elseif value > -65536 then
Allocate(3) WriteU8(2) WriteU16(-value)
elseif value > -16777216 then
Allocate(4) WriteU8(3) WriteU24(-value)
elseif value > -4294967296 then
Allocate(5) WriteU8(4) WriteU32(-value)
else
Allocate(9) WriteU8(10) WriteF64(value)
end
else
if value < 256 then
Allocate(2) WriteU8(5) WriteU8(value)
elseif value < 65536 then
Allocate(3) WriteU8(6) WriteU16(value)
elseif value < 16777216 then
Allocate(4) WriteU8(7) WriteU24(value)
elseif value < 4294967296 then
Allocate(5) WriteU8(8) WriteU32(value)
else
Allocate(9) WriteU8(10) WriteF64(value)
end
end
elseif value > -1048576 and value < 1048576 then
Allocate(5) WriteU8(9) WriteF32(value)
else
Allocate(9) WriteU8(10) WriteF64(value)
end
end
anyReads[11] = function() return ReadString(ReadU8()) end
anyWrites.string = function(value: string) local length = #value Allocate(2 + length) WriteU8(11) WriteU8(length) WriteString(value) end
anyReads[12] = function() return ReadBuffer(ReadU8()) end
anyWrites.buffer = function(value: buffer) local length = buffer.len(value) Allocate(2 + length) WriteU8(12) WriteU8(length) WriteBuffer(value) end
anyReads[13] = function() return ReadInstance() end
anyWrites.Instance = function(value: Instance) Allocate(1) WriteU8(13) WriteInstance(value) end
anyReads[14] = function() return ReadU8() == 1 end
anyWrites.boolean = function(value: boolean) Allocate(2) WriteU8(14) WriteU8(if value then 1 else 0) end
anyReads[15] = function() return NumberRange.new(ReadF32(), ReadF32()) end
anyWrites.NumberRange = function(value: NumberRange) Allocate(9) WriteU8(15) WriteF32(value.Min) WriteF32(value.Max) end
anyReads[16] = function() return BrickColor.new(ReadU16()) end
anyWrites.BrickColor = function(value: BrickColor) Allocate(3) WriteU8(16) WriteU16(value.Number) end
anyReads[17] = function() return Color3.fromRGB(ReadU8(), ReadU8(), ReadU8()) end
anyWrites.Color3 = function(value: Color3) Allocate(4) WriteU8(17) WriteU8(value.R * 255 + 0.5) WriteU8(value.G * 255 + 0.5) WriteU8(value.B * 255 + 0.5) end
anyReads[18] = function() return UDim.new(ReadS16() / 1000, ReadS16()) end
anyWrites.UDim = function(value: UDim) Allocate(5) WriteU8(18) WriteS16(value.Scale * 1000) WriteS16(value.Offset) end
anyReads[19] = function() return UDim2.new(ReadS16() / 1000, ReadS16(), ReadS16() / 1000, ReadS16()) end
anyWrites.UDim2 = function(value: UDim2) Allocate(9) WriteU8(19) WriteS16(value.X.Scale * 1000) WriteS16(value.X.Offset) WriteS16(value.Y.Scale * 1000) WriteS16(value.Y.Offset) end
anyReads[20] = function() return Rect.new(ReadF32(), ReadF32(), ReadF32(), ReadF32()) end
anyWrites.Rect = function(value: Rect) Allocate(17) WriteU8(20) WriteF32(value.Min.X) WriteF32(value.Min.Y) WriteF32(value.Max.X) WriteF32(value.Max.Y) end
anyReads[21] = function() return Vector2.new(ReadF32(), ReadF32()) end
anyWrites.Vector2 = function(value: Vector2) Allocate(9) WriteU8(21) WriteF32(value.X) WriteF32(value.Y) end
anyReads[22] = function() return Vector3.new(ReadF32(), ReadF32(), ReadF32()) end
anyWrites.Vector3 = function(value: Vector3) Allocate(13) WriteU8(22) WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z) end
anyReads[23] = function()
return CFrame.fromEulerAnglesXYZ(ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361)
+ Vector3.new(ReadF32(), ReadF32(), ReadF32())
end
anyWrites.CFrame = function(value: CFrame)
local rx, ry, rz = value:ToEulerAnglesXYZ()
Allocate(19)
WriteU8(23)
WriteU16(rx * 10430.219195527361 + 0.5) WriteU16(ry * 10430.219195527361 + 0.5) WriteU16(rz * 10430.219195527361 + 0.5)
WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z)
end
anyReads[24] = function()
return Region3.new(
Vector3.new(ReadF32(), ReadF32(), ReadF32()),
Vector3.new(ReadF32(), ReadF32(), ReadF32())
)
end
anyWrites.Region3 = function(value: Region3)
local halfSize = value.Size / 2
local minimum = value.CFrame.Position - halfSize
local maximum = value.CFrame.Position + halfSize
Allocate(25)
WriteU8(24)
WriteF32(minimum.X) WriteF32(minimum.Y) WriteF32(minimum.Z)
WriteF32(maximum.X) WriteF32(maximum.Y) WriteF32(maximum.Z)
end
anyReads[25] = function()
local length = ReadU8()
local keypoints = table.create(length)
for index = 1, length do
table.insert(keypoints, NumberSequenceKeypoint.new(ReadU8() / 255, ReadU8() / 255, ReadU8() / 255))
end
return NumberSequence.new(keypoints)
end
anyWrites.NumberSequence = function(value: NumberSequence)
local length = #value.Keypoints
Allocate(2 + length * 3)
WriteU8(25)
WriteU8(length)
for index, keypoint in value.Keypoints do
WriteU8(keypoint.Time * 255 + 0.5) WriteU8(keypoint.Value * 255 + 0.5) WriteU8(keypoint.Envelope * 255 + 0.5)
end
end
anyReads[26] = function()
local length = ReadU8()
local keypoints = table.create(length)
for index = 1, length do
table.insert(keypoints, ColorSequenceKeypoint.new(ReadU8() / 255, Color3.fromRGB(ReadU8(), ReadU8(), ReadU8())))
end
return ColorSequence.new(keypoints)
end
anyWrites.ColorSequence = function(value: ColorSequence)
local length = #value.Keypoints
Allocate(2 + length * 4)
WriteU8(26)
WriteU8(length)
for index, keypoint in value.Keypoints do
WriteU8(keypoint.Time * 255 + 0.5)
WriteU8(keypoint.Value.R * 255 + 0.5) WriteU8(keypoint.Value.G * 255 + 0.5) WriteU8(keypoint.Value.B * 255 + 0.5)
end
end
anyReads[27] = function()
return enums[ReadU8()]:FromValue(ReadU16())
end
anyWrites.EnumItem = function(value: EnumItem)
Allocate(4)
WriteU8(27)
WriteU8(enumIndices[value.EnumType])
WriteU16(value.Value)
end
anyReads[28] = function()
local value = {}
while true do
local typeId = ReadU8()
if typeId == 0 then return value else value[anyReads[typeId]()] = anyReads[ReadU8()]() end
end
end
anyWrites.table = function(value: {[any]: any})
Allocate(1)
WriteU8(28)
for index, value in value do anyWrites[typeof(index)](index) anyWrites[typeof(value)](value) end
Allocate(1)
WriteU8(0)
end
return {
Import = function(cursor: Cursor)
activeCursor = cursor
activeBuffer = cursor.Buffer
bufferLength = cursor.BufferLength
bufferOffset = cursor.BufferOffset
instances = cursor.Instances
instancesOffset = cursor.InstancesOffset
end,
Export = function()
activeCursor.BufferLength = bufferLength
activeCursor.BufferOffset = bufferOffset
activeCursor.InstancesOffset = instancesOffset
return activeCursor
end,
Truncate = function()
local truncatedBuffer = buffer.create(bufferOffset)
buffer.copy(truncatedBuffer, 0, activeBuffer, 0, bufferOffset)
if instancesOffset == 0 then return truncatedBuffer else return truncatedBuffer, instances end
end,
Ended = function()
return bufferOffset >= bufferLength
end,
Types = types,
Reads = reads,
Writes = writes,
}

View file

@ -0,0 +1,368 @@
--!strict
-- Requires
local Signal = require(script.Signal)
local Task = require(script.Task)
local Types = require(script.Types)
-- Types
export type Packet<A... = (), B... = ()> = {
Type: "Packet",
Id: number,
Name: string,
Reads: {() -> any},
Writes: {(any) -> ()},
ResponseTimeout: number,
ResponseTimeoutValue: any,
ResponseReads: {() -> any},
ResponseWrites: {(any) -> ()},
OnServerEvent: Signal.Signal<(Player, A...)>,
OnClientEvent: Signal.Signal<A...>,
OnServerInvoke: nil | (player: Player, A...) -> B...,
OnClientInvoke: nil | (A...) -> B...,
Response: (self: Packet<A..., B...>, B...) -> Packet<A..., B...>,
Fire: (self: Packet<A..., B...>, A...) -> B...,
FireClient: (self: Packet<A..., B...>, player: Player, A...) -> B...,
Serialize: (self: Packet<A..., B...>, A...) -> (buffer, {Instance}?),
Deserialize: (self: Packet<A..., B...>, serializeBuffer: buffer, instances: {Instance}?) -> A...,
}
-- Varables
local ParametersToFunctions, TableToFunctions, ReadParameters, WriteParameters, Timeout
local RunService = game:GetService("RunService")
local PlayersService = game:GetService("Players")
local reads, writes, Import, Export, Truncate, Ended = Types.Reads, Types.Writes, Types.Import, Types.Export, Types.Truncate, Types.Ended
local ReadU8, WriteU8, ReadU16, WriteU16 = reads.NumberU8, writes.NumberU8, reads.NumberU16, writes.NumberU16
local Packet = {} :: Packet<...any, ...any>
local packets = {} :: {[string | number]: Packet<...any, ...any>}
local playerCursors : {[Player]: Types.Cursor}
local playerThreads : {[Player]: {[number]: {Yielded: thread, Timeout: thread}, Index: number}}
local threads : {[number]: {Yielded: thread, Timeout: thread}, Index: number}
local remoteEvent : RemoteEvent
local packetCounter : number
local cursor = {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0}
-- Constructor
local function Constructor<A..., B...>(_, name: string, ...: A...)
local packet = packets[name] :: Packet<A..., B...>
if packet then return packet end
local packet = (setmetatable({}, Packet) :: any) :: Packet<A..., B...>
packet.Name = name
if RunService:IsServer() then
packet.Id = packetCounter
packet.OnServerEvent = Signal() :: Signal.Signal<(Player, A...)>
remoteEvent:SetAttribute(name, packetCounter)
packets[packetCounter] = packet
packetCounter += 1
else
packet.Id = remoteEvent:GetAttribute(name)
packet.OnClientEvent = Signal() :: Signal.Signal<A...>
if packet.Id then packets[packet.Id] = packet end
end
packet.Reads, packet.Writes = ParametersToFunctions(table.pack(...))
packets[packet.Name] = packet
return packet
end
-- Packet
Packet["__index"] = Packet
Packet.Type = "Packet"
function Packet:Response(...)
self.ResponseTimeout = self.ResponseTimeout or 10
self.ResponseReads, self.ResponseWrites = ParametersToFunctions(table.pack(...))
return self
end
function Packet:Fire(...)
Import(cursor)
WriteU8(self.Id)
if self.ResponseReads then
WriteU8(threads.Index)
threads[threads.Index] = {Yielded = coroutine.running(), Timeout = Task:Delay(self.ResponseTimeout, Timeout, coroutine.running(), self.ResponseTimeoutValue)}
threads.Index = (threads.Index + 1) % 128
WriteParameters(self.Writes, {...})
cursor = Export()
return coroutine.yield()
else
WriteParameters(self.Writes, {...})
cursor = Export()
end
end
function Packet:FireClient(player, ...)
if player.Parent == nil then return end
Import(playerCursors[player] or {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0})
WriteU8(self.Id)
if self.ResponseReads then
local threads = playerThreads[player]
if threads == nil then threads = {Index = 0} playerThreads[player] = threads end
WriteU8(threads.Index)
threads[threads.Index] = {Yielded = coroutine.running(), Timeout = Task:Delay(self.ResponseTimeout, Timeout, coroutine.running(), self.ResponseTimeoutValue)}
threads.Index = (threads.Index + 1) % 128
WriteParameters(self.Writes, {...})
playerCursors[player] = Export()
return coroutine.yield()
else
WriteParameters(self.Writes, {...})
playerCursors[player] = Export()
end
end
function Packet:Serialize(...)
Import({Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0})
WriteParameters(self.Writes, {...})
return Truncate()
end
function Packet:Deserialize(serializeBuffer, instances)
Import({Buffer = serializeBuffer, BufferLength = buffer.len(serializeBuffer), BufferOffset = 0, Instances = instances or {}, InstancesOffset = 0})
return ReadParameters(self.Reads)
end
-- Functions
function ParametersToFunctions(parameters: {any})
local readFunctions, writeFunctions = table.create(#parameters), table.create(#parameters)
for index, parameter in ipairs(parameters) do
if type(parameter) == "table" then
readFunctions[index], writeFunctions[index] = TableToFunctions(parameter)
else
readFunctions[index], writeFunctions[index] = reads[parameter], writes[parameter]
end
end
return readFunctions, writeFunctions
end
function TableToFunctions(parameters: {any})
if #parameters == 1 then
local parameter = parameters[1]
local ReadFunction, WriteFunction
if type(parameter) == "table" then
ReadFunction, WriteFunction = TableToFunctions(parameter)
else
ReadFunction, WriteFunction = reads[parameter], writes[parameter]
end
local Read = function()
local length = ReadU16()
local values = table.create(length)
for index = 1, length do values[index] = ReadFunction() end
return values
end
local Write = function(values: {any})
WriteU16(#values)
for index, value in values do WriteFunction(value) end
end
return Read, Write
else
local keys = {} for key, value in parameters do table.insert(keys, key) end table.sort(keys)
local readFunctions, writeFunctions = table.create(#keys), table.create(#keys)
for index, key in keys do
local parameter = parameters[key]
if type(parameter) == "table" then
readFunctions[index], writeFunctions[index] = TableToFunctions(parameter)
else
readFunctions[index], writeFunctions[index] = reads[parameter], writes[parameter]
end
end
local Read = function()
local values = {}
for index, ReadFunction in readFunctions do values[keys[index]] = ReadFunction() end
return values
end
local Write = function(values: {[any]: any})
for index, WriteFunction in writeFunctions do WriteFunction(values[keys[index]]) end
end
return Read, Write
end
end
function ReadParameters(reads: {() -> any})
local values = table.create(#reads)
for index, func in reads do values[index] = func() end
return table.unpack(values)
end
function WriteParameters(writes: {(any) -> ()}, values: {any})
for index, func in writes do func(values[index]) end
end
function Timeout(thread: thread, value: any)
task.defer(thread, value)
end
-- Initialize
if RunService:IsServer() then
playerCursors = {}
playerThreads = {}
packetCounter = 0
remoteEvent = Instance.new("RemoteEvent", script)
local playerBytes = {}
local thread = task.spawn(function()
while true do
coroutine.yield()
if cursor.BufferOffset > 0 then
local truncatedBuffer = buffer.create(cursor.BufferOffset)
buffer.copy(truncatedBuffer, 0, cursor.Buffer, 0, cursor.BufferOffset)
if cursor.InstancesOffset == 0 then
remoteEvent:FireAllClients(truncatedBuffer)
else
remoteEvent:FireAllClients(truncatedBuffer, cursor.Instances)
cursor.InstancesOffset = 0
table.clear(cursor.Instances)
end
cursor.BufferOffset = 0
end
for player, cursor in playerCursors do
local truncatedBuffer = buffer.create(cursor.BufferOffset)
buffer.copy(truncatedBuffer, 0, cursor.Buffer, 0, cursor.BufferOffset)
if cursor.InstancesOffset == 0 then
remoteEvent:FireClient(player, truncatedBuffer)
else
remoteEvent:FireClient(player, truncatedBuffer, cursor.Instances)
end
end
table.clear(playerCursors)
table.clear(playerBytes)
end
end)
local respond = function(packet: Packet, player: Player, threadIndex: number, ...)
if packet.OnServerInvoke == nil then if RunService:IsStudio() then warn("OnServerInvoke not found for packet:", packet.Name, "discarding event:", ...) end return end
local values = {packet.OnServerInvoke(player, ...)}
if player.Parent == nil then return end
Import(playerCursors[player] or {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0})
WriteU8(packet.Id)
WriteU8(threadIndex + 128)
WriteParameters(packet.ResponseWrites, values)
playerCursors[player] = Export()
end
local onServerEvent = function(player: Player, receivedBuffer: buffer, instances: {Instance}?)
local bytes = (playerBytes[player] or 0) + math.max(buffer.len(receivedBuffer), 800)
if bytes > 8_000 then if RunService:IsStudio() then warn(player.Name, "is exceeding the data/rate limit; some events may be dropped") end return end
playerBytes[player] = bytes
Import({Buffer = receivedBuffer, BufferLength = buffer.len(receivedBuffer), BufferOffset = 0, Instances = instances or {}, InstancesOffset = 0})
while Ended() == false do
local packet = packets[ReadU8()]
if packet.ResponseReads then
local threadIndex = ReadU8()
if threadIndex < 128 then
Task:Defer(respond, packet, player, threadIndex, ReadParameters(packet.Reads))
else
threadIndex -= 128
local threads = playerThreads[player][threadIndex]
if threads then
task.cancel(threads.Timeout)
task.defer(threads.Yielded, ReadParameters(packet.ResponseReads))
playerThreads[player][threadIndex] = nil
elseif RunService:IsStudio() then
warn("Response thread not found for packet:", packet.Name, "discarding response:", ReadParameters(packet.ResponseReads))
else
ReadParameters(packet.ResponseReads)
end
end
else
packet.OnServerEvent:Fire(player, ReadParameters(packet.Reads))
end
end
end
remoteEvent.OnServerEvent:Connect(function(player: Player, ...)
local success, errorMessage: string? = pcall(onServerEvent, player, ...)
if errorMessage and RunService:IsStudio() then warn(player.Name, errorMessage) end
end)
PlayersService.PlayerRemoving:Connect(function(player)
playerCursors[player] = nil
playerThreads[player] = nil
playerBytes[player] = nil
end)
RunService.Heartbeat:Connect(function(deltaTime) task.defer(thread) end)
else
threads = {Index = 0}
remoteEvent = script:WaitForChild("RemoteEvent")
local totalTime = 0
local thread = task.spawn(function()
while true do
coroutine.yield()
if cursor.BufferOffset > 0 then
local truncatedBuffer = buffer.create(cursor.BufferOffset)
buffer.copy(truncatedBuffer, 0, cursor.Buffer, 0, cursor.BufferOffset)
if cursor.InstancesOffset == 0 then
remoteEvent:FireServer(truncatedBuffer)
else
remoteEvent:FireServer(truncatedBuffer, cursor.Instances)
cursor.InstancesOffset = 0
table.clear(cursor.Instances)
end
cursor.BufferOffset = 0
end
end
end)
local respond = function(packet: Packet, threadIndex: number, ...)
if packet.OnClientInvoke == nil then warn("OnClientInvoke not found for packet:", packet.Name, "discarding event:", ...) return end
local values = {packet.OnClientInvoke(...)}
Import(cursor)
WriteU8(packet.Id)
WriteU8(threadIndex + 128)
WriteParameters(packet.ResponseWrites, values)
cursor = Export()
end
remoteEvent.OnClientEvent:Connect(function(receivedBuffer: buffer, instances: {Instance}?)
Import({Buffer = receivedBuffer, BufferLength = buffer.len(receivedBuffer), BufferOffset = 0, Instances = instances or {}, InstancesOffset = 0})
while Ended() == false do
local packet = packets[ReadU8()]
if packet.ResponseReads then
local threadIndex = ReadU8()
if threadIndex < 128 then
Task:Defer(respond, packet, threadIndex, ReadParameters(packet.Reads))
else
threadIndex -= 128
local threads = threads[threadIndex]
if threads then
task.cancel(threads.Timeout)
task.defer(threads.Yielded, ReadParameters(packet.ResponseReads))
threads[threadIndex] = nil
else
warn("Response thread not found for packet:", packet.Name, "discarding response:", ReadParameters(packet.ResponseReads))
end
end
else
packet.OnClientEvent:Fire(ReadParameters(packet.Reads))
end
end
end)
remoteEvent.AttributeChanged:Connect(function(name)
local packet = packets[name]
if packet then
if packet.Id then packets[packet.Id] = nil end
packet.Id = remoteEvent:GetAttribute(name)
if packet.Id then packets[packet.Id] = packet end
end
end)
RunService.Heartbeat:Connect(function(deltaTime)
totalTime += deltaTime
if totalTime > 0.016666666666666666 then
totalTime %= 0.016666666666666666
task.defer(thread)
end
end)
end
return setmetatable(Types.Types, {__call = Constructor})

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
--!strict
--From Roblox docs
local Queue = {}
Queue.__index = Queue
export type Queue<T> = typeof(setmetatable(
{} :: {
_first: number,
_last: number,
_queue: { T },
},
Queue
))
function Queue.new<T>(): Queue<T>
local self = setmetatable({
_first = 0,
_last = -1,
_queue = {},
}, Queue)
return self
end
-- Check if the queue is empty
function Queue.isEmpty<T>(self: Queue<T>)
return self._first > self._last
end
-- Add a value to the queue
function Queue.enqueue<T>(self: Queue<T>, value: T)
local last = self._last + 1
self._last = last
self._queue[last] = value
end
-- Remove a value from the queue
function Queue.dequeue<T>(self: Queue<T>): T?
if self:isEmpty() then
return nil
end
local first = self._first
local value = self._queue[first]
self._queue[first] = nil
self._first = first + 1
return value
end
return Queue

View file

@ -0,0 +1,113 @@
--- Lua-side duplication of the API of events on Roblox objects.
-- Signals are needed for to ensure that for local events objects are passed by
-- reference rather than by value where possible, as the BindableEvent objects
-- always pass signal arguments by value, meaning tables will be deep copied.
-- Roblox's deep copy method parses to a non-lua table compatable format.
-- @classmod Signal
local HttpService = game:GetService("HttpService")
local ENABLE_TRACEBACK = false
local Signal = {}
Signal.__index = Signal
Signal.ClassName = "Signal"
--- Constructs a new signal.
-- @constructor Signal.new()
-- @treturn Signal
function Signal.new()
local self = setmetatable({}, Signal)
self._bindableEvent = Instance.new("BindableEvent")
self._argMap = {}
self._source = ENABLE_TRACEBACK and debug.traceback() or ""
-- Events in Roblox execute in reverse order as they are stored in a linked list and
-- new connections are added at the head. This event will be at the tail of the list to
-- clean up memory.
self._bindableEvent.Event:Connect(function(key)
self._argMap[key] = nil
-- We've been destroyed here and there's nothing left in flight.
-- Let's remove the argmap too.
-- This code may be slower than leaving this table allocated.
if (not self._bindableEvent) and (not next(self._argMap)) then
self._argMap = nil
end
end)
return self
end
--- Fire the event with the given arguments. All handlers will be invoked. Handlers follow
-- Roblox signal conventions.
-- @param ... Variable arguments to pass to handler
-- @treturn nil
function Signal:Fire(...)
if not self._bindableEvent then
warn(("Signal is already destroyed. %s"):format(self._source))
return
end
local args = table.pack(...)
-- TODO: Replace with a less memory/computationally expensive key generation scheme
local key = HttpService:GenerateGUID(false)
self._argMap[key] = args
-- Queues each handler onto the queue.
self._bindableEvent:Fire(key)
end
--- Connect a new handler to the event. Returns a connection object that can be disconnected.
-- @tparam function handler Function handler called with arguments passed when `:Fire(...)` is called
-- @treturn Connection Connection object that can be disconnected
function Signal:Connect(handler)
if not (type(handler) == "function") then
error(("connect(%s)"):format(typeof(handler)), 2)
end
return self._bindableEvent.Event:Connect(function(key)
-- note we could queue multiple events here, but we'll do this just as Roblox events expect
-- to behave.
local args = self._argMap[key]
if args then
handler(table.unpack(args, 1, args.n))
else
error("Missing arg data, probably due to reentrance.")
end
end)
end
--- Wait for fire to be called, and return the arguments it was given.
-- @treturn ... Variable arguments from connection
function Signal:Wait()
local key = self._bindableEvent.Event:Wait()
local args = self._argMap[key]
if args then
return table.unpack(args, 1, args.n)
else
error("Missing arg data, probably due to reentrance.")
return nil
end
end
--- Disconnects all connected events to the signal. Voids the signal as unusable.
-- @treturn nil
function Signal:Destroy()
if self._bindableEvent then
-- This should disconnect all events, but in-flight events should still be
-- executed.
self._bindableEvent:Destroy()
self._bindableEvent = nil
end
-- Do not remove the argmap. It will be cleaned up by the cleanup connection.
setmetatable(self, nil)
end
return Signal

View file

@ -0,0 +1,237 @@
-- Tree Library == Vibe coded by Sovereignty
export type Node<value = any> = {
Key: string,
Value: value,
Children: {Node<value>},
Parent: Node<value>?,
FullPath: string,
new: (key: string, value: any?) -> Node<value>,
AddChild: (self: Node<value>, key: string, value: any?) -> Node<value>,
GetChild: (self: Node<value>, key: string) -> Node<value>?,
GetChildren: (self: Node<value>) -> {Node<value>},
GetAllDescendants: (self: Node<value>) -> {Node<value>},
GetPath: (self: Node<value>) -> {string},
SetValue: (self: Node<value>, value: any?) -> (),
TraverseDFS: (self: Node<value>, callback: (node: Node<value>) -> ()) -> (),
TraverseBFS: (self: Node<value>, callback: (node: Node<value>) -> ()) -> (),
}
export type Tree<nodeValue = any> = {
Root: Node<nodeValue>,
new: (rootKey: string?, rootValue: any?) -> (Tree<nodeValue>),
AddNode: (self: Tree<nodeValue>, pathParts: {string}, value: any?) -> Node<nodeValue>,
GetNode: (self: Tree<nodeValue>, pathParts: {string}) -> Node<nodeValue>?,
GetNodeChildrenByPath: (self: Tree<nodeValue>, pathParts: {string}) -> {Node<nodeValue>},
GetDescendantsByPath: (self: Tree<nodeValue>, pathParts: {string}) -> {Node<nodeValue>},
FindNode: (self: Tree<nodeValue>, predicate: (node: Node<nodeValue>) -> boolean) -> Node<nodeValue>?,
RemoveNode: (self: Tree<nodeValue>, pathParts: {string}) -> boolean,
UpdateNode: (self: Tree<nodeValue>, pathParts: {string}, newValue: any) -> boolean,
GetPathString: (self: Tree<nodeValue>, pathParts: {string}) -> string,
Traverse: (self: Tree<nodeValue>, method: "DFS" | "BFS", callback: (node: Node<nodeValue>) -> ()) -> (),
Print: (self: Tree<nodeValue>) -> (),
}
local Node = {} :: Node
Node.__index = Node
function Node.new(key: string, value: any?): Node
return setmetatable({
Key = key,
Value = value,
Children = {} :: {Node},
Parent = nil :: Node?,
FullPath = ""
}, Node) :: any
end
function Node:AddChild(key: string, value: any?): Node
local child = Node.new(key, value)
child.Parent = self
if self.FullPath == "/" then
child.FullPath = "/" .. key
else
child.FullPath = self.FullPath .. "/" .. key
end
table.insert(self.Children, child)
return child
end
function Node:GetChild(key: string): Node?
for _, child in ipairs(self.Children) do
if child.Key == key then
return child
end
end
return nil
end
function Node:GetChildren(): {Node}
return self.Children
end
function Node:GetAllDescendants(): {Node}
local descendants = {} :: {Node}
local function traverse(node: Node)
for _, child in ipairs(node.Children) do
table.insert(descendants, child)
traverse(child)
end
end
traverse(self)
return descendants
end
function Node:GetPath(): {string}
local parts = {} :: {string}
local current: Node? = self
while current do
table.insert(parts, 1, current.Key)
current = current.Parent
end
return parts
end
function Node:SetValue(value: any?)
self.Value = value
end
function Node:TraverseDFS(callback: (node: Node) -> ())
callback(self)
for _, child in ipairs(self.Children) do
child:TraverseDFS(callback)
end
end
function Node:TraverseBFS(callback: (node: Node) -> ())
local queue = {self} :: {Node}
while #queue > 0 do
local current = table.remove(queue, 1)
callback(current)
for _, child in ipairs(current.Children) do
table.insert(queue, child)
end
end
end
local Tree = {} :: Tree
Tree.__index = Tree
function Tree.new(rootKey: string?, rootValue: any?): Tree
local rootKey = rootKey or "/"
local root = Node.new(rootKey, rootValue)
root.FullPath = rootKey == "/" and "/" or "/" .. rootKey
return setmetatable({
Root = root
}, Tree) :: Tree
end
function Tree:AddNode(pathParts: {string}, value: any?): Node
local current: Node = self.Root
for _, part in ipairs(pathParts) do
local child = current:GetChild(part)
if not child then
child = current:AddChild(part, nil)
end
current = child
end
current.Value = value
return current
end
function Tree:GetNode(pathParts: {string}): Node?
local current: Node? = self.Root
for _, part in ipairs(pathParts) do
local nextNode = current:GetChild(part)
if not nextNode then break end
current = nextNode
end
return current ~= self.Root and current
end
function Tree:GetNodeChildrenByPath(pathParts: {string}): {Node}
local node = self:GetNode(pathParts)
return node and node:GetChildren() or {}
end
function Tree:GetDescendantsByPath(pathParts: {string}): {Node}
local node = self:GetNode(pathParts)
return node and node:GetAllDescendants() or {}
end
function Tree:FindNode(predicate: (node: Node) -> boolean): Node?
local found: Node? = nil
self.Root:TraverseDFS(function(node)
if predicate(node) then
found = node
end
end)
return found
end
function Tree:RemoveNode(pathParts: {string}): boolean
local node = self:GetNode(pathParts)
if not node or node == self.Root then return false end
local parent = node.Parent
if not parent then return false end
for i, child in ipairs(parent.Children) do
if child == node then
table.remove(parent.Children, i)
return true
end
end
return false
end
function Tree:UpdateNode(pathParts: {string}, newValue: any): boolean
local node = self:GetNode(pathParts)
if node then
node.Value = newValue
return true
end
return false
end
function Tree:GetPathString(pathParts: {string}): string
return "/" .. table.concat(pathParts, "/")
end
function Tree:Traverse(method: "DFS" | "BFS", callback: (node: Node) -> ())
if method == "DFS" then
self.Root:TraverseDFS(callback)
elseif method == "BFS" then
self.Root:TraverseBFS(callback)
else
error("Invalid traversal method. Use 'DFS' or 'BFS'")
end
end
function Tree:Print()
print("Tree Structure:")
self.Root:TraverseDFS(function(node)
local indent = string.rep(" ", #node:GetPath() - 1)
-- Fix: Format root path correctly
local displayPath = node.FullPath
if node == self.Root and node.Key == "/" then
displayPath = "/"
end
print(indent .. node.Key .. " (" .. displayPath .. ")")
end)
end
return Tree

View file

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

View file

@ -0,0 +1,323 @@
--[[
@module Reactor
This module facilitates the creation and replication of stateful objects, called "Reactions,"
from the server to clients. It is designed to be used in an ECS (Entity-Component System)
environment.
- A "Reactor" is a factory for creating "Reactions" of a specific type.
- A "Reaction" is a state object identified by a name and a unique key.
- State changes within a Reaction are automatically replicated to the appropriate clients.
- It uses a tokenization system to minimize network bandwidth for property names and paths.
Yes, the documentation was generated.
]]
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local RootFolder = script.Parent.Parent
local Packet = require(RootFolder.Packages.Packet)
local Signal = require(RootFolder.Packages.Signals)
local Promise = require(RootFolder.Packages.Promise)
local Queue = require(RootFolder.Packages.Queue)
local ECS = require(RootFolder.ECS)
local Cache = require(RootFolder.Cache)
local Symbols = require(RootFolder.Symbols)
local Effect = require(RootFolder.Factories.Effect)
local Reaction = require(RootFolder.Factories.Reaction)
local Array = require(RootFolder.Functions.Array)
local Blueprint = require(RootFolder.Functions.Blueprint)
local Is = require(RootFolder.Functions.Is)
type Player = { UserId: number }
type Symbol = Symbols.Symbol
type Reaction<T> = Reaction.Reaction<T> & { To: { Player } | Symbol, Name: string, Key: string }
local network = {
Create = Packet("C__Create", Packet.String, Packet.String, { Packet.NumberU8 }, Packet.Any, Packet.Any),
Update = Packet("C__Update", { Packet.NumberU8 }, { Packet.NumberU8 }, Packet.Any),
UpdateChanges = Packet("C__UpdateChange", { Packet.NumberU8 }, { Packet.NumberU8 }, Packet.Any), -- TODO
Destroy = Packet("C__Destroy", { Packet.NumberU8 }),
}
local Tokens = Cache.Tokens.new()
local Reactions: { [string]: { [string]: Reaction<any> } } = {}
local ReactionQueues: { [string]: { [string]: Queue.Queue<{ { number } | any }> } } = {}
local OnReactionCreated = Signal.new()
--- Recursively walks a table structure and creates a map of string keys to numerical tokens.
-- This is used to prepare a state object for replication, ensuring the client can reconstruct it.
local function createPathTokenMap(
snapshotTable: { [any]: any },
originalTable: { [any]: any }
): { [string]: number }
local result = {}
for key, snapshotValue in pairs(snapshotTable) do
local originalValue = originalTable[key]
if typeof(key) ~= "string" then
continue
end
result[key] = Tokens:ToToken(key)
if Is.Array(snapshotValue) and Is.Array(originalValue) then
for k, token in createPathTokenMap(snapshotValue, originalValue) do
result[k] = token
end
end
end
return result
end
--- Sends a network packet to a specified target (all players or a list of players).
local AllPlayers = Symbols.All("Players")
local function sendToTarget(target: { Player } | Symbol, packet: any, ...: any)
if target == AllPlayers then
packet:Fire(...)
else
for _, player in target :: { Player } do
packet:FireClient(player, ...)
end
end
end
--- Handles the server-side logic for replicating a Reaction to clients.
local function replicate(reaction: Reaction<any>)
local name = reaction.Name
local key = reaction.Key
local target = reaction.To
local nameToken = Tokens:ToToken(name)
local keyToken = Tokens:ToToken(key)
local reactionIdTokens = { nameToken, keyToken }
local blueprint = reaction:blueprint()
local pathTokenMap = createPathTokenMap(reaction:snapshot(), reaction:get())
sendToTarget(target, network.Create, name, key, reactionIdTokens, pathTokenMap, blueprint)
Array.Walk(reaction:get(), function(path: { string }, value: any)
if Is.Stateful(value) then
local pathTokens = Tokens:ToTokenPath(path)
local eff = Effect(function()
sendToTarget(target, network.Update, reactionIdTokens, pathTokens, value:get())
end)
ECS.World:add(eff.entity, ECS.JECS.pair(ECS.Tags.InScope, reaction.entity))
end
end)
-- Patch the reaction's cleanup to notify clients of its destruction.
ECS.World:set(reaction.entity, ECS.Components.CleanupFn, function()
if Reactions[name] and Reactions[name][key] then
Reactions[name][key] = nil
end
sendToTarget(target, network.Destroy, reactionIdTokens)
end)
end
if RunService:IsClient() then
--- Reconstructs a Reaction on the client based on data from the server.
local function reconstruct(name: string, key: string, reactionIdTokens: { number }, pathTokenMap: { [string]: number }, blueprint: { string: {T: number, V: any} }): ()
if Reactions[name] and Reactions[name][key] then
return
end
-- Map the incoming tokens so we can translate them back to strings later.
Tokens:Map({ [name] = reactionIdTokens[1], [key] = reactionIdTokens[2] })
Tokens:Map(pathTokenMap)
-- Create the local version of the Reaction
local reaction = Reaction(name, key, Blueprint:Read(blueprint :: any))
reaction.Name = name
reaction.Key = key
if not Reactions[name] then
Reactions[name] = {}
end
Reactions[name][key] = reaction
-- Process any queued updates that arrived before this creation packet.
if ReactionQueues[name] and ReactionQueues[name][key] then
local queue = ReactionQueues[name][key]
while not queue:isEmpty() do
local args = queue:dequeue()
local pathTokens, value = table.unpack(args)
local path = Tokens:FromPath(pathTokens)
local statefulValue = Array.FindOnPath(reaction:get(), path)
if statefulValue and statefulValue.set then
statefulValue:set(value)
end
end
ReactionQueues[name][key] = nil -- Clear the queue
end
OnReactionCreated:Fire(name, key, reaction)
end
--- Applies a state update from the server to a local Reaction.
local function update(reactionIdTokens: { number }, pathTokens: { number }, value: any)
local name = Tokens:From(reactionIdTokens[1])
local key = Tokens:From(reactionIdTokens[2])
local path = Tokens:FromPath(pathTokens)
if not name or not key then return end
local reaction = Reactions[name] and Reactions[name][key]
-- If the reaction doesn't exist yet, queue the update.
if not reaction then
if not ReactionQueues[name] then ReactionQueues[name] = {} end
if not ReactionQueues[name][key] then ReactionQueues[name][key] = Queue.new() end
ReactionQueues[name][key]:enqueue({ pathTokens, value })
return
end
if reaction.__destroyed then return end
-- Apply the update
local container = reaction:get()
local statefulValue = Array.FindOnPath(container, path)
if statefulValue and statefulValue.set then
statefulValue:set(value)
end
end
--- Destroys a local Reaction when notified by the server.
local function destroy(reactionIdTokens: { number })
local name = Tokens:From(reactionIdTokens[1])
local key = Tokens:From(reactionIdTokens[2])
if not name or not key then return end
local reaction = Reactions[name] and Reactions[name][key]
if not reaction or not reaction.entity then return end
reaction:destroy()
Reactions[name][key] = nil
end
-- Connect client network events to their handler functions
network.Create.OnClientEvent:Connect(reconstruct)
network.Update.OnClientEvent:Connect(update)
network.Destroy.OnClientEvent:Connect(destroy)
else
Players.PlayerAdded:Connect(function(player: Player)
for _, keyedReactions in Reactions do
for _, reaction in keyedReactions do
if reaction.To == Symbols.All("Players") or table.find(reaction.To, player) then
replicate(reaction)
end
end
end
end)
end
--// Public API
local api = {}
--- Awaits the creation of a specific Reaction on the client.
-- @param name The name of the Reactor that creates the Reaction.
-- @param key The unique key of the Reaction.
-- @return A promise that resolves with the Reaction once it's created.
function api.await<T>(name: string, key: string): Reaction<T>
if Reactions[name] and Reactions[name][key] then
return Reactions[name][key]
end
return Promise.fromEvent(OnReactionCreated, function(n: string, k: string, _)
return name == n and key == k
end):andThen(function(...)
return select(3, ...) -- Return the reaction object
end):expect()
end
--- Listens for the creation of any Reaction from a specific Reactor.
-- @param name The name of the Reactor.
-- @param callback A function to call with the key and Reaction object.
-- @return A connection object with a :Disconnect() method.
function api.onCreate(name: string, callback: (key: string, reaction: Reaction<any>) -> ())
return OnReactionCreated:Connect(function(n: string, k: string, reaction: Reaction<any>)
if n == name then
task.spawn(callback, k, reaction)
end
end)
end
--- Creates a "Reactor" factory.
-- @param config A configuration table with a 'Name' and optional 'Subjects' (players).
-- @param constructor A function that returns the initial state for a new Reaction.
-- @return A Reactor object with `create` and `await` methods.
return function<T, U...>(
config: { Name: string, Subjects: { Player } | Symbol? },
constructor: (key: string, U...) -> T
)
local name = config.Name
assert(name, "Reactor config must include a 'Name'.")
local to = config.Subjects or Symbols.All("Players")
local reactor = {}
--- Creates and replicates a new Reaction. [SERVER-ONLY]
-- @param self The reactor object.
-- @param key A unique key for this Reaction.
-- @param ... Additional arguments to be passed to the constructor.
-- @return The created Reaction instance.
function reactor:create(key: string, ...: U...): Reaction<T>
assert(not RunService:IsClient(), "Reactions can only be created on the server.")
if Reactions[name] and Reactions[name][key] then
warn(string.format("Reactor '%s' is overwriting an existing reaction with key '%s'", name, key))
end
local reaction = Reaction(name, key, constructor(key, ...))
reaction.To = to
reaction.Name = name
reaction.Key = key
if not Reactions[name] then
Reactions[name] = {}
end
Reactions[name][key] = reaction
-- The new, encapsulated replicate function handles all server-side logic.
replicate(reaction)
return reaction
end
--- Awaits a specific Reaction from this Reactor. [CLIENT-ONLY]
function reactor:await(key: string): Reaction<T>
return api.await(name, key)
end
--- Listens for new Reactions created by this Reactor. [CLIENT-ONLY]
function reactor:onCreate(callback: (key: string, reaction: Reaction<T>) -> ())
return api.onCreate(name, callback)
end
return reactor
end

View file

@ -0,0 +1,95 @@
--!native
--!optimize 2
local RunService = game:GetService("RunService")
local RootFolder = script.Parent.Parent
local ECS = require(RootFolder.ECS)
local Components = ECS.Components
local Tags = ECS.Tags
local JECS = ECS.JECS
local World = ECS.World
local MAX_COMPUTATION_DEPTH = 100
local dirtySourceQuery = World:query(Components.Object)
:with(Tags.IsStateful, Tags.IsDirty)
:without(Tags.IsEffect, Tags.IsComputed)
:cached()
local dirtyComputedsQuery = World:query(Components.Object)
:with(Tags.IsComputed, Tags.IsDirty)
:cached()
local dirtyEffectsQuery = World:query(Components.Object)
:with(Tags.IsEffect, Tags.IsDirty)
:cached()
local GetSubscribers = require(RootFolder.Functions.GetSubscribers)
local Scheduler = {}
function Scheduler:Update()
-- PROPAGATE DIRTINESS
for sourceEntity, _ in dirtySourceQuery:iter() do
local subscribers = GetSubscribers(sourceEntity)
for _, subscriberEntity in ipairs(subscribers) do
if not World:has(subscriberEntity, Tags.IsDirty) then
World:add(subscriberEntity, Tags.IsDirty)
end
end
World:remove(sourceEntity, Tags.IsDirty)
end
-- RE-RUN COMPUTED VALUES
for i = 1, MAX_COMPUTATION_DEPTH do
local computedsToProcess = {}
for entity, computable in dirtyComputedsQuery:iter() do
table.insert(computedsToProcess, computable)
end
if #computedsToProcess == 0 then
break
end
for _, computable in ipairs(computedsToProcess) do
computable:compute()
World:remove(computable.entity, Tags.IsDirty)
for _, subscriber in ipairs(GetSubscribers(computable.entity)) do
World:add(subscriber, Tags.IsDirty)
end
end
if i == MAX_COMPUTATION_DEPTH then
warn("Chemical: Max computation depth exceeded. Check for a circular dependency in your Computed values.")
end
end
-- RUN EFFECTS & OBSERVERS
for _, runnable in dirtyEffectsQuery:iter() do
runnable:run()
World:remove(runnable.entity, Tags.IsDirty)
end
end
if RunService:IsServer() then
RunService.Heartbeat:Connect(function()
Scheduler:Update()
end)
else
RunService.RenderStepped:Connect(function()
Scheduler:Update()
end)
end
return Scheduler

View file

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

26
src/Chemical/Symbols.lua Normal file
View file

@ -0,0 +1,26 @@
local RootFolder = script.Parent
local GuiTypes = require(RootFolder.Types.Gui)
local GetSymbol = require(RootFolder.Functions.GetSymbol)
export type Symbol<S = string, T = string> = GetSymbol.Symbol<S, T>
local module = {}
module.OnEvent = function(eventName: GuiTypes.EventNames)
return GetSymbol(eventName, "Event")
end
module.OnChange = function(eventName: GuiTypes.PropertyNames)
return GetSymbol(eventName, "Change")
end
module.Children = GetSymbol("All", "Children")
module.All = function<S>(subjects: S)
return GetSymbol(subjects, "All") :: Symbol<S, "All">
end
--OnEvent symbols are handled by the OnEvent function.
return module

360
src/Chemical/Types/Gui.lua Normal file
View file

@ -0,0 +1,360 @@
type Stateful<T> = { set: (T) -> (), get: () -> (T), __entity: number }
export type GuiBaseProperties = {
Name: (Stateful<string> | string)?,
Visible: (Stateful<boolean> | boolean)?,
Active: (Stateful<boolean> | boolean)?,
AnchorPoint: (Stateful<Vector2> | Vector2)?,
Position: (Stateful<UDim2> | UDim2)?,
Size: (Stateful<UDim2> | UDim2)?,
Rotation: (Stateful<number> | number)?,
ZIndex: (Stateful<number> | number)?,
LayoutOrder: (Stateful<number> | number)?,
BackgroundTransparency: (Stateful<number> | number)?,
BackgroundColor3: (Stateful<Color3> | Color3)?,
BorderSizePixel: (Stateful<number> | number)?,
BorderColor3: (Stateful<Color3> | Color3)?,
ClipsDescendants: (Stateful<boolean> | boolean)?,
Selectable: (Stateful<boolean> | boolean)?,
Parent: GuiObject?,
Children: { [number]: Instance | Stateful<GuiObject> },
[any]: (() -> ())?
}
type GuiBaseEvents = {
InputBegan: (input: InputObject, gameProcessed: boolean) -> (),
InputEnded: (input: InputObject, gameProcessed: boolean) -> (),
InputChanged: (input: InputObject, gameProcessed: boolean) -> (),
-- Mouse Events
MouseEnter: () -> (),
MouseLeave: () -> (),
MouseMoved: (deltaX: number, deltaY: number) -> (),
MouseWheelForward: (scrollDelta: number) -> (),
MouseWheelBackward: (scrollDelta: number) -> (),
-- Touch Events
TouchTap: (touchPositions: {Vector2}, state: Enum.UserInputState) -> (),
TouchPinch: (scale: number, velocity: number, state: Enum.UserInputState) -> (),
TouchPan: (pan: Vector2, velocity: Vector2, state: Enum.UserInputState) -> (),
TouchSwipe: (direction: Enum.SwipeDirection, touches: number) -> (),
TouchRotate: (rotation: number, velocity: number, state: Enum.UserInputState) -> (),
TouchLongPress: (duration: number) -> (),
-- Console/Selection Events
SelectionGained: () -> (),
SelectionLost: () -> (),
SelectionChanged: (newSelection: Instance) -> (),
}
type ImageGuiProperties = GuiBaseProperties & {
Image: (Stateful<string> | string)?,
ImageColor3: (Stateful<Color3> | Color3)?,
ImageTransparency: (Stateful<number> | number)?,
ScaleType: (Stateful<Enum.ScaleType> | Enum.ScaleType)?,
SliceCenter: (Stateful<Rect> | Rect)?,
TileSize: (Stateful<UDim2> | UDim2)?,
ResampleMode: (Stateful<Enum.ResamplerMode> | Enum.ResamplerMode)?,
}
type TextGuiProperties = GuiBaseProperties & {
Text: (Stateful<string> | string)?,
TextColor3: (Stateful<Color3> | Color3)?,
TextTransparency: (Stateful<number> | number)?,
TextStrokeColor3: (Stateful<Color3> | Color3)?,
TextStrokeTransparency: (Stateful<number> | number)?,
TextScaled: (Stateful<boolean> | boolean)?,
TextSize: (Stateful<number> | number)?,
TextWrapped: (Stateful<boolean> | boolean)?,
FontFace: (Stateful<Font> | Font)?,
LineHeight: (Stateful<number> | number)?,
RichText: (Stateful<boolean> | boolean)?,
TextXAlignment: (Stateful<Enum.TextXAlignment> | Enum.TextXAlignment)?,
TextYAlignment: (Stateful<Enum.TextYAlignment> | Enum.TextYAlignment)?,
TextTruncate: (Stateful<Enum.TextTruncate> | Enum.TextTruncate)?,
[any]: (() -> ())?,
}
export type FrameProperties = GuiBaseProperties
export type TextLabelProperties = TextGuiProperties
export type ImageLabelProperties = ImageGuiProperties
-- Interactive Elements
type ButtonEvents = GuiBaseEvents & {
Activated: (inputType: Enum.UserInputType?) -> (),
MouseButton1Click: () -> (),
MouseButton2Click: () -> (),
MouseButton2Down: () -> (),
MouseButton2Up: () -> (),
MouseWheelForward: nil,
MouseWheelBackward: nil,
}
export type ButtonProperties = {
AutoButtonColor: (Stateful<boolean> | boolean)?,
Modal: (Stateful<boolean> | boolean)?,
Selected: (Stateful<boolean> | boolean)?,
ButtonHoverStyle: (Stateful<Enum.ButtonStyle> | Enum.ButtonStyle)?,
ButtonPressStyle: (Stateful<Enum.ButtonStyle> | Enum.ButtonStyle)?,
ActivationBehavior: (Stateful<Enum.ActivationBehavior> | Enum.ActivationBehavior)?,
SelectionGroup: (Stateful<number> | number)?,
SelectionBehaviorUp: (Stateful<Enum.SelectionBehavior> | Enum.SelectionBehavior)?,
SelectionBehaviorDown: (Stateful<Enum.SelectionBehavior> | Enum.SelectionBehavior)?,
SelectionBehaviorLeft: (Stateful<Enum.SelectionBehavior> | Enum.SelectionBehavior)?,
SelectionBehaviorRight: (Stateful<Enum.SelectionBehavior> | Enum.SelectionBehavior)?,
GamepadPriority: (Stateful<number> | number)?,
}
export type TextButtonProperties = TextGuiProperties & ButtonProperties
export type ImageButtonProperties = ImageGuiProperties & ButtonProperties
type TextBoxEvents = GuiBaseEvents & {
FocusLost: (enterPressed: boolean) -> (),
FocusGained: () -> (),
TextChanged: (text: string) -> (),
}
export type TextBoxProperties = TextGuiProperties & {
ClearTextOnFocus: (Stateful<boolean> | boolean)?,
MultiLine: (Stateful<boolean> | boolean)?,
PlaceholderText: (Stateful<string> | string)?,
PlaceholderColor3: (Stateful<Color3> | Color3)?,
CursorPosition: (Stateful<number> | number)?,
SelectionStart: (Stateful<number> | number)?,
ShowNativeInput: (Stateful<boolean> | boolean)?,
TextInputType: (Stateful<Enum.TextInputType> | Enum.TextInputType)?,
}
-- Containers
type ScrollingFrameEvents = GuiBaseEvents & {
Scrolled: (scrollVelocity: Vector2) -> (),
}
export type ScrollingFrameProperties = FrameProperties & {
ScrollBarImageColor3: (Stateful<Color3> | Color3)?,
ScrollBarThickness: (Stateful<number> | number)?,
ScrollingDirection: (Stateful<Enum.ScrollingDirection> | Enum.ScrollingDirection)?,
CanvasSize: (Stateful<UDim2> | UDim2)?,
CanvasPosition: (Stateful<Vector2> | Vector2)?,
AutomaticCanvasSize: (Stateful<Enum.AutomaticSize> | Enum.AutomaticSize)?,
VerticalScrollBarInset: (Stateful<Enum.ScrollBarInset> | Enum.ScrollBarInset)?,
HorizontalScrollBarInset: (Stateful<Enum.ScrollBarInset> | Enum.ScrollBarInset)?,
ScrollBarImageTransparency: (Stateful<number> | number)?,
ElasticBehavior: (Stateful<Enum.ElasticBehavior> | Enum.ElasticBehavior)?,
VerticalScrollBarPosition: (Stateful<Enum.VerticalScrollBarPosition> | Enum.VerticalScrollBarPosition)?,
}
type ViewportFrameEvents = GuiBaseEvents & {
ViewportResized: (newSize: Vector2) -> (),
CameraChanged: (newCamera: Camera) -> (),
}
export type ViewportFrameProperties = FrameProperties & {
CurrentCamera: (Stateful<Camera> | Camera)?,
ImageColor3: (Stateful<Color3> | Color3)?,
LightColor: (Stateful<Color3> | Color3)?,
LightDirection: (Stateful<Vector3> | Vector3)?,
Ambient: (Stateful<Color3> | Color3)?,
LightAngularInfluence: (Stateful<number> | number)?,
}
-- Layouts
export type UIListLayoutProperties = {
Padding: (Stateful<UDim> | UDim)?,
FillDirection: (Stateful<Enum.FillDirection> | Enum.FillDirection)?,
HorizontalAlignment: (Stateful<Enum.HorizontalAlignment> | Enum.HorizontalAlignment)?,
VerticalAlignment: (Stateful<Enum.VerticalAlignment> | Enum.VerticalAlignment)?,
SortOrder: (Stateful<Enum.SortOrder> | Enum.SortOrder)?,
Appearance: (Stateful<Enum.Appearance> | Enum.Appearance)?,
}
export type UIGridLayoutProperties = {
CellSize: (Stateful<UDim2> | UDim2)?,
CellPadding: (Stateful<UDim2> | UDim2)?,
StartCorner: (Stateful<Enum.StartCorner> | Enum.StartCorner)?,
FillDirection: (Stateful<Enum.FillDirection> | Enum.FillDirection)?,
HorizontalAlignment: (Stateful<Enum.HorizontalAlignment> | Enum.HorizontalAlignment)?,
VerticalAlignment: (Stateful<Enum.VerticalAlignment> | Enum.VerticalAlignment)?,
SortOrder: (Stateful<Enum.SortOrder> | Enum.SortOrder)?,
}
-- Style Elements
export type UICornerProperties = {
CornerRadius: (Stateful<UDim> | UDim)?,
}
export type UIStrokeProperties = {
Color: (Stateful<Color3> | Color3)?,
Thickness: (Stateful<number> | number)?,
Transparency: (Stateful<number> | number)?,
Enabled: (Stateful<boolean> | boolean)?,
ApplyStrokeMode: (Stateful<Enum.ApplyStrokeMode> | Enum.ApplyStrokeMode)?,
LineJoinMode: (Stateful<Enum.LineJoinMode> | Enum.LineJoinMode)?,
}
export type UIGradientProperties = {
Color: (Stateful<ColorSequence> | ColorSequence)?,
Transparency: (Stateful<NumberSequence> | NumberSequence)?,
Offset: (Stateful<Vector2> | Vector2)?,
Rotation: (Stateful<number> | number)?,
Enabled: (Stateful<boolean> | boolean)?,
}
export type UIPaddingProperties = {
PaddingTop: (Stateful<UDim> | UDim)?,
PaddingBottom: (Stateful<UDim> | UDim)?,
PaddingLeft: (Stateful<UDim> | UDim)?,
PaddingRight: (Stateful<UDim> | UDim)?,
}
export type UIScaleProperties = {
Scale: (Stateful<number> | number)?,
}
type CanvasMouseEvents = GuiBaseEvents & {
MouseWheel: (direction: Enum.MouseWheelDirection, delta: number) -> (),
}
export type CanvasGroupProperties = {
GroupTransparency: (Stateful<number> | number)?,
GroupColor3: (Stateful<Color3> | Color3)?,
} & CanvasMouseEvents
-- Constraints
export type UIAspectRatioConstraintProperties = {
AspectRatio: (Stateful<number> | number)?,
AspectType: (Stateful<Enum.AspectType> | Enum.AspectType)?,
DominantAxis: (Stateful<Enum.DominantAxis> | Enum.DominantAxis)?,
}
export type UISizeConstraintProperties = {
MinSize: (Stateful<Vector2> | Vector2)?,
MaxSize: (Stateful<Vector2> | Vector2)?,
}
-- Specialized
export type BillboardGuiProperties = GuiBaseProperties & {
Active: (Stateful<boolean> | boolean)?,
AlwaysOnTop: (Stateful<boolean> | boolean)?,
LightInfluence: (Stateful<number> | number)?,
MaxDistance: (Stateful<number> | number)?,
SizeOffset: (Stateful<Vector2> | Vector2)?,
StudsOffset: (Stateful<Vector3> | Vector3)?,
ExtentsOffset: (Stateful<Vector3> | Vector3)?,
}
export type SurfaceGuiProperties = GuiBaseProperties & {
Active: (Stateful<boolean> | boolean)?,
AlwaysOnTop: (Stateful<boolean> | boolean)?,
Brightness: (Stateful<number> | number)?,
CanvasSize: (Stateful<Vector2> | Vector2)?,
Face: (Stateful<Enum.NormalId> | Enum.NormalId)?,
LightInfluence: (Stateful<number> | number)?,
PixelsPerStud: (Stateful<number> | number)?,
SizingMode: (Stateful<Enum.SurfaceGuiSizingMode> | Enum.SurfaceGuiSizingMode)?,
ToolPunchThroughDistance: (Stateful<number> | number)?,
}
export type ScreenGuiProperties = GuiBaseProperties & {
Active: (Stateful<boolean> | boolean)?,
AlwaysOnTop: (Stateful<boolean> | boolean)?,
Brightness: (Stateful<number> | number)?,
DisplayOrder: (Stateful<number> | number)?,
IgnoreGuiInset: (Stateful<boolean> | boolean)?,
OnTopOfCoreBlur: (Stateful<boolean> | boolean)?,
ScreenInsets: (Stateful<Enum.ScreenInsets> | Enum.ScreenInsets)?,
ZIndexBehavior: (Stateful<Enum.ZIndexBehavior> | Enum.ZIndexBehavior)?,
}
export type EventNames = (
"InputBegan" | "InputEnded" | "InputChanged" |
"MouseEnter" | "MouseLeave" | "MouseMoved" |
"MouseButton1Down" | "MouseButton1Up" |
"MouseWheelForward" | "MouseWheelBackward" |
"TouchTap" | "TouchPinch" | "TouchPan" |
"TouchSwipe" | "TouchRotate" | "TouchLongPress" |
"SelectionGained" | "SelectionLost" | "SelectionChanged" |
"Activated" | "MouseButton1Click" | "MouseButton2Click" |
"MouseButton2Down" | "MouseButton2Up" |
"FocusLost" | "FocusGained" | "TextChanged" |
"Scrolled" |
"ViewportResized" | "CameraChanged" |
"BillboardTransformed" |
"SurfaceChanged" |
"GroupTransparencyChanged" |
"StrokeUpdated" |
"GradientOffsetChanged" |
"ChildAdded" | "ChildRemoved" | "AncestryChanged"
)
export type PropertyNames = (
"Name" | "Visible" | "Active" | "AnchorPoint" | "Position" | "Size" |
"Rotation" | "ZIndex" | "LayoutOrder" | "BackgroundTransparency" |
"BackgroundColor3" | "BorderSizePixel" | "BorderColor3" |
"ClipsDescendants" | "Selectable" |
"Image" | "ImageColor3" | "ImageTransparency" | "ScaleType" |
"SliceCenter" | "TileSize" | "ResampleMode" |
"Text" | "TextColor3" | "TextTransparency" | "TextStrokeColor3" |
"TextStrokeTransparency" | "TextScaled" | "TextSize" | "TextWrapped" |
"FontFace" | "LineHeight" | "RichText" | "TextXAlignment" |
"TextYAlignment" | "TextTruncate" |
"AutoButtonColor" | "Modal" | "Selected" | "ButtonHoverStyle" |
"ButtonPressStyle" | "ActivationBehavior" | "SelectionGroup" |
"SelectionBehaviorUp" | "SelectionBehaviorDown" |
"SelectionBehaviorLeft" | "SelectionBehaviorRight" | "GamepadPriority" |
"ClearTextOnFocus" | "MultiLine" | "PlaceholderText" |
"PlaceholderColor3" | "CursorPosition" | "SelectionStart" |
"ShowNativeInput" | "TextInputType" |
"ScrollBarImageColor3" | "ScrollBarThickness" | "ScrollingDirection" |
"CanvasSize" | "CanvasPosition" | "AutomaticCanvasSize" |
"VerticalScrollBarInset" | "HorizontalScrollBarInset" |
"ScrollBarImageTransparency" | "ElasticBehavior" | "VerticalScrollBarPosition" |
"CurrentCamera" | "LightColor" | "LightDirection" | "Ambient" |
"LightAngularInfluence" |
"Padding" | "FillDirection" | "HorizontalAlignment" | "VerticalAlignment" |
"SortOrder" | "Appearance" | "CellSize" | "CellPadding" | "StartCorner" |
"CornerRadius" | "Color" | "Thickness" | "Transparency" | "Enabled" |
"ApplyStrokeMode" | "LineJoinMode" | "Offset" | "Rotation" |
"PaddingTop" | "PaddingBottom" | "PaddingLeft" | "PaddingRight" | "Scale" |
"GroupTransparency" | "GroupColor3" |
"AspectRatio" | "AspectType" | "DominantAxis" | "MinSize" | "MaxSize" |
"AlwaysOnTop" | "LightInfluence" | "MaxDistance" | "SizeOffset" |
"StudsOffset" | "ExtentsOffset" |
"Brightness" | "Face" | "PixelsPerStud" | "SizingMode" | "ToolPunchThroughDistance" |
"Parent" | "Children"
)
return {}

View file

@ -0,0 +1,66 @@
--!strict
local Gui = require(script.Parent.Gui)
-- Define the custom method we're adding.
type CompositionHandle = {
Destroy: (self: CompositionHandle) -> ()
}
-- The factory function now returns an intersection type.
-- It tells Luau "this object has all the properties of P AND all the properties of CompositionHandle".
type ComposerFactory<P> = (blueprint: P) -> (P & CompositionHandle)
-- The overloads remain the same, but their return type is now more powerful.
export type ComposeFunction = (
-- Overloads for creating new instances via class name strings
((target: "Frame") -> ComposerFactory<Gui.FrameProperties>) &
((target: "TextLabel") -> ComposerFactory<Gui.TextLabelProperties>) &
((target: "ImageLabel") -> ComposerFactory<Gui.ImageLabelProperties>) &
((target: "TextButton") -> ComposerFactory<Gui.TextButtonProperties>) &
((target: "ImageButton") -> ComposerFactory<Gui.ImageButtonProperties>) &
((target: "TextBox") -> ComposerFactory<Gui.TextBoxProperties>) &
((target: "ScrollingFrame") -> ComposerFactory<Gui.ScrollingFrameProperties>) &
((target: "ViewportFrame") -> ComposerFactory<Gui.ViewportFrameProperties>) &
((target: "CanvasGroup") -> ComposerFactory<Gui.CanvasGroupProperties>) &
((target: "UIListLayout") -> ComposerFactory<Gui.UIListLayoutProperties>) &
((target: "UIGridLayout") -> ComposerFactory<Gui.UIGridLayoutProperties>) &
((target: "UICorner") -> ComposerFactory<Gui.UICornerProperties>) &
((target: "UIStroke") -> ComposerFactory<Gui.UIStrokeProperties>) &
((target: "UIGradient") -> ComposerFactory<Gui.UIGradientProperties>) &
((target: "UIPadding") -> ComposerFactory<Gui.UIPaddingProperties>) &
((target: "UIScale") -> ComposerFactory<Gui.UIScaleProperties>) &
((target: "UIAspectRatioConstraint") -> ComposerFactory<Gui.UIAspectRatioConstraintProperties>) &
((target: "UISizeConstraint") -> ComposerFactory<Gui.UISizeConstraintProperties>) &
((target: "BillboardGui") -> ComposerFactory<Gui.BillboardGuiProperties>) &
((target: "SurfaceGui") -> ComposerFactory<Gui.SurfaceGuiProperties>) &
((target: "ScreenGui") -> ComposerFactory<Gui.ScreenGuiProperties>) &
-- Overloads for adopting existing instances
((target: Frame) -> ComposerFactory<Gui.FrameProperties>) &
((target: TextLabel) -> ComposerFactory<Gui.TextLabelProperties>) &
((target: ImageLabel) -> ComposerFactory<Gui.ImageLabelProperties>) &
((target: TextButton) -> ComposerFactory<Gui.TextButtonProperties>) &
((target: ImageButton) -> ComposerFactory<Gui.ImageButtonProperties>) &
((target: TextBox) -> ComposerFactory<Gui.TextBoxProperties>) &
((target: ScrollingFrame) -> ComposerFactory<Gui.ScrollingFrameProperties>) &
((target: ViewportFrame) -> ComposerFactory<Gui.ViewportFrameProperties>) &
((target: CanvasGroup) -> ComposerFactory<Gui.CanvasGroupProperties>) &
((target: UIListLayout) -> ComposerFactory<Gui.UIListLayoutProperties>) &
((target: UIGridLayout) -> ComposerFactory<Gui.UIGridLayoutProperties>) &
((target: UICorner) -> ComposerFactory<Gui.UICornerProperties>) &
((target: UIStroke) -> ComposerFactory<Gui.UIStrokeProperties>) &
((target: UIGradient) -> ComposerFactory<Gui.UIGradientProperties>) &
((target: UIPadding) -> ComposerFactory<Gui.UIPaddingProperties>) &
((target: UIScale) -> ComposerFactory<Gui.UIScaleProperties>) &
((target: UIAspectRatioConstraint) -> ComposerFactory<Gui.UIAspectRatioConstraintProperties>) &
((target: UISizeConstraint) -> ComposerFactory<Gui.UISizeConstraintProperties>) &
((target: BillboardGui) -> ComposerFactory<Gui.BillboardGuiProperties>) &
((target: SurfaceGui) -> ComposerFactory<Gui.SurfaceGuiProperties>) &
((target: ScreenGui) -> ComposerFactory<Gui.ScreenGuiProperties>) &
-- Fallback overloads for generic/unspecified types
((target: string) -> ComposerFactory<Gui.GuiBaseProperties>) &
((target: GuiObject) -> ComposerFactory<Gui.GuiBaseProperties>)
)
return {}

View file

@ -0,0 +1,17 @@
local ECS = require(script.Parent.Packages.JECS)
local module = {}
export type HasEntity = {
entity: ECS.Entity
}
export type MaybeDestroyable = {
__internalDestroy: (MaybeDestroyable) -> (),
}
export type MaybeCleanable = {
clean: (MaybeCleanable) -> ()
}
return module

66
src/Chemical/init.lua Normal file
View file

@ -0,0 +1,66 @@
--!strict
--version 0.2.5 == Sovereignty
--TODO:
-- Reactors and Reactions: Tables and Maps change networking, rather than full value networking on change.
-- Export Types
-- Improve file organization
-- Templating
-- Entity recycling - if possible
local ECS = require(script.ECS)
local Overrides = require(script.Types.Overrides)
local Value = require(script.Factories.Value)
local Table = require(script.Factories.Table)
local Map = require(script.Factories.Map)
local Computed = require(script.Factories.Computed)
local Observer = require(script.Factories.Observer)
local Watch = require(script.Factories.Watch)
local Effect = require(script.Factories.Effect)
local Reaction = require(script.Factories.Reaction)
local Compose = require(script.Functions.Compose)
local Is = require(script.Functions.Is)
local Peek = require(script.Functions.Peek)
local Array = require(script.Functions.Array)
local Alive = require(script.Functions.Alive)
local Destroy = require(script.Functions:FindFirstChild("Destroy"))
local Blueprint = require(script.Functions.Blueprint)
local Symbols = require(script.Symbols)
local Scheduler = require(script.Singletons.Scheduler)
local Reactor = require(script.Singletons.Reactor)
return {
Value = (Value :: any) :: Value.ValueFactory,
Table = Table,
Map = Map,
Computed = Computed,
Observer = Observer,
Watch = Watch,
Effect = Effect,
Reaction = Reaction,
Compose = (Compose :: any) :: Overrides.ComposeFunction,
Reactor = Reactor,
Is = Is,
Peek = Peek,
Array = Array,
Alive = Alive,
Destroy = Destroy,
Blueprint = Blueprint,
OnEvent = Symbols.OnEvent,
OnChange = Symbols.OnChange,
Children = Symbols.Children
}