mirror of
https://github.com/Sovvie/Chemical.git
synced 2025-06-19 11:09:18 +00:00
Chemical v0.2.5
This commit is contained in:
commit
172a63f664
72 changed files with 10826 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal 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
7
aftman.toml
Normal file
|
@ -0,0 +1,7 @@
|
|||
# This file lists tools managed by Aftman, a cross-platform toolchain manager.
|
||||
# For more information, see https://github.com/LPGhatguy/aftman
|
||||
|
||||
# To add a new tool, add an entry to this table.
|
||||
[tools]
|
||||
rojo = "rojo-rbx/rojo@7.5.1"
|
||||
# rojo = "rojo-rbx/rojo@6.2.0"
|
30
default.project.json
Normal file
30
default.project.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "project",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"ReplicatedStorage": {
|
||||
"$className": "ReplicatedStorage",
|
||||
"$ignoreUnknownInstances": true,
|
||||
"$path": "src/ReplicatedStorage"
|
||||
},
|
||||
"ServerScriptService": {
|
||||
"$className": "ServerScriptService",
|
||||
"$ignoreUnknownInstances": true,
|
||||
"$path": "src/ServerScriptService"
|
||||
},
|
||||
"StarterPlayer": {
|
||||
"$className": "StarterPlayer",
|
||||
"StarterPlayerScripts": {
|
||||
"$className": "StarterPlayerScripts",
|
||||
"$ignoreUnknownInstances": true,
|
||||
"$path": "src/StarterPlayer/StarterPlayerScripts"
|
||||
},
|
||||
"$ignoreUnknownInstances": true
|
||||
},
|
||||
"TestService": {
|
||||
"$className": "TestService",
|
||||
"$ignoreUnknownInstances": true,
|
||||
"$path": "src/TestService"
|
||||
}
|
||||
}
|
||||
}
|
1
selene.toml
Normal file
1
selene.toml
Normal file
|
@ -0,0 +1 @@
|
|||
std = "roblox"
|
123
src/Chemical/Cache.lua
Normal file
123
src/Chemical/Cache.lua
Normal file
|
@ -0,0 +1,123 @@
|
|||
local module = {}
|
||||
module.Tokens = {}
|
||||
module.Stack = {}
|
||||
module.Queues = {}
|
||||
|
||||
do
|
||||
local tokenCount = 0
|
||||
local stringsToTokens = {}
|
||||
local tokensToSrings = {}
|
||||
|
||||
local tokenMap = {}
|
||||
|
||||
function module:Tokenize(key: string): number
|
||||
if stringsToTokens[key] then return stringsToTokens[key] end
|
||||
|
||||
tokenCount += 1
|
||||
|
||||
stringsToTokens[key] = tokenCount
|
||||
tokensToSrings[tokenCount] = key
|
||||
|
||||
return tokenCount
|
||||
end
|
||||
|
||||
function module:FromToken(token: number): string
|
||||
return tokensToSrings[token]
|
||||
end
|
||||
|
||||
|
||||
function module:MapAToken(key: string, token: number)
|
||||
tokenMap[token] = key
|
||||
end
|
||||
|
||||
function module:FromAMap(token: number): string
|
||||
return tokenMap[token]
|
||||
end
|
||||
|
||||
function module:TokenClear(key: string | number)
|
||||
if typeof(key) == "string" then
|
||||
local token = stringsToTokens[key]
|
||||
stringsToTokens[key] = nil
|
||||
tokensToSrings[token] = nil
|
||||
elseif typeof(key) == "number" then
|
||||
local str = tokensToSrings[key]
|
||||
stringsToTokens[str] = nil
|
||||
tokensToSrings[key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function module.Tokens.new()
|
||||
local tokenCount = 0
|
||||
local stringsToTokens = {}
|
||||
local tokensToSrings = {}
|
||||
|
||||
return {
|
||||
ToToken = function(self: {}, key: string): number
|
||||
if stringsToTokens[key] then return stringsToTokens[key] end
|
||||
|
||||
tokenCount += 1
|
||||
|
||||
stringsToTokens[key] = tokenCount
|
||||
tokensToSrings[tokenCount] = key
|
||||
return tokenCount
|
||||
end,
|
||||
|
||||
ToTokenPath = function(self: {}, keys: {string}): { number }
|
||||
local tokens = {}
|
||||
|
||||
for _, key in keys do
|
||||
table.insert(tokens, self:ToToken(key))
|
||||
end
|
||||
|
||||
return tokens
|
||||
end,
|
||||
|
||||
Is = function(self: {}, key: string): boolean
|
||||
return stringsToTokens[key] ~= nil
|
||||
end,
|
||||
|
||||
From = function(self: {}, token: number): string
|
||||
return tokensToSrings[token]
|
||||
end,
|
||||
|
||||
FromPath = function(self: {}, tokens: { number }): { string }
|
||||
local strings = {}
|
||||
for _, token in tokens do
|
||||
table.insert(strings, tokensToSrings[token])
|
||||
end
|
||||
return strings
|
||||
end,
|
||||
|
||||
Map = function(self: {}, stringsToTokens: { [string]: number })
|
||||
for key, value in stringsToTokens do
|
||||
if typeof(value) == "table" then
|
||||
self:Map(value)
|
||||
continue
|
||||
end
|
||||
stringsToTokens[key] = value
|
||||
tokensToSrings[value] = key
|
||||
end
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
local stack = {}
|
||||
function module.Stack:Push(entry: any): number
|
||||
table.insert(stack, entry)
|
||||
return #stack
|
||||
end
|
||||
|
||||
function module.Stack:Top(): any
|
||||
return stack[#stack]
|
||||
end
|
||||
|
||||
function module.Stack:Pop(index: number?)
|
||||
stack[index or #stack] = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
return module
|
41
src/Chemical/Classes/Object.lua
Normal file
41
src/Chemical/Classes/Object.lua
Normal file
|
@ -0,0 +1,41 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Types = require(RootFolder.Types)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local module = {}
|
||||
|
||||
type Object = Types.HasEntity & {
|
||||
use: (self: Object, ...{}) -> (),
|
||||
}
|
||||
|
||||
function module.new(metamethods: {}?): Object
|
||||
local inherits = {}
|
||||
metamethods = metamethods or {}
|
||||
|
||||
metamethods.__index = function(self, index)
|
||||
local has = rawget(self, index)
|
||||
if has then return has
|
||||
else if inherits[index] then return inherits[index] end
|
||||
end
|
||||
end
|
||||
|
||||
local object = setmetatable({
|
||||
entity = ECS.World:entity()
|
||||
}, metamethods)
|
||||
|
||||
object.use = function(self, ...: {})
|
||||
local classes = { ... }
|
||||
for _, class in classes do
|
||||
for key, value in class do
|
||||
inherits[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
object.use = nil
|
||||
end
|
||||
|
||||
return object
|
||||
end
|
||||
|
||||
return module
|
3
src/Chemical/Classes/init.meta.json
Normal file
3
src/Chemical/Classes/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
109
src/Chemical/ECS.lua
Normal file
109
src/Chemical/ECS.lua
Normal file
|
@ -0,0 +1,109 @@
|
|||
--!nonstrict
|
||||
local Packages = script.Parent.Packages
|
||||
local ECS = require(Packages.JECS)
|
||||
|
||||
export type Entity<T = nil> = ECS.Entity<T>
|
||||
|
||||
local Tags = {
|
||||
-- State Management Tags
|
||||
IsStatic = ECS.tag(),
|
||||
IsPrimitive = ECS.tag(),
|
||||
IsSettable = ECS.tag(),
|
||||
IsStateful = ECS.tag(),
|
||||
IsStatefulTable = ECS.tag(),
|
||||
IsStatefulDictionary = ECS.tag(),
|
||||
IsComputed = ECS.tag(),
|
||||
IsEffect = ECS.tag(),
|
||||
IsDirty = ECS.tag(),
|
||||
IsDeepComparable = ECS.tag(),
|
||||
|
||||
-- Relationship Tags
|
||||
SubscribesTo = ECS.tag(),
|
||||
HasSubscriber = ECS.tag(),
|
||||
InScope = ECS.tag(),
|
||||
ChildOf = ECS.ChildOf,
|
||||
|
||||
-- UI-specific Tags TODO
|
||||
IsHost = ECS.tag(),
|
||||
ManagedBy = ECS.tag(),
|
||||
UIParent = ECS.tag(),
|
||||
}
|
||||
|
||||
local World = ECS.world()
|
||||
|
||||
|
||||
local Components = {
|
||||
Name = ECS.Name,
|
||||
Object = World:component(),
|
||||
Value = World:component(),
|
||||
PrevValue = World:component(),
|
||||
Callback = World:component(),
|
||||
CallbackList = World:component(),
|
||||
OnChangeCallbacks = World:component(),
|
||||
OnKVChangeCallbacks = World:component(),
|
||||
Connection = World:component() :: ECS.Entity<RBXScriptConnection>,
|
||||
ConnectionList = World:component() :: ECS.Entity<{RBXScriptConnection}>,
|
||||
Instance = World:component(),
|
||||
ManagedItems = World:component(),
|
||||
LoopType = World:component(),
|
||||
|
||||
|
||||
ComputeFn = World:component(),
|
||||
EffectFn = World:component(),
|
||||
CleanupFn = World:component(),
|
||||
}
|
||||
|
||||
World:set(Components.Connection, ECS.OnRemove, function(entity)
|
||||
local connection = World:get(entity, Components.Connection)
|
||||
if connection then
|
||||
connection:Disconnect()
|
||||
end
|
||||
end)
|
||||
|
||||
World:set(Components.ConnectionList, ECS.OnRemove, function(entity)
|
||||
local connections = World:get(entity, Components.ConnectionList)
|
||||
if connections then
|
||||
for _, conn in ipairs(connections) do
|
||||
conn:Disconnect()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
World:set(Components.Instance, ECS.OnRemove, function(entity)
|
||||
if World:has(entity, Tags.IsHost) then
|
||||
for effectEntity in World:each(ECS.pair(Tags.InScope, entity)) do
|
||||
World:delete(effectEntity)
|
||||
end
|
||||
if World:has(entity, Components.ConnectionList) then
|
||||
World:remove(entity, Components.ConnectionList)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
World:set(Components.Value, ECS.OnChange, function(entity, id, data)
|
||||
if World:has(entity, Tags.IsSettable) then
|
||||
World:add(entity, Tags.IsDirty)
|
||||
end
|
||||
end)
|
||||
|
||||
World:set(Components.Object, ECS.OnRemove, function(entity: ECS.Entity<any>, id: ECS.Id<any>)
|
||||
local object = World:get(entity, Components.Object)
|
||||
if object and object.__internalDestroy then
|
||||
object:__internalDestroy()
|
||||
end
|
||||
end)
|
||||
|
||||
World:add(Tags.SubscribesTo, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
|
||||
World:add(Tags.HasSubscriber, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
|
||||
World:add(Tags.InScope, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
|
||||
World:add(Tags.ManagedBy, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
|
||||
World:add(Tags.UIParent, ECS.pair(ECS.OnDeleteTarget, ECS.Delete))
|
||||
|
||||
local module = {
|
||||
Components = Components,
|
||||
Tags = Tags,
|
||||
JECS = ECS,
|
||||
World = World,
|
||||
}
|
||||
|
||||
return module
|
46
src/Chemical/Factories/Computed.lua
Normal file
46
src/Chemical/Factories/Computed.lua
Normal file
|
@ -0,0 +1,46 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
|
||||
local Object = require(RootFolder.Classes.Object)
|
||||
|
||||
local Stateful = require(RootFolder.Mixins.Stateful)
|
||||
local Destroyable = require(RootFolder.Mixins.Destroyable)
|
||||
local Cleanable = require(RootFolder.Mixins.Cleanable)
|
||||
local Computable = require(RootFolder.Mixins.Computable)
|
||||
|
||||
|
||||
export type Computed<T> = Stateful.Stateful<T> & Computable.Computable<T> & Destroyable.Destroyable<T> & Cleanable.Cleanable<T>
|
||||
|
||||
return function<T>(computeFn: () -> T, cleanupFn: (T) -> ()?): Computed<T>
|
||||
local obj = Object.new({
|
||||
__tostring = function(self)
|
||||
local rawValue = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
return `Computed<{tostring(rawValue)}>`
|
||||
end,
|
||||
})
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsComputed)
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.ComputeFn, computeFn)
|
||||
if cleanupFn then ECS.World:set(obj.entity, ECS.Components.CleanupFn, cleanupFn) end
|
||||
|
||||
|
||||
obj:use(
|
||||
Computable,
|
||||
Stateful,
|
||||
Destroyable,
|
||||
Cleanable
|
||||
)
|
||||
|
||||
|
||||
obj:compute()
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
|
||||
return obj
|
||||
end
|
43
src/Chemical/Factories/Effect.lua
Normal file
43
src/Chemical/Factories/Effect.lua
Normal file
|
@ -0,0 +1,43 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Classes = RootFolder.Classes
|
||||
local Mixins = RootFolder.Mixins
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Object = require(Classes.Object)
|
||||
|
||||
local Effectable = require(Mixins.Effectable)
|
||||
local Destroyable = require(Mixins.Destroyable)
|
||||
local Cleanable = require(Mixins.Cleanable)
|
||||
|
||||
export type Effect = Effectable.Effectable & Destroyable.Destroyable & Cleanable.Cleanable
|
||||
type CleanUp = () -> ()
|
||||
|
||||
--- Effect
|
||||
-- Effects will fire after the batch of stateful object changes are propogated.
|
||||
-- The optional cleanup function will fire first, and then the effect's function.
|
||||
-- The effect function can optionally return a cleanup function.
|
||||
-- Effects will be deleted when any one of its dependent objects are destroyed.
|
||||
return function(effectFn: () -> ( CleanUp | nil )): Effect
|
||||
local obj = Object.new()
|
||||
|
||||
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsEffect)
|
||||
ECS.World:set(obj.entity, ECS.Components.EffectFn, effectFn)
|
||||
|
||||
|
||||
obj:use(
|
||||
Cleanable,
|
||||
Destroyable,
|
||||
Effectable
|
||||
)
|
||||
|
||||
|
||||
obj:run()
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
|
||||
return obj
|
||||
end
|
44
src/Chemical/Factories/Map.lua
Normal file
44
src/Chemical/Factories/Map.lua
Normal file
|
@ -0,0 +1,44 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Classes = RootFolder.Classes
|
||||
local Mixins = RootFolder.Mixins
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local Object = require(Classes.Object)
|
||||
|
||||
local Stateful = require(Mixins.Stateful)
|
||||
local StatefulTable = require(Mixins.StatefulTable)
|
||||
local StatefulDictionary = require(Mixins.StatefulDictionary)
|
||||
|
||||
local Settable = require(Mixins.Settable)
|
||||
local Numerical = require(Mixins.Numerical)
|
||||
local Destroyable = require(Mixins.Destroyable)
|
||||
|
||||
export type Value<T> = Stateful.Stateful<T> & Settable.Settable<T> & StatefulDictionary.StatefulDictionary<T> & Destroyable.Destroyable
|
||||
|
||||
return function<T>(value: T): Value<T>
|
||||
local obj = Object.new({
|
||||
__tostring = function(self)
|
||||
return `Map<{self.entity}>`
|
||||
end,
|
||||
})
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStatefulDictionary)
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsSettable)
|
||||
|
||||
|
||||
obj:use(
|
||||
Stateful,
|
||||
Settable,
|
||||
StatefulDictionary,
|
||||
Destroyable
|
||||
)
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Value, value)
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
|
||||
return obj
|
||||
end
|
70
src/Chemical/Factories/Observer.lua
Normal file
70
src/Chemical/Factories/Observer.lua
Normal file
|
@ -0,0 +1,70 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
|
||||
local LinkedList = require(RootFolder.Packages.LinkedList)
|
||||
|
||||
|
||||
local Classes = RootFolder.Classes
|
||||
local Mixins = RootFolder.Mixins
|
||||
local Functions = RootFolder.Functions
|
||||
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Is = require(Functions.Is)
|
||||
|
||||
|
||||
local Object = require(Classes.Object)
|
||||
local Stateful = require(Mixins.Stateful)
|
||||
local Observable = require(Mixins.Observable)
|
||||
local Destroyable = require(Mixins.Destroyable)
|
||||
|
||||
|
||||
export type Observer<T> = Observable.Observable<T> & Destroyable.Destroyable
|
||||
export type ObserverTable<T> = Observable.ObservableTable<T> & Destroyable.Destroyable
|
||||
|
||||
export type ObserverFactory = (<T>(sourceObject: Stateful.Stateful<{T}>) -> ObserverTable<T>)
|
||||
& (<T>(sourceObject: Stateful.Stateful<T>) -> Observer<T>)
|
||||
|
||||
|
||||
--- Observer
|
||||
-- Creates an observer that reacts to changes in a stateful source.
|
||||
-- If the subject's value is a table, upon first creation of Observer, onKVChange callbacks will be supported.
|
||||
-- @param sourceObject The stateful object to observe.
|
||||
-- @return A new observer object.
|
||||
local function createObserver<T>(sourceObject: Stateful.Stateful<T>)
|
||||
if not Is.Stateful(sourceObject) then
|
||||
error("The first argument of an Observer must be a stateful object.", 2)
|
||||
end
|
||||
|
||||
local obj = Object.new()
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsEffect)
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.OnChangeCallbacks, LinkedList.new())
|
||||
|
||||
|
||||
if typeof(sourceObject:get()) == "table" then
|
||||
ECS.World:add(sourceObject.entity, ECS.Tags.IsDeepComparable)
|
||||
ECS.World:set(obj.entity, ECS.Components.OnKVChangeCallbacks, LinkedList.new())
|
||||
end
|
||||
|
||||
|
||||
ECS.World:add(obj.entity, ECS.JECS.pair(ECS.Tags.SubscribesTo, sourceObject.entity))
|
||||
ECS.World:add(sourceObject.entity, ECS.JECS.pair(ECS.Tags.HasSubscriber, obj.entity))
|
||||
|
||||
|
||||
obj:use(
|
||||
Destroyable,
|
||||
Observable
|
||||
)
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
|
||||
return obj
|
||||
end
|
||||
|
||||
return (createObserver :: any) :: ObserverFactory
|
53
src/Chemical/Factories/Reaction.lua
Normal file
53
src/Chemical/Factories/Reaction.lua
Normal file
|
@ -0,0 +1,53 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
|
||||
local LinkedList = require(RootFolder.Packages.LinkedList)
|
||||
|
||||
|
||||
local Classes = RootFolder.Classes
|
||||
local Mixins = RootFolder.Mixins
|
||||
local Functions = RootFolder.Functions
|
||||
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Is = require(Functions.Is)
|
||||
local SetInScope = require(Functions.SetInScope)
|
||||
|
||||
|
||||
local Object = require(Classes.Object)
|
||||
local Stateful = require(Mixins.Stateful)
|
||||
local Destroyable = require(Mixins.Destroyable)
|
||||
local Serializable = require(Mixins.Serializable)
|
||||
|
||||
export type Reaction<T> = Stateful.Stateful<T> & Destroyable.Destroyable & Serializable.Serializable & T
|
||||
|
||||
--- Reaction
|
||||
-- A Stateful container with helper methods for converting data into different formats.
|
||||
local function createReaction<T>(name: string, key: string, container: T): Reaction<T>
|
||||
|
||||
local obj = Object.new({
|
||||
__tostring = function(self)
|
||||
local isAlive = Is.Dead(self) and "Dead" or "Alive"
|
||||
return `Reaction<{name}/{key}> - {isAlive}`
|
||||
end,
|
||||
})
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
|
||||
|
||||
SetInScope(container :: any, obj.entity)
|
||||
|
||||
obj:use(
|
||||
Stateful,
|
||||
Destroyable,
|
||||
Serializable,
|
||||
(container :: any)
|
||||
)
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Value, container)
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
return obj :: Reaction<T>
|
||||
end
|
||||
|
||||
return createReaction
|
48
src/Chemical/Factories/Table.lua
Normal file
48
src/Chemical/Factories/Table.lua
Normal file
|
@ -0,0 +1,48 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Classes = RootFolder.Classes
|
||||
local Mixins = RootFolder.Mixins
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local Object = require(Classes.Object)
|
||||
|
||||
local Stateful = require(Mixins.Stateful)
|
||||
local StatefulTable = require(Mixins.StatefulTable)
|
||||
local StatefulDictionary = require(Mixins.StatefulDictionary)
|
||||
|
||||
local Settable = require(Mixins.Settable)
|
||||
local Numerical = require(Mixins.Numerical)
|
||||
local Destroyable = require(Mixins.Destroyable)
|
||||
|
||||
export type Value<T> = Stateful.Stateful<T> & Settable.Settable<T> & StatefulTable.StatefulTable<T> & Destroyable.Destroyable
|
||||
|
||||
return function<T>(value: T): Value<T>
|
||||
local obj = Object.new({
|
||||
__len = function(self)
|
||||
return #ECS.World:get(self.entity, ECS.Components.Value)
|
||||
end,
|
||||
|
||||
__tostring = function(self)
|
||||
return `Table<{ tostring(self.entity) }>`
|
||||
end,
|
||||
})
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStatefulTable)
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsSettable)
|
||||
|
||||
|
||||
obj:use(
|
||||
Stateful,
|
||||
Settable,
|
||||
StatefulTable,
|
||||
Destroyable
|
||||
)
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Value, value)
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
|
||||
return obj
|
||||
end
|
65
src/Chemical/Factories/Value.lua
Normal file
65
src/Chemical/Factories/Value.lua
Normal file
|
@ -0,0 +1,65 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Classes = RootFolder.Classes
|
||||
local Mixins = RootFolder.Mixins
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local Object = require(Classes.Object)
|
||||
|
||||
local Stateful = require(Mixins.Stateful)
|
||||
local StatefulTable = require(Mixins.StatefulTable)
|
||||
local StatefulDictionary = require(Mixins.StatefulDictionary)
|
||||
|
||||
local Settable = require(Mixins.Settable)
|
||||
local Numerical = require(Mixins.Numerical)
|
||||
local Destroyable = require(Mixins.Destroyable)
|
||||
|
||||
type Value<T> = Stateful.Stateful<T> & Settable.Settable<T> & Destroyable.Destroyable
|
||||
|
||||
export type ValueFactory = (
|
||||
((value: number) -> Value<number> & Numerical.Numerical) &
|
||||
(<T>(value: T) -> Value<T>)
|
||||
)
|
||||
|
||||
--- Value
|
||||
-- Stateful value container which enables reactivity.
|
||||
-- Depending on the type of the initial value, certain methods are exposed correlating to the value type.
|
||||
-- @param value any -- The initial value to set.
|
||||
-- @return The Value object.
|
||||
return function<T>(value: T): Value<T>
|
||||
local obj = Object.new({
|
||||
__len = typeof(value) == "table" and function(self)
|
||||
return #ECS.World:get(self.entity, ECS.Components.Value)
|
||||
end or nil,
|
||||
|
||||
__tostring = function(self)
|
||||
local rawValue = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
return `Value<{tostring(rawValue)}>`
|
||||
end,
|
||||
})
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsStateful)
|
||||
ECS.World:add(obj.entity, ECS.Tags.IsSettable)
|
||||
|
||||
|
||||
local mtMethods: { {} } = {
|
||||
Stateful,
|
||||
Settable,
|
||||
Destroyable,
|
||||
}
|
||||
|
||||
|
||||
if typeof(value) == "number" then
|
||||
table.insert(mtMethods, Numerical)
|
||||
end
|
||||
|
||||
|
||||
obj:use(table.unpack(mtMethods))
|
||||
|
||||
|
||||
ECS.World:set(obj.entity, ECS.Components.Value, value)
|
||||
ECS.World:set(obj.entity, ECS.Components.Object, obj)
|
||||
|
||||
|
||||
return obj
|
||||
end
|
47
src/Chemical/Factories/Watch.lua
Normal file
47
src/Chemical/Factories/Watch.lua
Normal file
|
@ -0,0 +1,47 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
local Observer = require(RootFolder.Factories.Observer)
|
||||
|
||||
local Stateful = require(RootFolder.Mixins.Stateful)
|
||||
local Computable = require(RootFolder.Mixins.Computable)
|
||||
|
||||
|
||||
export type WatchHandle = {
|
||||
destroy: (self: WatchHandle) -> ()
|
||||
}
|
||||
|
||||
|
||||
type Watchable<T> = Stateful.Stateful<T> | Computable.Computable<T>
|
||||
|
||||
--- Creates a watcher that runs a callback function whenever a reactive source changes.
|
||||
-- @param source The Value or Computed object to watch.
|
||||
-- @param watchCallback A function that will be called with (newValue, oldValue).
|
||||
-- @returns A handle with a :destroy() method to stop watching.
|
||||
return function<T>(source: Watchable<T>, watchCallback: (new: T, old: T) -> ()): WatchHandle
|
||||
if not source or not source.entity then
|
||||
error("Chemical.Watch requires a valid Value or Computed object as its first argument.", 2)
|
||||
end
|
||||
|
||||
if typeof(watchCallback) ~= "function" then
|
||||
error("Chemical.Watch requires a function as its second argument.", 2)
|
||||
end
|
||||
|
||||
local obs = Observer(source)
|
||||
|
||||
obs:onChange(function(newValue, oldValue)
|
||||
local success, err = pcall(watchCallback, newValue, oldValue)
|
||||
if not success then
|
||||
warn("Chemical Watch Error: ", err)
|
||||
end
|
||||
end)
|
||||
|
||||
local handle: WatchHandle = {
|
||||
destroy = function()
|
||||
obs:destroy()
|
||||
end,
|
||||
}
|
||||
|
||||
return handle
|
||||
end
|
3
src/Chemical/Factories/init.meta.json
Normal file
3
src/Chemical/Factories/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
8
src/Chemical/Functions/Alive.lua
Normal file
8
src/Chemical/Functions/Alive.lua
Normal file
|
@ -0,0 +1,8 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
return function(obj: Types.HasEntity): boolean
|
||||
return ECS.World:contains(obj.entity)
|
||||
end
|
80
src/Chemical/Functions/Array.lua
Normal file
80
src/Chemical/Functions/Array.lua
Normal file
|
@ -0,0 +1,80 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
|
||||
|
||||
|
||||
|
||||
local module = {}
|
||||
|
||||
function module.Transform<K, V, R>(tbl: { [K]: V }, doFn: (k: K, v: V) -> R): { [K]: R }
|
||||
local newTbl = {}
|
||||
for key, value in tbl do
|
||||
if Is.Array(value) then
|
||||
newTbl[key] = module.Transform(value, doFn)
|
||||
else
|
||||
newTbl[key] = doFn(key, value)
|
||||
end
|
||||
end
|
||||
return newTbl
|
||||
end
|
||||
|
||||
function module.ShallowTransform<K, V, R>(tbl: { [K]: V }, doFn: (k: K, v: V) -> R): { [K]: R }
|
||||
local newTbl = {}
|
||||
for key, value in tbl do
|
||||
newTbl[key] = doFn(key, value)
|
||||
end
|
||||
return newTbl
|
||||
end
|
||||
|
||||
|
||||
function module.Traverse(tbl: {}, doFn: (k: any, v: any) -> ())
|
||||
for key, value in tbl do
|
||||
if Is.Array(value) then
|
||||
module.Traverse(value, doFn)
|
||||
else
|
||||
doFn(key, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Recursively walks a table, calling a visitor function for every
|
||||
value encountered. The visitor receives the path (an array of keys)
|
||||
and the value at that path.
|
||||
|
||||
@param target The table to walk.
|
||||
@param visitor The function to call, with signature: (path: {any}, value: any) -> ()
|
||||
--]]
|
||||
function module.Walk(target: {any}, visitor: (path: {any}, value: any) -> ())
|
||||
local function _walk(currentValue: any, currentPath: {any})
|
||||
visitor(currentPath, currentValue)
|
||||
|
||||
if Is.Array(currentValue) then
|
||||
for key, childValue in pairs(currentValue) do
|
||||
local childPath = table.clone(currentPath)
|
||||
table.insert(childPath, key)
|
||||
_walk(childValue, childPath)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
_walk(target, {})
|
||||
end
|
||||
|
||||
|
||||
|
||||
function module.FindOnPath<K, V>(tbl: {[K]: V}, path: { number | string }): V
|
||||
local current = tbl
|
||||
for _, key in path do
|
||||
current = current[key]
|
||||
end
|
||||
|
||||
return current
|
||||
end
|
||||
|
||||
return module
|
78
src/Chemical/Functions/Blueprint.lua
Normal file
78
src/Chemical/Functions/Blueprint.lua
Normal file
|
@ -0,0 +1,78 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
local Value = require(RootFolder.Factories.Value)
|
||||
local Table = require(RootFolder.Factories.Table)
|
||||
local Map = require(RootFolder.Factories.Map)
|
||||
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
local Peek = require(RootFolder.Functions.Peek)
|
||||
local Array = require(RootFolder.Functions.Array)
|
||||
|
||||
type Blueprint = { T: ECS.Entity, V: any }
|
||||
|
||||
local module = {}
|
||||
|
||||
|
||||
local function blueprintTreeFromValue(value: any): Blueprint
|
||||
if Is.Stateful(value) then
|
||||
if Is.StatefulTable(value) then
|
||||
return { T = ECS.Tags.IsStatefulTable, V = Peek(value) }
|
||||
elseif Is.StatefulDictionary(value) then
|
||||
return { T = ECS.Tags.IsStatefulDictionary, V = Peek(value) }
|
||||
else
|
||||
return { T = ECS.Tags.IsStateful, V = Peek(value) }
|
||||
end
|
||||
elseif typeof(value) == "table" then
|
||||
local childrenAsBlueprints = Array.ShallowTransform(value, function(k, v)
|
||||
return blueprintTreeFromValue(v)
|
||||
end)
|
||||
return { T = ECS.Tags.IsStatic, V = childrenAsBlueprints }
|
||||
else
|
||||
return { T = ECS.Tags.IsStatic, V = value }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function module:From(value: any): Blueprint
|
||||
return blueprintTreeFromValue(value)
|
||||
end
|
||||
|
||||
|
||||
local buildFromBlueprintTree
|
||||
|
||||
|
||||
local function buildFromABlueprint(blueprint: Blueprint)
|
||||
if blueprint.T == ECS.Tags.IsStateful then
|
||||
return Value(blueprint.V)
|
||||
elseif blueprint.T == ECS.Tags.IsStatefulTable then
|
||||
return Table(blueprint.V)
|
||||
elseif blueprint.T == ECS.Tags.IsStatefulDictionary then
|
||||
return Map(blueprint.V)
|
||||
elseif blueprint.T == ECS.Tags.IsStatic then
|
||||
if typeof(blueprint.V) == "table" then
|
||||
return buildFromBlueprintTree(blueprint.V)
|
||||
else
|
||||
return blueprint.V
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
buildFromBlueprintTree = function(blueprintTable: {any})
|
||||
return Array.ShallowTransform(blueprintTable, function(key, value)
|
||||
return buildFromABlueprint(value)
|
||||
end)
|
||||
end
|
||||
|
||||
function module:Read(rootBlueprint: Blueprint)
|
||||
return buildFromABlueprint(rootBlueprint)
|
||||
end
|
||||
|
||||
|
||||
return module
|
57
src/Chemical/Functions/Compare.lua
Normal file
57
src/Chemical/Functions/Compare.lua
Normal file
|
@ -0,0 +1,57 @@
|
|||
export type Change = {
|
||||
Path: {string | number},
|
||||
OldValue: any,
|
||||
NewValue: any,
|
||||
}
|
||||
|
||||
local DeepCompare = {}
|
||||
|
||||
local function compare(oldTable, newTable, path, changes)
|
||||
path = path or {}
|
||||
changes = changes or {}
|
||||
|
||||
for key, newValue in pairs(newTable) do
|
||||
local oldValue = oldTable and oldTable[key]
|
||||
local currentPath = table.clone(path)
|
||||
table.insert(currentPath, key)
|
||||
|
||||
if oldValue ~= newValue then
|
||||
if typeof(newValue) == "table" and typeof(oldValue) == "table" then
|
||||
compare(oldValue, newValue, currentPath, changes)
|
||||
else
|
||||
table.insert(changes, {
|
||||
Path = currentPath,
|
||||
OldValue = oldValue,
|
||||
NewValue = newValue,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if oldTable then
|
||||
for key, oldValue in pairs(oldTable) do
|
||||
if newTable[key] == nil then
|
||||
local currentPath = table.clone(path)
|
||||
table.insert(currentPath, key)
|
||||
|
||||
table.insert(changes, {
|
||||
Path = currentPath,
|
||||
OldValue = oldValue,
|
||||
NewValue = nil,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return changes
|
||||
end
|
||||
|
||||
--- Compares two tables deeply and returns an array of changes.
|
||||
-- Each change object contains a `Path`, `OldValue`, and `NewValue`.
|
||||
return function(oldTable: {}, newTable: {}): {Change}
|
||||
if typeof(oldTable) ~= "table" or typeof(newTable) ~= "table" then
|
||||
return {}
|
||||
end
|
||||
|
||||
return compare(oldTable, newTable)
|
||||
end
|
147
src/Chemical/Functions/Compose.lua
Normal file
147
src/Chemical/Functions/Compose.lua
Normal file
|
@ -0,0 +1,147 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Symbols = require(RootFolder.Symbols)
|
||||
|
||||
local Effect = require(RootFolder.Factories.Effect)
|
||||
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
local Has = require(RootFolder.Functions.Has)
|
||||
local GetInScope = require(RootFolder.Functions.GetInScope)
|
||||
|
||||
local JECS = ECS.JECS
|
||||
local World = ECS.World
|
||||
local Components = ECS.Components
|
||||
local Tags = ECS.Tags
|
||||
|
||||
local RESERVED_KEYS = { "Children", "Parent" }
|
||||
|
||||
local INSTANCE_TO_ENTITY = setmetatable({}, { __mode = "k" })
|
||||
|
||||
local function applyProperty(instance, prop, value)
|
||||
instance[prop] = value
|
||||
end
|
||||
|
||||
local function bindEvent(instance: Instance, instanceEntity: ECS.Entity, event, callback)
|
||||
local connection = instance[event]:Connect(callback)
|
||||
local connectionList = World:get(instanceEntity, Components.ConnectionList)
|
||||
table.insert(connectionList, connection)
|
||||
World:set(instanceEntity, Components.ConnectionList, connectionList)
|
||||
end
|
||||
|
||||
local function bindChange(instance: Instance, instanceEntity: ECS.Entity, prop, action)
|
||||
local connection
|
||||
|
||||
if Is.Settable(action) then
|
||||
connection = instance:GetPropertyChangedSignal(prop):Connect(function(...: any) action:set(instance[prop]) end)
|
||||
else
|
||||
connection = instance:GetPropertyChangedSignal(prop):Connect(action)
|
||||
end
|
||||
|
||||
local connectionList = World:get(instanceEntity, Components.ConnectionList)
|
||||
table.insert(connectionList, connection)
|
||||
World:set(instanceEntity, Components.ConnectionList, connectionList)
|
||||
end
|
||||
|
||||
local function bindReactive(instance: Instance, instanceEntity: ECS.Entity, prop, value): Effect.Effect
|
||||
local propType = typeof(instance[prop])
|
||||
local propIsString = propType == "string"
|
||||
|
||||
local propEffect = Effect(function()
|
||||
local currentValue = value:get()
|
||||
|
||||
if propIsString and typeof(currentValue) ~= "string" then
|
||||
instance[prop] = tostring(currentValue)
|
||||
else
|
||||
instance[prop] = currentValue
|
||||
end
|
||||
end)
|
||||
|
||||
applyProperty(instance, prop, value:get())
|
||||
|
||||
World:add(propEffect.entity, JECS.pair(Tags.InScope, instanceEntity))
|
||||
end
|
||||
|
||||
local function applyVirtualNode(instance: Instance, instanceEntity: ECS.Entity, properties: {})
|
||||
for key, value in properties do
|
||||
if table.find(RESERVED_KEYS, key) then continue end
|
||||
|
||||
if Is.Symbol(key) then
|
||||
if Has.Symbol("Event", key) then
|
||||
if Is.Stateful(value) then error("Chemical OnEvent Error: Chemical does not currently support Stateful values.") end
|
||||
if typeof(value) ~= "function" then error("Chemical OnEvent Error: can only be bound to a callback", 2) end
|
||||
|
||||
|
||||
bindEvent(instance, instanceEntity, key.Symbol, value)
|
||||
elseif Has.Symbol("Change", key) then
|
||||
if typeof(value) ~= "function"
|
||||
and not Is.Settable(value) then error("Chemical OnChange Error: can only be bound to a callback or settable Stateful object.", 2) end
|
||||
|
||||
|
||||
bindChange(instance, instanceEntity, key.Symbol, value)
|
||||
elseif Has.Symbol("Children", key) then
|
||||
for _, child in value do
|
||||
child.Parent = instance
|
||||
end
|
||||
end
|
||||
elseif Is.Stateful(value) then
|
||||
bindReactive(instance, instanceEntity, key, value)
|
||||
elseif Is.Literal(value) then
|
||||
applyProperty(instance, key, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function Compose(target: string | Instance)
|
||||
return function(properties: {})
|
||||
local instance: Instance
|
||||
local instanceEntity: ECS.Entity
|
||||
|
||||
if typeof(target) == "string" then
|
||||
instance = Instance.new(target)
|
||||
instanceEntity = World:entity()
|
||||
|
||||
World:add(instanceEntity, Tags.IsHost)
|
||||
World:set(instanceEntity, Components.Instance, instance)
|
||||
World:set(instanceEntity, Components.ConnectionList, {})
|
||||
|
||||
World:set(instanceEntity, Components.Connection, instance.Destroying:Once(function()
|
||||
INSTANCE_TO_ENTITY[instance] = nil
|
||||
|
||||
if World:contains(instanceEntity) then
|
||||
World:delete(instanceEntity)
|
||||
end
|
||||
end))
|
||||
else
|
||||
instance = target
|
||||
instanceEntity = INSTANCE_TO_ENTITY[instance]
|
||||
|
||||
if not instanceEntity or not World:contains(instanceEntity) then
|
||||
instanceEntity = World:entity()
|
||||
INSTANCE_TO_ENTITY[instance] = instanceEntity
|
||||
|
||||
World:add(instanceEntity, Tags.IsHost)
|
||||
World:set(instanceEntity, Components.Instance, instance)
|
||||
World:set(instanceEntity, Components.ConnectionList, {})
|
||||
|
||||
World:set(instanceEntity, Components.Connection, instance.Destroying:Once(function()
|
||||
INSTANCE_TO_ENTITY[instance] = nil
|
||||
|
||||
if World:contains(instanceEntity) then
|
||||
World:delete(instanceEntity)
|
||||
end
|
||||
end))
|
||||
end
|
||||
end
|
||||
|
||||
applyVirtualNode(instance, instanceEntity, properties)
|
||||
|
||||
if properties.Parent and not instance.Parent then
|
||||
instance.Parent = properties.Parent
|
||||
end
|
||||
|
||||
return instance
|
||||
end
|
||||
end
|
||||
|
||||
return Compose
|
38
src/Chemical/Functions/Destroy.lua
Normal file
38
src/Chemical/Functions/Destroy.lua
Normal file
|
@ -0,0 +1,38 @@
|
|||
export type Destroyable = Computed | Value | Observer | { destroy: (self: {}) -> () } | Instance | RBXScriptConnection | { Destroyable } | () -> () | thread
|
||||
|
||||
local function Destroy(subject: Destroyable )
|
||||
if typeof(subject) == "table" then
|
||||
if subject.destroy then
|
||||
subject:destroy()
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if subject.Destroy then
|
||||
subject:Destroy()
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if getmetatable(subject) then
|
||||
setmetatable(subject, nil)
|
||||
table.clear(subject)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
for _, value in subject do
|
||||
Destroy(value)
|
||||
end
|
||||
elseif typeof(subject) == "Instance" then
|
||||
subject:Destroy()
|
||||
elseif typeof(subject) == "RBXScriptConnection" then
|
||||
subject:Disconnect()
|
||||
elseif typeof(subject) == "function" then
|
||||
subject()
|
||||
elseif typeof(subject) == "thread" then
|
||||
task.cancel(subject)
|
||||
end
|
||||
end
|
||||
|
||||
return Destroy
|
13
src/Chemical/Functions/GetInScope.lua
Normal file
13
src/Chemical/Functions/GetInScope.lua
Normal file
|
@ -0,0 +1,13 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local function getInScope(entity: ECS.Entity)
|
||||
local scoped = {}
|
||||
for scopedEntity in ECS.World:each(ECS.JECS.pair(ECS.Tags.InScope, entity)) do
|
||||
table.insert(scoped, scopedEntity)
|
||||
end
|
||||
return scoped
|
||||
end
|
||||
|
||||
return getInScope
|
20
src/Chemical/Functions/GetSubscribers.lua
Normal file
20
src/Chemical/Functions/GetSubscribers.lua
Normal file
|
@ -0,0 +1,20 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
|
||||
|
||||
local function getSubscribers(entity: ECS.Entity)
|
||||
local subscribers = {}
|
||||
|
||||
local subscriberQuery = ECS.World:query(ECS.Components.Object)
|
||||
:with(ECS.JECS.pair(ECS.Tags.SubscribesTo, entity))
|
||||
|
||||
for subscriberEntity, _ in subscriberQuery:iter() do
|
||||
table.insert(subscribers, subscriberEntity)
|
||||
end
|
||||
|
||||
return subscribers
|
||||
end
|
||||
|
||||
return getSubscribers
|
14
src/Chemical/Functions/GetSymbol.lua
Normal file
14
src/Chemical/Functions/GetSymbol.lua
Normal file
|
@ -0,0 +1,14 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local symbolMap = {}
|
||||
|
||||
export type Symbol<S, T> = { Symbol: S, Type: T }
|
||||
|
||||
return function<S, T>(symbolName: S, symbolType: T): Symbol<S, T>
|
||||
if symbolMap[symbolName] then return symbolMap[symbolName] end
|
||||
|
||||
local symbol = { Symbol = symbolName, Type = symbolType }
|
||||
symbolMap[symbolName] = symbol
|
||||
|
||||
return symbol
|
||||
end
|
12
src/Chemical/Functions/Has.lua
Normal file
12
src/Chemical/Functions/Has.lua
Normal file
|
@ -0,0 +1,12 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local module = {}
|
||||
|
||||
function module.Symbol(typeOf: string, obj: {}): boolean
|
||||
return obj.Type == typeOf
|
||||
end
|
||||
|
||||
|
||||
return module
|
83
src/Chemical/Functions/Is.lua
Normal file
83
src/Chemical/Functions/Is.lua
Normal file
|
@ -0,0 +1,83 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local module = {}
|
||||
|
||||
function module.Stateful(obj: any): boolean
|
||||
if typeof(obj) == "table" and obj.entity then
|
||||
return ECS.World:has(obj.entity, ECS.Tags.IsStateful)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function module.Settable(obj: any): boolean
|
||||
if typeof(obj) == "table" and obj.entity then
|
||||
return ECS.World:has(obj.entity, ECS.Tags.IsSettable)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function module.Primitive(obj: any): boolean
|
||||
local typeA = typeof(obj)
|
||||
return typeA ~= "table" and typeA ~= "userdata"
|
||||
end
|
||||
|
||||
function module.Literal(obj: any): boolean
|
||||
local typeA = typeof(obj)
|
||||
return typeA ~= "table" and typeA ~= "userdata" and typeA ~= "thread" and typeA ~= "function" and typeA ~= "Instance"
|
||||
end
|
||||
|
||||
function module.Symbol(obj: any, typeOf: string?): boolean
|
||||
local is = typeof(obj) == "table" and obj.Type and obj.Symbol
|
||||
return typeOf == nil and is or is and obj.Type == typeOf
|
||||
end
|
||||
|
||||
function module.Array(obj: any): boolean
|
||||
return typeof(obj) == "table" and obj.entity == nil
|
||||
end
|
||||
|
||||
function module.StatefulTable(obj: any): boolean
|
||||
if typeof(obj) == "table" and obj.entity then
|
||||
return ECS.World:has(obj.entity, ECS.Tags.IsStateful) and ECS.World:has(obj.entity, ECS.Tags.IsStatefulTable)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function module.StatefulDictionary(obj: any): boolean
|
||||
if typeof(obj) == "table" and obj.entity then
|
||||
return ECS.World:has(obj.entity, ECS.Tags.IsStateful) and ECS.World:has(obj.entity, ECS.Tags.IsStatefulDictionary)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function module.Blueprint(obj: any): boolean
|
||||
if typeof(obj) == "table" and obj.T and obj.V then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function module.Dead(obj: any)
|
||||
return typeof(obj) == "table" and obj.__destroyed
|
||||
end
|
||||
|
||||
function module.Destroyed(obj: Instance): boolean
|
||||
if obj.Parent == nil then
|
||||
local Success, Error = pcall(function()
|
||||
obj.Parent = UserSettings() :: any
|
||||
end)
|
||||
|
||||
return Error ~= "Not allowed to add that under settings"
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
return module
|
15
src/Chemical/Functions/Peek.lua
Normal file
15
src/Chemical/Functions/Peek.lua
Normal file
|
@ -0,0 +1,15 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
|
||||
--- Peek
|
||||
-- View a stateful's value without triggering and scoped dependencies/subscriptions.
|
||||
return function(obj: any): any?
|
||||
if Is.Stateful(obj) then
|
||||
return ECS.World:get(obj.entity, ECS.Components.Value)
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
21
src/Chemical/Functions/SetInScope.lua
Normal file
21
src/Chemical/Functions/SetInScope.lua
Normal file
|
@ -0,0 +1,21 @@
|
|||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Array = require(RootFolder.Functions.Array)
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
|
||||
local function setInScope(scopable: {ECS.Entity} | ECS.Entity, entity: ECS.Entity)
|
||||
if Is.Stateful(scopable) then
|
||||
ECS.World:add(scopable.entity, ECS.JECS.pair(ECS.Tags.InScope, entity))
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
Array.Traverse(scopable, function(k, v)
|
||||
if Is.Stateful(v) then
|
||||
ECS.World:add(v.entity, ECS.JECS.pair(ECS.Tags.InScope, entity))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return setInScope
|
3
src/Chemical/Functions/init.meta.json
Normal file
3
src/Chemical/Functions/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
19
src/Chemical/Mixins/Cleanable.lua
Normal file
19
src/Chemical/Mixins/Cleanable.lua
Normal file
|
@ -0,0 +1,19 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
export type Cleanable = Types.HasEntity & {
|
||||
clean: (self: Cleanable) -> (),
|
||||
}
|
||||
|
||||
return {
|
||||
clean = function(self: Cleanable)
|
||||
local cleanupFn = ECS.World:get(self.entity, ECS.Components.CleanupFn)
|
||||
if cleanupFn then
|
||||
cleanupFn()
|
||||
end
|
||||
end,
|
||||
}
|
53
src/Chemical/Mixins/Computable.lua
Normal file
53
src/Chemical/Mixins/Computable.lua
Normal file
|
@ -0,0 +1,53 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
local GetSubscribers = require(RootFolder.Functions.GetSubscribers)
|
||||
|
||||
export type Computable<T = any> = Types.HasEntity & {
|
||||
compute: (self: Computable<T>) -> (),
|
||||
}
|
||||
|
||||
type MaybeCleanable = {
|
||||
clean: (self: MaybeCleanable) -> ()
|
||||
}
|
||||
|
||||
return {
|
||||
compute = function(self: Computable & MaybeCleanable)
|
||||
local computeFn = ECS.World:get(self.entity, ECS.Components.ComputeFn)
|
||||
if not computeFn then return end
|
||||
|
||||
|
||||
local oldValue = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local cleanupFn = ECS.World:get(self.entity, ECS.Components.CleanupFn)
|
||||
|
||||
if oldValue and cleanupFn then
|
||||
cleanupFn(oldValue)
|
||||
end
|
||||
|
||||
Cache.Stack:Push(self.entity)
|
||||
local s, result = pcall(computeFn)
|
||||
Cache.Stack:Pop()
|
||||
|
||||
if not s then
|
||||
warn("Chemical Computed Error: ", result)
|
||||
return
|
||||
end
|
||||
|
||||
if result ~= oldValue then
|
||||
ECS.World:set(self.entity, ECS.Components.PrevValue, oldValue)
|
||||
ECS.World:set(self.entity, ECS.Components.Value, result)
|
||||
|
||||
local subscribers = GetSubscribers(self.entity)
|
||||
for _, subscriberEntity in ipairs(subscribers) do
|
||||
if not ECS.World:has(subscriberEntity, ECS.Tags.IsDirty) then
|
||||
ECS.World:add(subscriberEntity, ECS.Tags.IsDirty)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
37
src/Chemical/Mixins/Destroyable.lua
Normal file
37
src/Chemical/Mixins/Destroyable.lua
Normal file
|
@ -0,0 +1,37 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Types = require(RootFolder.Types)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
export type Destroyable = Types.HasEntity & {
|
||||
__destroyed: boolean,
|
||||
|
||||
destroy: (self: Destroyable) -> (),
|
||||
--__internalDestroy: (self: Destroyable) -> (),
|
||||
}
|
||||
|
||||
local methods = {}
|
||||
|
||||
function methods:__internalDestroy()
|
||||
if self.__destroyed then return end
|
||||
self.__destroyed = true
|
||||
|
||||
local cleanupFn = ECS.World:get(self.entity, ECS.Components.CleanupFn)
|
||||
if cleanupFn then
|
||||
cleanupFn()
|
||||
|
||||
ECS.World:remove(self.entity, ECS.Components.CleanupFn)
|
||||
end
|
||||
end
|
||||
|
||||
function methods:destroy()
|
||||
if self.__destroyed then return end
|
||||
|
||||
self:__internalDestroy()
|
||||
|
||||
ECS.World:delete(self.entity)
|
||||
end
|
||||
|
||||
return methods
|
33
src/Chemical/Mixins/Effectable.lua
Normal file
33
src/Chemical/Mixins/Effectable.lua
Normal file
|
@ -0,0 +1,33 @@
|
|||
--!strict
|
||||
local RootFolder = script.Parent.Parent
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
export type Effectable = Types.HasEntity & {
|
||||
run: (self: Effectable) -> (),
|
||||
}
|
||||
|
||||
return {
|
||||
run = function(self: Effectable)
|
||||
local effectFn = ECS.World:get(self.entity, ECS.Components.EffectFn)
|
||||
if not effectFn then return end
|
||||
|
||||
|
||||
local oldCleanupFn = ECS.World:get(self.entity, ECS.Components.CleanupFn)
|
||||
if oldCleanupFn then
|
||||
oldCleanupFn()
|
||||
|
||||
|
||||
ECS.World:remove(self.entity, ECS.Components.CleanupFn)
|
||||
end
|
||||
|
||||
Cache.Stack:Push(self.entity)
|
||||
local newCleanupFn = effectFn()
|
||||
Cache.Stack:Pop()
|
||||
|
||||
if newCleanupFn then
|
||||
ECS.World:set(self.entity, ECS.Components.CleanupFn, newCleanupFn)
|
||||
end
|
||||
end,
|
||||
}
|
28
src/Chemical/Mixins/Numerical.lua
Normal file
28
src/Chemical/Mixins/Numerical.lua
Normal file
|
@ -0,0 +1,28 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
export type Numerical = Types.HasEntity & {
|
||||
increment: (self: Numerical, n: number) -> (),
|
||||
decrement: (self: Numerical, n: number) -> ()
|
||||
}
|
||||
|
||||
return {
|
||||
increment = function(self: Numerical, n: number)
|
||||
local cachedValue = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.PrevValue, cachedValue)
|
||||
ECS.World:set(self.entity, ECS.Components.Value, cachedValue + n)
|
||||
end,
|
||||
|
||||
decrement = function(self: Numerical, n: number)
|
||||
local cachedValue = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.PrevValue, cachedValue)
|
||||
ECS.World:set(self.entity, ECS.Components.Value, cachedValue - n)
|
||||
end,
|
||||
}
|
103
src/Chemical/Mixins/Observable.lua
Normal file
103
src/Chemical/Mixins/Observable.lua
Normal file
|
@ -0,0 +1,103 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local LinkedList = require(RootFolder.Packages.LinkedList)
|
||||
|
||||
local Types = require(RootFolder.Types)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local DeepCompare = require(RootFolder.Functions.Compare)
|
||||
|
||||
|
||||
export type Observable<T = any> = Types.HasEntity & {
|
||||
__destroyed: boolean,
|
||||
onChange: (self: Observable<T>, callback: (new: T, old: T) -> ()) -> { disconnect: () -> () },
|
||||
run: (self: Observable<T>) -> (),
|
||||
destroy: (self: Observable<T>) -> (),
|
||||
}
|
||||
|
||||
export type ObservableTable<T = {}> = Observable<T> & {
|
||||
onKVChange: (self: ObservableTable<T>, callback: (path: {string|number}, new: any, old: any) -> ()) -> { disconnect: () -> () },
|
||||
}
|
||||
|
||||
return {
|
||||
onChange = function(self: Observable, callback: (any, any) -> ())
|
||||
local callbackList = ECS.World:get(self.entity, ECS.Components.OnChangeCallbacks)
|
||||
callbackList:InsertBack(callback)
|
||||
|
||||
return {
|
||||
disconnect = function()
|
||||
callbackList:Remove(callback)
|
||||
end,
|
||||
}
|
||||
end,
|
||||
|
||||
onKVChange = function(self: Observable, callback: (path: {any}, any, any) -> ())
|
||||
local kvCallbackList = ECS.World:get(self.entity, ECS.Components.OnKVChangeCallbacks)
|
||||
kvCallbackList:InsertBack(callback)
|
||||
|
||||
return {
|
||||
disconnect = function()
|
||||
kvCallbackList:Remove(callback)
|
||||
end,
|
||||
}
|
||||
end,
|
||||
|
||||
run = function(self: Observable)
|
||||
local sourceEntity = ECS.World:target(self.entity, ECS.Tags.SubscribesTo)
|
||||
if not sourceEntity then return end
|
||||
|
||||
local newValue = ECS.World:get(sourceEntity, ECS.Components.Value)
|
||||
local oldValue = ECS.World:get(sourceEntity, ECS.Components.PrevValue)
|
||||
|
||||
|
||||
local callbacksList = ECS.World:get(self.entity, ECS.Components.OnChangeCallbacks)
|
||||
if callbacksList then
|
||||
for link, callback in callbacksList:IterateForward() do
|
||||
local s, err = pcall(callback, newValue, oldValue)
|
||||
if not s then warn("Chemical Observer Error: onChange: ", err) end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if ECS.World:has(sourceEntity, ECS.Tags.IsDeepComparable) then
|
||||
local kvCallbackList = ECS.World:get(self.entity, ECS.Components.OnKVChangeCallbacks)
|
||||
if kvCallbackList then
|
||||
local changes = DeepCompare(oldValue, newValue)
|
||||
for _, change in ipairs(changes) do
|
||||
for link, callback in kvCallbackList:IterateForward() do
|
||||
local s, err = pcall(callback, change.Path, change.NewValue, change.OldValue)
|
||||
if not s then warn("Chemical Observer Error: onKVChange: ", err) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
|
||||
__internalDestroy = function(self: Observable & Types.MaybeCleanable)
|
||||
if self.__destroyed then return end
|
||||
self.__destroyed = true
|
||||
|
||||
|
||||
local callbacksList = ECS.World:get(self.entity, ECS.Components.OnChangeCallbacks)
|
||||
if callbacksList then callbacksList:Destroy() end
|
||||
|
||||
local kvCallbackList = ECS.World:get(self.entity, ECS.Components.OnKVChangeCallbacks)
|
||||
if kvCallbackList then kvCallbackList:Destroy() end
|
||||
|
||||
|
||||
if self.clean then self:clean() end
|
||||
|
||||
|
||||
setmetatable(self, nil)
|
||||
end,
|
||||
|
||||
destroy = function(self: Observable & Types.MaybeCleanable & Types.MaybeDestroyable)
|
||||
if self.__destroyed then return end
|
||||
|
||||
self:__internalDestroy()
|
||||
|
||||
ECS.World:delete(self.entity)
|
||||
end,
|
||||
}
|
73
src/Chemical/Mixins/Serializable.lua
Normal file
73
src/Chemical/Mixins/Serializable.lua
Normal file
|
@ -0,0 +1,73 @@
|
|||
--!strict
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
local Peek = require(RootFolder.Functions.Peek)
|
||||
local Array = require(RootFolder.Functions.Array)
|
||||
local Blueprint = require(RootFolder.Functions.Blueprint)
|
||||
|
||||
export type Serializable = Types.HasEntity & {
|
||||
serialize: (self: Serializable) -> (any),
|
||||
snapshot: (self: Serializable) -> (any),
|
||||
blueprint: (self: Serializable) -> (any | { any }),
|
||||
}
|
||||
|
||||
return {
|
||||
serialize = function(self: Serializable): any
|
||||
local value = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local serialized
|
||||
|
||||
if Is.Stateful(value) then
|
||||
local theValue = Peek(value)
|
||||
serialized = Is.Primitive(theValue) and theValue
|
||||
|
||||
elseif Is.Array(value) then
|
||||
serialized = Array.Transform(value, function(k, v)
|
||||
if Is.Stateful(v) then
|
||||
local theValue = Peek(v)
|
||||
return Is.Primitive(theValue) and theValue or nil
|
||||
elseif Is.Primitive(v) then
|
||||
return v
|
||||
end
|
||||
|
||||
return nil
|
||||
end)
|
||||
|
||||
elseif Is.Primitive(value) then
|
||||
serialized = value
|
||||
|
||||
end
|
||||
|
||||
return serialized, if not serialized then warn("There was nothing to serialize, or the value was unserializable.") else nil
|
||||
end,
|
||||
|
||||
snapshot = function(self: Serializable): any
|
||||
local value = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
|
||||
if Is.Stateful(value) then
|
||||
return Peek(value)
|
||||
elseif Is.Array(value) then
|
||||
return Array.Transform(value, function(k, v)
|
||||
if Is.Stateful(v) then
|
||||
local theValue = Peek(v)
|
||||
return Peek(v)
|
||||
elseif Is.Primitive(v) then
|
||||
return v
|
||||
end
|
||||
|
||||
return nil
|
||||
end)
|
||||
else
|
||||
return value
|
||||
end
|
||||
end,
|
||||
|
||||
blueprint = function(self: Serializable): any | { any }
|
||||
local value = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
return Blueprint:From(value)
|
||||
end,
|
||||
}
|
24
src/Chemical/Mixins/Settable.lua
Normal file
24
src/Chemical/Mixins/Settable.lua
Normal file
|
@ -0,0 +1,24 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
export type Settable<T = any> = Types.HasEntity & {
|
||||
set: (self: Settable<T>, T) -> ()
|
||||
}
|
||||
|
||||
return {
|
||||
set = function(self: Settable, value: any)
|
||||
local cachedValue = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
|
||||
if value == cachedValue then
|
||||
return
|
||||
end
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.PrevValue, cachedValue)
|
||||
ECS.World:set(self.entity, ECS.Components.Value, value)
|
||||
end,
|
||||
}
|
23
src/Chemical/Mixins/Stateful.lua
Normal file
23
src/Chemical/Mixins/Stateful.lua
Normal file
|
@ -0,0 +1,23 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
export type Stateful<T = any> = Types.HasEntity & {
|
||||
get: (self: Stateful<T>) -> (T)
|
||||
}
|
||||
|
||||
return {
|
||||
get = function(self: Stateful)
|
||||
local withinEntity = Cache.Stack:Top()
|
||||
if withinEntity then
|
||||
ECS.World:add(withinEntity, ECS.JECS.pair(ECS.Tags.SubscribesTo, self.entity))
|
||||
ECS.World:add(self.entity, ECS.JECS.pair(ECS.Tags.HasSubscriber, withinEntity))
|
||||
end
|
||||
|
||||
return ECS.World:get(self.entity, ECS.Components.Value)
|
||||
end,
|
||||
}
|
52
src/Chemical/Mixins/StatefulDictionary.lua
Normal file
52
src/Chemical/Mixins/StatefulDictionary.lua
Normal file
|
@ -0,0 +1,52 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
local Destroy = require(RootFolder.Functions:FindFirstChild("Destroy"))
|
||||
|
||||
export type StatefulDictionary<T = {}> = Types.HasEntity & {
|
||||
key: <K, V>(self: StatefulDictionary<T>, key: K, value: V?) -> (),
|
||||
clear: <V>(self: StatefulDictionary<T>, cleanup: (value: V) -> ()?) -> (any?),
|
||||
}
|
||||
|
||||
local function recursive(tbl, func)
|
||||
for key, value in tbl do
|
||||
if typeof(value) == "table" and not value.type then
|
||||
recursive(value, func)
|
||||
|
||||
continue
|
||||
end
|
||||
|
||||
func(value)
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
key = function<T, V>(self: StatefulDictionary, key: T, value: V?)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local newTbl = table.clone(tbl)
|
||||
|
||||
newTbl[key] = value
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
|
||||
end,
|
||||
|
||||
clear = function(self: StatefulDictionary, cleanup: (any) -> ())
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local newTbl = table.clone(tbl)
|
||||
|
||||
if cleanup then
|
||||
recursive(newTbl, cleanup)
|
||||
end
|
||||
|
||||
newTbl = {}
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
|
||||
|
||||
end,
|
||||
|
||||
}
|
71
src/Chemical/Mixins/StatefulTable.lua
Normal file
71
src/Chemical/Mixins/StatefulTable.lua
Normal file
|
@ -0,0 +1,71 @@
|
|||
--!strict
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Types = require(RootFolder.Types)
|
||||
|
||||
export type StatefulTable<T = {}> = Types.HasEntity & {
|
||||
insert: <V>(self: StatefulTable<T>, value: V) -> (),
|
||||
remove: <V>(self: StatefulTable<T>, value: V) -> (),
|
||||
find: <V>(self: StatefulTable<T>, value: V) -> (number)?,
|
||||
|
||||
setAt: <V>(self: StatefulTable<T>, index: number, value: V) -> (),
|
||||
getAt: (self: StatefulTable<T>, index: number) -> (any?),
|
||||
|
||||
clear: (self: StatefulTable<T>) -> (),
|
||||
}
|
||||
|
||||
return {
|
||||
insert = function<T>(self: StatefulTable, value: T)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local newTbl = table.clone(tbl)
|
||||
|
||||
table.insert(newTbl, value)
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
|
||||
end,
|
||||
|
||||
remove = function<T>(self: StatefulTable, value: T)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local newTbl = table.clone(tbl)
|
||||
|
||||
local index = table.find(newTbl, value)
|
||||
local poppedValue = table.remove(newTbl, index)
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
|
||||
|
||||
return poppedValue
|
||||
end,
|
||||
|
||||
find = function<T>(self: StatefulTable, value: T)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local found = table.find(tbl, value)
|
||||
|
||||
return found
|
||||
end,
|
||||
|
||||
setAt = function<T>(self: StatefulTable, index: number, value: T)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local newTbl = table.clone(tbl)
|
||||
|
||||
newTbl[index] = value
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
|
||||
end,
|
||||
|
||||
getAt = function(self: StatefulTable, index: number)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
return tbl[index]
|
||||
end,
|
||||
|
||||
clear = function(self: StatefulTable)
|
||||
local tbl = ECS.World:get(self.entity, ECS.Components.Value)
|
||||
local newTbl = table.clone(tbl)
|
||||
|
||||
table.clear(newTbl)
|
||||
|
||||
ECS.World:set(self.entity, ECS.Components.Value, newTbl)
|
||||
end,
|
||||
}
|
3
src/Chemical/Mixins/init.meta.json
Normal file
3
src/Chemical/Mixins/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
52
src/Chemical/Packages/Datastore/Proxy.lua
Normal file
52
src/Chemical/Packages/Datastore/Proxy.lua
Normal file
|
@ -0,0 +1,52 @@
|
|||
-- Variables
|
||||
local Constructor = {}
|
||||
local Index, NewIndex
|
||||
|
||||
|
||||
|
||||
|
||||
-- Types
|
||||
export type Constructor = {
|
||||
new: (data: {[any]: any}, public: {[any]: any}?) -> ({}, {[any]: any}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Constructor
|
||||
Constructor.new = function(data, public)
|
||||
local proxy = newproxy(true)
|
||||
local metatable = getmetatable(proxy)
|
||||
for index, value in data do metatable[index] = value end
|
||||
metatable.__index = Index
|
||||
metatable.__newindex = NewIndex
|
||||
metatable.__public = public or {}
|
||||
return proxy, metatable
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-- Functions
|
||||
Index = function(proxy, index)
|
||||
local metatable = getmetatable(proxy)
|
||||
local public = metatable.__public[index]
|
||||
return if public == nil then metatable.__shared[index] else public
|
||||
end
|
||||
|
||||
NewIndex = function(proxy, index, value)
|
||||
local metatable = getmetatable(proxy)
|
||||
local set = metatable.__set[index]
|
||||
if set == nil then
|
||||
metatable.__public[index] = value
|
||||
elseif set == false then
|
||||
error("Attempt to modify a readonly value", 2)
|
||||
else
|
||||
set(proxy, metatable, value)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
return table.freeze(Constructor) :: Constructor
|
219
src/Chemical/Packages/Datastore/Signal.lua
Normal file
219
src/Chemical/Packages/Datastore/Signal.lua
Normal file
|
@ -0,0 +1,219 @@
|
|||
-- Variables
|
||||
local Proxy = require(script.Parent.Proxy)
|
||||
local Constructor, Signal, Connection = {}, {}, {}
|
||||
local Thread, Call
|
||||
local threads = {}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Types
|
||||
export type Constructor = {
|
||||
new: () -> Signal,
|
||||
}
|
||||
|
||||
export type Signal = {
|
||||
[any]: any,
|
||||
Connections: number,
|
||||
Connected: (connected: boolean, signal: Signal) -> ()?,
|
||||
Connect: (self: Signal, func: (...any) -> (), ...any) -> Connection,
|
||||
Once: (self: Signal, func: (...any) -> (), ...any) -> Connection,
|
||||
Wait: (self: Signal, ...any) -> ...any,
|
||||
Fire: (self: Signal, ...any) -> (),
|
||||
FastFire: (self: Signal, ...any) -> (),
|
||||
DisconnectAll: (self: Signal) -> (),
|
||||
}
|
||||
|
||||
export type Connection = {
|
||||
[any]: any,
|
||||
Signal: Signal?,
|
||||
Disconnect: (self: Connection) -> (),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Constructor
|
||||
Constructor.new = function()
|
||||
local proxy, signal = Proxy.new(Signal, {Connections = 0})
|
||||
return proxy
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-- Signal
|
||||
Signal.__tostring = function(proxy)
|
||||
return "Signal"
|
||||
end
|
||||
|
||||
Signal.__shared = {
|
||||
Connect = function(proxy, func, ...)
|
||||
local signal = getmetatable(proxy)
|
||||
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to Connect failed: Passed value is not a Signal", 3) end
|
||||
if type(func) ~= "function" then error("Attempt to Connect failed: Passed value is not a function", 3) end
|
||||
signal.__public.Connections += 1
|
||||
local connectionProxy, connection = Proxy.new(Connection, {Signal = proxy})
|
||||
connection.FunctionOrThread = func
|
||||
connection.Parameters = if ... == nil then nil else {...}
|
||||
if signal.Last == nil then signal.First, signal.Last = connection, connection else connection.Previous, signal.Last.Next, signal.Last = signal.Last, connection, connection end
|
||||
if signal.__public.Connections == 1 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, true, proxy) end
|
||||
return connectionProxy
|
||||
end,
|
||||
Once = function(proxy, func, ...)
|
||||
local signal = getmetatable(proxy)
|
||||
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to Connect failed: Passed value is not a Signal", 3) end
|
||||
if type(func) ~= "function" then error("Attempt to Connect failed: Passed value is not a function", 3) end
|
||||
signal.__public.Connections += 1
|
||||
local connectionProxy, connection = Proxy.new(Connection, {Signal = proxy})
|
||||
connection.FunctionOrThread = func
|
||||
connection.Once = true
|
||||
connection.Parameters = if ... == nil then nil else {...}
|
||||
if signal.Last == nil then signal.First, signal.Last = connection, connection else connection.Previous, signal.Last.Next, signal.Last = signal.Last, connection, connection end
|
||||
if signal.__public.Connections == 1 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, true, proxy) end
|
||||
return connectionProxy
|
||||
end,
|
||||
Wait = function(proxy, ...)
|
||||
local signal = getmetatable(proxy)
|
||||
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to Connect failed: Passed value is not a Signal", 3) end
|
||||
signal.__public.Connections += 1
|
||||
local connectionProxy, connection = Proxy.new(Connection, {Signal = proxy})
|
||||
connection.FunctionOrThread = coroutine.running()
|
||||
connection.Once = true
|
||||
connection.Parameters = if ... == nil then nil else {...}
|
||||
if signal.Last == nil then signal.First, signal.Last = connection, connection else connection.Previous, signal.Last.Next, signal.Last = signal.Last, connection, connection end
|
||||
if signal.__public.Connections == 1 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, true, proxy) end
|
||||
return coroutine.yield()
|
||||
end,
|
||||
Fire = function(proxy, ...)
|
||||
local signal = getmetatable(proxy)
|
||||
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to connect failed: Passed value is not a Signal", 3) end
|
||||
local connection = signal.First
|
||||
while connection ~= nil do
|
||||
if connection.Once == true then
|
||||
signal.__public.Connections -= 1
|
||||
connection.__public.Signal = nil
|
||||
if signal.First == connection then signal.First = connection.Next end
|
||||
if signal.Last == connection then signal.Last = connection.Previous end
|
||||
if connection.Previous ~= nil then connection.Previous.Next = connection.Next end
|
||||
if connection.Next ~= nil then connection.Next.Previous = connection.Previous end
|
||||
if signal.__public.Connections == 0 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, false, proxy) end
|
||||
end
|
||||
if type(connection.FunctionOrThread) == "thread" then
|
||||
if connection.Parameters == nil then
|
||||
task.spawn(connection.FunctionOrThread, ...)
|
||||
else
|
||||
local parameters = {...}
|
||||
task.spawn(connection.FunctionOrThread, table.unpack(table.move(connection.Parameters, 1, #connection.Parameters, #parameters + 1, parameters)))
|
||||
end
|
||||
else
|
||||
local thread = table.remove(threads)
|
||||
if thread == nil then thread = coroutine.create(Thread) coroutine.resume(thread) end
|
||||
if connection.Parameters == nil then
|
||||
task.spawn(thread, thread, connection.FunctionOrThread, ...)
|
||||
else
|
||||
local parameters = {...}
|
||||
task.spawn(thread, thread, connection.FunctionOrThread, table.unpack(table.move(connection.Parameters, 1, #connection.Parameters, #parameters + 1, parameters)))
|
||||
end
|
||||
end
|
||||
connection = connection.Next
|
||||
end
|
||||
end,
|
||||
FastFire = function(proxy, ...)
|
||||
local signal = getmetatable(proxy)
|
||||
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to connect failed: Passed value is not a Signal", 3) end
|
||||
local connection = signal.First
|
||||
while connection ~= nil do
|
||||
if connection.Once == true then
|
||||
signal.__public.Connections -= 1
|
||||
connection.__public.Signal = nil
|
||||
if signal.First == connection then signal.First = connection.Next end
|
||||
if signal.Last == connection then signal.Last = connection.Previous end
|
||||
if connection.Previous ~= nil then connection.Previous.Next = connection.Next end
|
||||
if connection.Next ~= nil then connection.Next.Previous = connection.Previous end
|
||||
if signal.__public.Connections == 0 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, false, proxy) end
|
||||
end
|
||||
if type(connection.FunctionOrThread) == "thread" then
|
||||
if connection.Parameters == nil then
|
||||
coroutine.resume(connection.FunctionOrThread, ...)
|
||||
else
|
||||
local parameters = {...}
|
||||
coroutine.resume(connection.FunctionOrThread, table.unpack(table.move(connection.Parameters, 1, #connection.Parameters, #parameters + 1, parameters)))
|
||||
end
|
||||
else
|
||||
if connection.Parameters == nil then
|
||||
connection.FunctionOrThread(...)
|
||||
else
|
||||
local parameters = {...}
|
||||
connection.FunctionOrThread(table.unpack(table.move(connection.Parameters, 1, #connection.Parameters, #parameters + 1, parameters)))
|
||||
end
|
||||
end
|
||||
connection = connection.Next
|
||||
end
|
||||
end,
|
||||
DisconnectAll = function(proxy)
|
||||
local signal = getmetatable(proxy)
|
||||
if type(signal) ~= "table" or signal.__shared ~= Signal.__shared then error("Attempt to Connect failed: Passed value is not a Signal", 3) end
|
||||
local connection = signal.First
|
||||
if connection == nil then return end
|
||||
while connection ~= nil do
|
||||
connection.__public.Signal = nil
|
||||
if type(connection.FunctionOrThread) == "thread" then task.cancel(connection.FunctionOrThread) end
|
||||
connection = connection.Next
|
||||
end
|
||||
if signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, false, proxy) end
|
||||
signal.__public.Connections, signal.First, signal.Last = 0, nil, nil
|
||||
end,
|
||||
}
|
||||
|
||||
Signal.__set = {
|
||||
Connections = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Connection
|
||||
Connection.__tostring = function(proxy)
|
||||
return "Connection"
|
||||
end
|
||||
|
||||
Connection.__shared = {
|
||||
Disconnect = function(proxy)
|
||||
local connection = getmetatable(proxy)
|
||||
if type(connection) ~= "table" or connection.__shared ~= Connection.__shared then error("Attempt to Disconnect failed: Passed value is not a Connection", 3) end
|
||||
local signal = getmetatable(connection.__public.Signal)
|
||||
if signal == nil then return end
|
||||
signal.__public.Connections -= 1
|
||||
connection.__public.Signal = nil
|
||||
if signal.First == connection then signal.First = connection.Next end
|
||||
if signal.Last == connection then signal.Last = connection.Previous end
|
||||
if connection.Previous ~= nil then connection.Previous.Next = connection.Next end
|
||||
if connection.Next ~= nil then connection.Next.Previous = connection.Previous end
|
||||
if type(connection.FunctionOrThread) == "thread" then task.cancel(connection.FunctionOrThread) end
|
||||
if signal.__public.Connections == 0 and signal.__public.Connected ~= nil then task.defer(signal.__public.Connected, false, proxy) end
|
||||
end,
|
||||
}
|
||||
|
||||
Connection.__set = {
|
||||
Signal = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Functions
|
||||
Thread = function()
|
||||
while true do Call(coroutine.yield()) end
|
||||
end
|
||||
|
||||
Call = function(thread, func, ...)
|
||||
func(...)
|
||||
if #threads >= 16 then return end
|
||||
table.insert(threads, thread)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
return table.freeze(Constructor) :: Constructor
|
266
src/Chemical/Packages/Datastore/SynchronousTaskManager.lua
Normal file
266
src/Chemical/Packages/Datastore/SynchronousTaskManager.lua
Normal file
|
@ -0,0 +1,266 @@
|
|||
-- Variables
|
||||
local Proxy = require(script.Parent.Proxy)
|
||||
local Constructor, TaskManager, SynchronousTask, RunningTask = {}, {}, {}, {}
|
||||
local Run
|
||||
|
||||
|
||||
|
||||
|
||||
-- Types
|
||||
export type Constructor = {
|
||||
new: () -> TaskManager,
|
||||
}
|
||||
|
||||
export type TaskManager = {
|
||||
[any]: any,
|
||||
Enabled: boolean,
|
||||
Tasks: number,
|
||||
Running: SynchronousTask?,
|
||||
InsertFront: (self: TaskManager, func: (RunningTask, ...any) -> (), ...any) -> SynchronousTask,
|
||||
InsertBack: (self: TaskManager, func: (RunningTask, ...any) -> (), ...any) -> SynchronousTask,
|
||||
FindFirst: (self: TaskManager, func: (RunningTask, ...any) -> ()) -> (SynchronousTask?, number?),
|
||||
FindLast: (self: TaskManager, func: (RunningTask, ...any) -> ()) -> (SynchronousTask?, number?),
|
||||
CancelAll: (self: TaskManager, func: (RunningTask, ...any) -> ()?) -> (),
|
||||
}
|
||||
|
||||
export type SynchronousTask = {
|
||||
[any]: any,
|
||||
TaskManager: TaskManager?,
|
||||
Running: boolean,
|
||||
Wait: (self: SynchronousTask, ...any) -> ...any,
|
||||
Cancel: (self: SynchronousTask) -> (),
|
||||
}
|
||||
|
||||
export type RunningTask = {
|
||||
Next: (self: RunningTask) -> (thread, ...any),
|
||||
Iterate: (self: RunningTask) -> ((self: RunningTask) -> (thread, ...any), RunningTask),
|
||||
End: (self: RunningTask) -> (),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Constructor
|
||||
Constructor.new = function()
|
||||
local proxy, taskManager = Proxy.new(TaskManager, {Enabled = true, Tasks = 0})
|
||||
taskManager.Active = false
|
||||
return proxy
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-- TaskManager
|
||||
TaskManager.__tostring = function(proxy)
|
||||
return "Task Manager"
|
||||
end
|
||||
|
||||
TaskManager.__shared = {
|
||||
InsertFront = function(proxy, func, ...)
|
||||
local taskManager = getmetatable(proxy)
|
||||
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to InsertFront failed: Passed value is not a Task Manager", 3) end
|
||||
if type(func) ~= "function" then error("Attempt to InsertFront failed: Passed value is not a function", 3) end
|
||||
taskManager.__public.Tasks += 1
|
||||
local proxy, synchronousTask = Proxy.new(SynchronousTask, {TaskManager = proxy, Running = false})
|
||||
synchronousTask.Active = true
|
||||
synchronousTask.Function = func
|
||||
synchronousTask.Parameters = if ... == nil then nil else {...}
|
||||
if taskManager.First == nil then taskManager.First, taskManager.Last = proxy, proxy else synchronousTask.Next, getmetatable(taskManager.First).Previous, taskManager.First = taskManager.First, proxy, proxy end
|
||||
if taskManager.Active == false and taskManager.__public.Enabled == true then taskManager.Active = true task.defer(Run, taskManager) end
|
||||
return proxy
|
||||
end,
|
||||
InsertBack = function(proxy, func, ...)
|
||||
local taskManager = getmetatable(proxy)
|
||||
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to InsertBack failed: Passed value is not a Task Manager", 3) end
|
||||
if type(func) ~= "function" then error("Attempt to InsertBack failed: Passed value is not a function", 3) end
|
||||
taskManager.__public.Tasks += 1
|
||||
local proxy, synchronousTask = Proxy.new(SynchronousTask, {TaskManager = proxy, Running = false})
|
||||
synchronousTask.Active = true
|
||||
synchronousTask.Function = func
|
||||
synchronousTask.Parameters = if ... == nil then nil else {...}
|
||||
if taskManager.Last == nil then taskManager.First, taskManager.Last = proxy, proxy else synchronousTask.Previous, getmetatable(taskManager.Last).Next, taskManager.Last = taskManager.Last, proxy, proxy end
|
||||
if taskManager.Active == false and taskManager.__public.Enabled == true then taskManager.Active = true task.defer(Run, taskManager) end
|
||||
return proxy
|
||||
end,
|
||||
FindFirst = function(proxy, func)
|
||||
local taskManager = getmetatable(proxy)
|
||||
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to FindFirst failed: Passed value is not a Task Manager", 3) end
|
||||
if type(func) ~= "function" then error("Attempt to FindFirst failed: Passed value is not a function", 3) end
|
||||
proxy = taskManager.__public.Running
|
||||
if proxy ~= nil then
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if synchronousTask.Active == true and synchronousTask.Function == func then return proxy, 0 end
|
||||
end
|
||||
local index = 1
|
||||
proxy = taskManager.First
|
||||
while proxy ~= nil do
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if synchronousTask.Function == func then return proxy, index end
|
||||
proxy = synchronousTask.Next
|
||||
index += 1
|
||||
end
|
||||
end,
|
||||
FindLast = function(proxy, func)
|
||||
local taskManager = getmetatable(proxy)
|
||||
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to FindLast failed: Passed value is not a Task Manager", 3) end
|
||||
if type(func) ~= "function" then error("Attempt to FindFirst failed: Passed value is not a function", 3) end
|
||||
local index = if taskManager.__public.Running == nil then taskManager.__public.Tasks else taskManager.__public.Tasks - 1
|
||||
proxy = taskManager.Last
|
||||
while proxy ~= nil do
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if synchronousTask.Function == func then return proxy, index end
|
||||
proxy = synchronousTask.Previous
|
||||
index -= 1
|
||||
end
|
||||
proxy = taskManager.__public.Running
|
||||
if proxy ~= nil then
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if synchronousTask.Active == true and synchronousTask.Function == func then return proxy, 0 end
|
||||
end
|
||||
end,
|
||||
CancelAll = function(proxy, func)
|
||||
local taskManager = getmetatable(proxy)
|
||||
if type(taskManager) ~= "table" or taskManager.__shared ~= TaskManager.__shared then error("Attempt to FindLast failed: Passed value is not a Task Manager", 3) end
|
||||
if func == nil then
|
||||
local proxy = taskManager.First
|
||||
taskManager.First = nil
|
||||
taskManager.Last = nil
|
||||
if taskManager.__public.Running == nil then taskManager.__public.Tasks = 0 else taskManager.__public.Tasks = 1 end
|
||||
while proxy ~= nil do
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
proxy, synchronousTask.Active, synchronousTask.__public.TaskManager, synchronousTask.Previous, synchronousTask.Next = synchronousTask.Next, false, nil, nil, nil
|
||||
end
|
||||
else
|
||||
if type(func) ~= "function" then error("Attempt to CancelAll failed: Passed value is not nil or function", 3) end
|
||||
local proxy = taskManager.First
|
||||
while proxy ~= nil do
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if synchronousTask.Function == func then
|
||||
taskManager.__public.Tasks -= 1
|
||||
if taskManager.First == proxy then taskManager.First = synchronousTask.Next end
|
||||
if taskManager.Last == proxy then taskManager.Last = synchronousTask.Previous end
|
||||
if synchronousTask.Previous ~= nil then getmetatable(synchronousTask.Previous).Next = synchronousTask.Next end
|
||||
if synchronousTask.Next ~= nil then getmetatable(synchronousTask.Next).Previous = synchronousTask.Previous end
|
||||
proxy, synchronousTask.Active, synchronousTask.__public.TaskManager, synchronousTask.Previous, synchronousTask.Next = synchronousTask.Next, false, nil, nil, nil
|
||||
else
|
||||
proxy = synchronousTask.Next
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
TaskManager.__set = {
|
||||
Enabled = function(proxy, taskManager, value)
|
||||
if type(value) ~= "boolean" then error("Attempt to set Enabled failed: Passed value is not a boolean", 3) end
|
||||
taskManager.__public.Enabled = value
|
||||
if value == false or taskManager.First == nil or taskManager.Active == true then return end
|
||||
taskManager.Active = true
|
||||
task.defer(Run, taskManager)
|
||||
end,
|
||||
Tasks = false,
|
||||
Running = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- SynchronousTask
|
||||
SynchronousTask.__tostring = function(proxy)
|
||||
return "Synchronous Task"
|
||||
end
|
||||
|
||||
SynchronousTask.__shared = {
|
||||
Wait = function(proxy, ...)
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if type(synchronousTask) ~= "table" or synchronousTask.__shared ~= SynchronousTask.__shared then error("Attempt to Wait failed: Passed value is not a Synchronous Task", 3) end
|
||||
if synchronousTask.Active == false then return end
|
||||
local waiter = {coroutine.running(), ...}
|
||||
if synchronousTask.Last == nil then synchronousTask.First, synchronousTask.Last = waiter, waiter else synchronousTask.Last.Next, synchronousTask.Last = waiter, waiter end
|
||||
return coroutine.yield()
|
||||
end,
|
||||
Cancel = function(proxy)
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
if type(synchronousTask) ~= "table" or synchronousTask.__shared ~= SynchronousTask.__shared then error("Attempt to Cancel failed: Passed value is not a Synchronous Task", 3) end
|
||||
if synchronousTask.__public.Running == true then return false end
|
||||
local taskManager = synchronousTask.__public.TaskManager
|
||||
if taskManager == nil then return false end
|
||||
taskManager = getmetatable(taskManager)
|
||||
taskManager.__public.Tasks -= 1
|
||||
if taskManager.First == proxy then taskManager.First = synchronousTask.Next end
|
||||
if taskManager.Last == proxy then taskManager.Last = synchronousTask.Previous end
|
||||
if synchronousTask.Previous ~= nil then getmetatable(synchronousTask.Previous).Next = synchronousTask.Next end
|
||||
if synchronousTask.Next ~= nil then getmetatable(synchronousTask.Next).Previous = synchronousTask.Previous end
|
||||
synchronousTask.Active, synchronousTask.__public.TaskManager, synchronousTask.Previous, synchronousTask.Next = false, nil, nil, nil
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
SynchronousTask.__set = {
|
||||
TaskManager = false,
|
||||
Running = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- RunningTask
|
||||
RunningTask.__tostring = function(proxy)
|
||||
return "Running Task"
|
||||
end
|
||||
|
||||
RunningTask.__shared = {
|
||||
Next = function(proxy)
|
||||
local runningTask = getmetatable(proxy)
|
||||
if type(runningTask) ~= "table" or runningTask.__shared ~= RunningTask.__shared then error("Attempt to Next failed: Passed value is not a Running Task", 3) end
|
||||
local synchronousTask = runningTask.SynchronousTask
|
||||
local waiter = synchronousTask.First
|
||||
if waiter == nil then return end
|
||||
synchronousTask.First = waiter.Next
|
||||
if synchronousTask.Last == waiter then synchronousTask.Last = nil end
|
||||
return table.unpack(waiter)
|
||||
end,
|
||||
Iterate = function(proxy)
|
||||
local runningTask = getmetatable(proxy)
|
||||
if type(runningTask) ~= "table" or runningTask.__shared ~= RunningTask.__shared then error("Attempt to Iterate failed: Passed value is not a Running Task", 3) end
|
||||
return runningTask.__shared.Next, proxy
|
||||
end,
|
||||
End = function(proxy)
|
||||
local runningTask = getmetatable(proxy)
|
||||
if type(runningTask) ~= "table" or runningTask.__shared ~= RunningTask.__shared then error("Attempt to End failed: Passed value is not a Running Task", 3) end
|
||||
runningTask.SynchronousTask.Active = false
|
||||
end,
|
||||
}
|
||||
|
||||
RunningTask.__set = {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Functions
|
||||
Run = function(taskManager)
|
||||
if taskManager.__public.Enabled == false then taskManager.Active = false return end
|
||||
local proxy = taskManager.First
|
||||
if proxy == nil then taskManager.Active = false return end
|
||||
local synchronousTask = getmetatable(proxy)
|
||||
taskManager.__public.Running = proxy
|
||||
taskManager.First = synchronousTask.Next
|
||||
synchronousTask.__public.Running = true
|
||||
if synchronousTask.Next == nil then taskManager.Last = nil else getmetatable(synchronousTask.Next).Previous = nil synchronousTask.Next = nil end
|
||||
local proxy, runningTask = Proxy.new(RunningTask)
|
||||
runningTask.SynchronousTask = synchronousTask
|
||||
if synchronousTask.Parameters == nil then synchronousTask.Function(proxy) else synchronousTask.Function(proxy, table.unpack(synchronousTask.Parameters)) end
|
||||
taskManager.__public.Tasks -= 1
|
||||
taskManager.__public.Running = nil
|
||||
synchronousTask.Active = false
|
||||
synchronousTask.__public.TaskManager = nil
|
||||
synchronousTask.__public.Running = false
|
||||
if taskManager.__public.Enabled == false or taskManager.First == nil then taskManager.Active = false else task.defer(Run, taskManager) end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
return table.freeze(Constructor) :: Constructor
|
734
src/Chemical/Packages/Datastore/init.lua
Normal file
734
src/Chemical/Packages/Datastore/init.lua
Normal file
|
@ -0,0 +1,734 @@
|
|||
-- Variables
|
||||
local Proxy = require(script.Proxy)
|
||||
local Signal = require(script.Signal)
|
||||
local SynchronousTaskManager = require(script.SynchronousTaskManager)
|
||||
local dataStoreService, memoryStoreService, httpService = game:GetService("DataStoreService"), game:GetService("MemoryStoreService"), game:GetService("HttpService")
|
||||
local Constructor, DataStore = {}, {}
|
||||
local OpenTask, ReadTask, LockTask, SaveTask, CloseTask, DestroyTask, Lock, Unlock, Load, Save, StartSaveTimer, StopSaveTimer, SaveTimerEnded, StartLockTimer, StopLockTimer, LockTimerEnded, ProcessQueue, SignalConnected, Clone, Reconcile, Compress, Decompress, Encode, Decode, BindToClose
|
||||
local dataStores, bindToClose, active = {}, {}, true
|
||||
local characters = {[0] = "0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","!","$","%","&","'",",",".","/",":",";","=","?","@","[","]","^","_","`","{","}","~"}
|
||||
local bytes = {} for i = (0), #characters do bytes[string.byte(characters[i])] = i end
|
||||
local base = #characters + 1
|
||||
|
||||
|
||||
|
||||
|
||||
-- Types
|
||||
export type Constructor = {
|
||||
new: (name: string, scope: string, key: string?) -> DataStore,
|
||||
hidden: (name: string, scope: string, key: string?) -> DataStore,
|
||||
find: (name: string, scope: string, key: string?) -> DataStore?,
|
||||
Response: {Success: string, Saved: string, Locked: string, State: string, Error: string},
|
||||
}
|
||||
|
||||
export type DataStore = {
|
||||
Value: any,
|
||||
Metadata: {[string]: any},
|
||||
UserIds: {any},
|
||||
SaveInterval: number,
|
||||
SaveDelay: number,
|
||||
LockInterval: number,
|
||||
LockAttempts: number,
|
||||
SaveOnClose: boolean,
|
||||
Id: string,
|
||||
UniqueId: string,
|
||||
Key: string,
|
||||
State: boolean?,
|
||||
Hidden: boolean,
|
||||
AttemptsRemaining: number,
|
||||
CreatedTime: number,
|
||||
UpdatedTime: number,
|
||||
Version: string,
|
||||
CompressedValue: string,
|
||||
StateChanged: Signal.Signal,
|
||||
Saving: Signal.Signal,
|
||||
Saved: Signal.Signal,
|
||||
AttemptsChanged: Signal.Signal,
|
||||
ProcessQueue: Signal.Signal,
|
||||
Open: (self: DataStore, template: any?) -> (string, any),
|
||||
Read: (self: DataStore, template: any?) -> (string, any),
|
||||
Save: (self: DataStore) -> (string, any),
|
||||
Close: (self: DataStore) -> (string, any),
|
||||
Destroy: (self: DataStore) -> (string, any),
|
||||
Queue: (self: DataStore, value: any, expiration: number?, priority: number?) -> (string, any),
|
||||
Remove: (self: DataStore, id: string) -> (string, any),
|
||||
Clone: (self: DataStore) -> any,
|
||||
Reconcile: (self: DataStore, template: any) -> (),
|
||||
Usage: (self: DataStore) -> (number, number),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Constructor
|
||||
Constructor.new = function(name, scope, key)
|
||||
if key == nil then key, scope = scope, "global" end
|
||||
local id = name .. "/" .. scope .. "/" .. key
|
||||
if dataStores[id] ~= nil then return dataStores[id] end
|
||||
local proxy, dataStore = Proxy.new(DataStore, {
|
||||
Metadata = {},
|
||||
UserIds = {},
|
||||
SaveInterval = 30,
|
||||
SaveDelay = 0,
|
||||
LockInterval = 60,
|
||||
LockAttempts = 5,
|
||||
SaveOnClose = true,
|
||||
Id = id,
|
||||
UniqueId = httpService:GenerateGUID(false),
|
||||
Key = key,
|
||||
State = false,
|
||||
Hidden = false,
|
||||
AttemptsRemaining = 0,
|
||||
CreatedTime = 0,
|
||||
UpdatedTime = 0,
|
||||
Version = "",
|
||||
CompressedValue = "",
|
||||
StateChanged = Signal.new(),
|
||||
Saving = Signal.new(),
|
||||
Saved = Signal.new(),
|
||||
AttemptsChanged = Signal.new(),
|
||||
ProcessQueue = Signal.new(),
|
||||
})
|
||||
dataStore.TaskManager = SynchronousTaskManager.new()
|
||||
dataStore.LockTime = -math.huge
|
||||
dataStore.SaveTime = -math.huge
|
||||
dataStore.ActiveLockInterval = 0
|
||||
dataStore.ProcessingQueue = false
|
||||
dataStore.DataStore = dataStoreService:GetDataStore(name, scope)
|
||||
dataStore.MemoryStore = memoryStoreService:GetSortedMap(id)
|
||||
dataStore.Queue = memoryStoreService:GetQueue(id)
|
||||
dataStore.Options = Instance.new("DataStoreSetOptions")
|
||||
dataStore.__public.ProcessQueue.DataStore = proxy
|
||||
dataStore.__public.ProcessQueue.Connected = SignalConnected
|
||||
dataStores[id] = proxy
|
||||
if active == true then bindToClose[dataStore.__public.UniqueId] = proxy end
|
||||
return proxy
|
||||
end
|
||||
|
||||
Constructor.hidden = function(name, scope, key)
|
||||
if key == nil then key, scope = scope, "global" end
|
||||
local id = name .. "/" .. scope .. "/" .. key
|
||||
local proxy, dataStore = Proxy.new(DataStore, {
|
||||
Metadata = {},
|
||||
UserIds = {},
|
||||
SaveInterval = 30,
|
||||
SaveDelay = 0,
|
||||
LockInterval = 60,
|
||||
LockAttempts = 5,
|
||||
SaveOnClose = true,
|
||||
Id = id,
|
||||
UniqueId = httpService:GenerateGUID(false),
|
||||
Key = key,
|
||||
State = false,
|
||||
Hidden = true,
|
||||
AttemptsRemaining = 0,
|
||||
CreatedTime = 0,
|
||||
UpdatedTime = 0,
|
||||
Version = "",
|
||||
CompressedValue = "",
|
||||
StateChanged = Signal.new(),
|
||||
Saving = Signal.new(),
|
||||
Saved = Signal.new(),
|
||||
AttemptsChanged = Signal.new(),
|
||||
ProcessQueue = Signal.new(),
|
||||
})
|
||||
dataStore.TaskManager = SynchronousTaskManager.new()
|
||||
dataStore.LockTime = -math.huge
|
||||
dataStore.SaveTime = -math.huge
|
||||
dataStore.ActiveLockInterval = 0
|
||||
dataStore.ProcessingQueue = false
|
||||
dataStore.DataStore = dataStoreService:GetDataStore(name, scope)
|
||||
dataStore.MemoryStore = memoryStoreService:GetSortedMap(id)
|
||||
dataStore.Queue = memoryStoreService:GetQueue(id)
|
||||
dataStore.Options = Instance.new("DataStoreSetOptions")
|
||||
dataStore.__public.ProcessQueue.DataStore = proxy
|
||||
dataStore.__public.ProcessQueue.Connected = SignalConnected
|
||||
if active == true then bindToClose[dataStore.__public.UniqueId] = proxy end
|
||||
return proxy
|
||||
end
|
||||
|
||||
Constructor.find = function(name, scope, key)
|
||||
if key == nil then key, scope = scope, "global" end
|
||||
local id = name .. "/" .. scope .. "/" .. key
|
||||
return dataStores[id]
|
||||
end
|
||||
|
||||
Constructor.Response = {Success = "Success", Saved = "Saved", Locked = "Locked", State = "State", Error = "Error"}
|
||||
|
||||
|
||||
|
||||
|
||||
-- DataStore
|
||||
DataStore.__tostring = function(proxy)
|
||||
return "DataStore"
|
||||
end
|
||||
|
||||
DataStore.__shared = {
|
||||
Open = function(proxy, template)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Open failed: Passed value is not a DataStore", 3) end
|
||||
if dataStore.__public.State == nil then return "State", "Destroyed" end
|
||||
local synchronousTask = dataStore.TaskManager:FindFirst(OpenTask)
|
||||
if synchronousTask ~= nil then return synchronousTask:Wait(template) end
|
||||
if dataStore.TaskManager:FindLast(DestroyTask) ~= nil then return "State", "Destroying" end
|
||||
if dataStore.__public.State == true and dataStore.TaskManager:FindLast(CloseTask) == nil then
|
||||
if dataStore.__public.Value == nil then
|
||||
dataStore.__public.Value = Clone(template)
|
||||
elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
|
||||
Reconcile(dataStore.__public.Value, template)
|
||||
end
|
||||
return "Success"
|
||||
end
|
||||
return dataStore.TaskManager:InsertBack(OpenTask, proxy):Wait(template)
|
||||
end,
|
||||
Read = function(proxy, template)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Read failed: Passed value is not a DataStore", 3) end
|
||||
local synchronousTask = dataStore.TaskManager:FindFirst(ReadTask)
|
||||
if synchronousTask ~= nil then return synchronousTask:Wait(template) end
|
||||
if dataStore.__public.State == true and dataStore.TaskManager:FindLast(CloseTask) == nil then return "State", "Open" end
|
||||
return dataStore.TaskManager:InsertBack(ReadTask, proxy):Wait(template)
|
||||
end,
|
||||
Save = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Save failed: Passed value is not a DataStore", 3) end
|
||||
if dataStore.__public.State == false then return "State", "Closed" end
|
||||
if dataStore.__public.State == nil then return "State", "Destroyed" end
|
||||
local synchronousTask = dataStore.TaskManager:FindFirst(SaveTask)
|
||||
if synchronousTask ~= nil then return synchronousTask:Wait() end
|
||||
if dataStore.TaskManager:FindLast(CloseTask) ~= nil then return "State", "Closing" end
|
||||
if dataStore.TaskManager:FindLast(DestroyTask) ~= nil then return "State", "Destroying" end
|
||||
return dataStore.TaskManager:InsertBack(SaveTask, proxy):Wait()
|
||||
end,
|
||||
Close = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Close failed: Passed value is not a DataStore", 3) end
|
||||
if dataStore.__public.State == nil then return "Success" end
|
||||
local synchronousTask = dataStore.TaskManager:FindFirst(CloseTask)
|
||||
if synchronousTask ~= nil then return synchronousTask:Wait() end
|
||||
if dataStore.__public.State == false and dataStore.TaskManager:FindLast(OpenTask) == nil then return "Success" end
|
||||
local synchronousTask = dataStore.TaskManager:FindFirst(DestroyTask)
|
||||
if synchronousTask ~= nil then return synchronousTask:Wait() end
|
||||
StopLockTimer(dataStore)
|
||||
StopSaveTimer(dataStore)
|
||||
return dataStore.TaskManager:InsertBack(CloseTask, proxy):Wait()
|
||||
end,
|
||||
Destroy = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Destroy failed: Passed value is not a DataStore", 3) end
|
||||
if dataStore.__public.State == nil then return "Success" end
|
||||
dataStores[dataStore.__public.Id] = nil
|
||||
StopLockTimer(dataStore)
|
||||
StopSaveTimer(dataStore)
|
||||
return (dataStore.TaskManager:FindFirst(DestroyTask) or dataStore.TaskManager:InsertBack(DestroyTask, proxy)):Wait()
|
||||
end,
|
||||
Queue = function(proxy, value, expiration, priority)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Queue failed: Passed value is not a DataStore", 3) end
|
||||
if expiration ~= nil and type(expiration) ~= "number" then error("Attempt to Queue failed: Passed value is not nil or number", 3) end
|
||||
if priority ~= nil and type(priority) ~= "number" then error("Attempt to Queue failed: Passed value is not nil or number", 3) end
|
||||
local success, errorMessage
|
||||
for i = 1, 3 do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, errorMessage = pcall(dataStore.Queue.AddAsync, dataStore.Queue, value, expiration or 604800, priority)
|
||||
if success == true then return "Success" end
|
||||
end
|
||||
return "Error", errorMessage
|
||||
end,
|
||||
Remove = function(proxy, id)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Remove failed: Passed value is not a DataStore", 3) end
|
||||
if type(id) ~= "string" then error("Attempt to RemoveQueue failed: Passed value is not a string", 3) end
|
||||
local success, errorMessage
|
||||
for i = 1, 3 do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, errorMessage = pcall(dataStore.Queue.RemoveAsync, dataStore.Queue, id)
|
||||
if success == true then return "Success" end
|
||||
end
|
||||
return "Error", errorMessage
|
||||
end,
|
||||
Clone = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Clone failed: Passed value is not a DataStore", 3) end
|
||||
return Clone(dataStore.__public.Value)
|
||||
end,
|
||||
Reconcile = function(proxy, template)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Reconcile failed: Passed value is not a DataStore", 3) end
|
||||
if dataStore.__public.Value == nil then
|
||||
dataStore.__public.Value = Clone(template)
|
||||
elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
|
||||
Reconcile(dataStore.__public.Value, template)
|
||||
end
|
||||
end,
|
||||
Usage = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Usage failed: Passed value is not a DataStore", 3) end
|
||||
if dataStore.__public.Value == nil then return 0, 0 end
|
||||
if type(dataStore.__public.Metadata.Compress) ~= "table" then
|
||||
local characters = #httpService:JSONEncode(dataStore.__public.Value)
|
||||
return characters, characters / 4194303
|
||||
else
|
||||
local level = dataStore.__public.Metadata.Compress.Level or 2
|
||||
local decimals = 10 ^ (dataStore.__public.Metadata.Compress.Decimals or 3)
|
||||
local safety = if dataStore.__public.Metadata.Compress.Safety == nil then true else dataStore.__public.Metadata.Compress.Safety
|
||||
dataStore.__public.CompressedValue = Compress(dataStore.__public.Value, level, decimals, safety)
|
||||
local characters = #httpService:JSONEncode(dataStore.__public.CompressedValue)
|
||||
return characters, characters / 4194303
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
DataStore.__set = {
|
||||
Metadata = function(proxy, dataStore, value)
|
||||
if type(value) ~= "table" then error("Attempt to set Metadata failed: Passed value is not a table", 3) end
|
||||
dataStore.__public.Metadata = value
|
||||
end,
|
||||
UserIds = function(proxy, dataStore, value)
|
||||
if type(value) ~= "table" then error("Attempt to set UserIds failed: Passed value is not a table", 3) end
|
||||
dataStore.__public.UserIds = value
|
||||
end,
|
||||
SaveInterval = function(proxy, dataStore, value)
|
||||
if type(value) ~= "number" then error("Attempt to set SaveInterval failed: Passed value is not a number", 3) end
|
||||
if value < 10 and value ~= 0 then error("Attempt to set SaveInterval failed: Passed value is less then 10 and not 0", 3) end
|
||||
if value > 1000 then error("Attempt to set SaveInterval failed: Passed value is more then 1000", 3) end
|
||||
if value == dataStore.__public.SaveInterval then return end
|
||||
dataStore.__public.SaveInterval = value
|
||||
if dataStore.__public.State ~= true then return end
|
||||
if value == 0 then
|
||||
StopSaveTimer(dataStore)
|
||||
elseif dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then
|
||||
StartSaveTimer(proxy)
|
||||
end
|
||||
end,
|
||||
SaveDelay = function(proxy, dataStore, value)
|
||||
if type(value) ~= "number" then error("Attempt to set SaveDelay failed: Passed value is not a number", 3) end
|
||||
if value < 0 then error("Attempt to set SaveDelay failed: Passed value is less then 0", 3) end
|
||||
if value > 10 then error("Attempt to set SaveDelay failed: Passed value is more then 10", 3) end
|
||||
dataStore.__public.SaveDelay = value
|
||||
end,
|
||||
LockInterval = function(proxy, dataStore, value)
|
||||
if type(value) ~= "number" then error("Attempt to set LockInterval failed: Passed value is not a number", 3) end
|
||||
if value < 10 then error("Attempt to set LockInterval failed: Passed value is less then 10", 3) end
|
||||
if value > 1000 then error("Attempt to set LockInterval failed: Passed value is more then 1000", 3) end
|
||||
dataStore.__public.LockInterval = value
|
||||
end,
|
||||
LockAttempts = function(proxy, dataStore, value)
|
||||
if type(value) ~= "number" then error("Attempt to set LockAttempts failed: Passed value is not a number", 3) end
|
||||
if value < 1 then error("Attempt to set LockAttempts failed: Passed value is less then 1", 3) end
|
||||
if value > 100 then error("Attempt to set LockAttempts failed: Passed value is more then 100", 3) end
|
||||
dataStore.__public.LockAttempts = value
|
||||
end,
|
||||
SaveOnClose = function(proxy, dataStore, value)
|
||||
if type(value) ~= "boolean" then error("Attempt to set SaveOnClose failed: Passed value is not a boolean", 3) end
|
||||
dataStore.__public.SaveOnClose = value
|
||||
end,
|
||||
Id = false,
|
||||
UniqueId = false,
|
||||
Key = false,
|
||||
State = false,
|
||||
Hidden = false,
|
||||
AttemptsRemaining = false,
|
||||
CreatedTime = false,
|
||||
UpdatedTime = false,
|
||||
Version = false,
|
||||
CompressedValue = false,
|
||||
StateChanged = false,
|
||||
Saving = false,
|
||||
Saved = false,
|
||||
AttemptsChanged = false,
|
||||
ProcessQueue = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-- Functions
|
||||
OpenTask = function(runningTask, proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
local response, responseData = Lock(dataStore, 3)
|
||||
if response ~= "Success" then for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end return end
|
||||
local response, responseData = Load(dataStore, 3)
|
||||
if response ~= "Success" then Unlock(dataStore, 3) for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end return end
|
||||
dataStore.__public.State = true
|
||||
if dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then
|
||||
StartSaveTimer(proxy)
|
||||
StartLockTimer(proxy)
|
||||
end
|
||||
for thread, template in runningTask:Iterate() do
|
||||
if dataStore.__public.Value == nil then
|
||||
dataStore.__public.Value = Clone(template)
|
||||
elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
|
||||
Reconcile(dataStore.__public.Value, template)
|
||||
end
|
||||
task.defer(thread, response)
|
||||
end
|
||||
if dataStore.ProcessingQueue == false and dataStore.__public.ProcessQueue.Connections > 0 then task.defer(ProcessQueue, proxy) end
|
||||
dataStore.__public.StateChanged:Fire(true, proxy)
|
||||
end
|
||||
|
||||
ReadTask = function(runningTask, proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.__public.State == true then for thread in runningTask:Iterate() do task.defer(thread, "State", "Open") end return end
|
||||
local response, responseData = Load(dataStore, 3)
|
||||
if response ~= "Success" then for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end return end
|
||||
for thread, template in runningTask:Iterate() do
|
||||
if dataStore.__public.Value == nil then
|
||||
dataStore.__public.Value = Clone(template)
|
||||
elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
|
||||
Reconcile(dataStore.__public.Value, template)
|
||||
end
|
||||
task.defer(thread, response)
|
||||
end
|
||||
end
|
||||
|
||||
LockTask = function(runningTask, proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
local attemptsRemaining = dataStore.__public.AttemptsRemaining
|
||||
local response, responseData = Lock(dataStore, 3)
|
||||
if response ~= "Success" then dataStore.__public.AttemptsRemaining -= 1 end
|
||||
if dataStore.__public.AttemptsRemaining ~= attemptsRemaining then dataStore.__public.AttemptsChanged:Fire(dataStore.__public.AttemptsRemaining, proxy) end
|
||||
if dataStore.__public.AttemptsRemaining > 0 then
|
||||
if dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then StartLockTimer(proxy) end
|
||||
else
|
||||
dataStore.__public.State = false
|
||||
StopLockTimer(dataStore)
|
||||
StopSaveTimer(dataStore)
|
||||
if dataStore.__public.SaveOnClose == true then Save(proxy, 3) end
|
||||
Unlock(dataStore, 3)
|
||||
dataStore.__public.StateChanged:Fire(false, proxy)
|
||||
end
|
||||
for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
|
||||
end
|
||||
|
||||
SaveTask = function(runningTask, proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.__public.State == false then for thread in runningTask:Iterate() do task.defer(thread, "State", "Closed") end return end
|
||||
StopSaveTimer(dataStore)
|
||||
runningTask:End()
|
||||
local response, responseData = Save(proxy, 3)
|
||||
if dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then StartSaveTimer(proxy) end
|
||||
for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
|
||||
end
|
||||
|
||||
CloseTask = function(runningTask, proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.__public.State == false then for thread in runningTask:Iterate() do task.defer(thread, "Success") end return end
|
||||
dataStore.__public.State = false
|
||||
local response, responseData = nil, nil
|
||||
if dataStore.__public.SaveOnClose == true then response, responseData = Save(proxy, 3) end
|
||||
Unlock(dataStore, 3)
|
||||
dataStore.__public.StateChanged:Fire(false, proxy)
|
||||
if response == "Saved" then
|
||||
for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
|
||||
else
|
||||
for thread in runningTask:Iterate() do task.defer(thread, "Success") end
|
||||
end
|
||||
end
|
||||
|
||||
DestroyTask = function(runningTask, proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
local response, responseData = nil, nil
|
||||
if dataStore.__public.State == false then
|
||||
dataStore.__public.State = nil
|
||||
else
|
||||
dataStore.__public.State = nil
|
||||
if dataStore.__public.SaveOnClose == true then response, responseData = Save(proxy, 3) end
|
||||
Unlock(dataStore, 3)
|
||||
end
|
||||
dataStore.__public.StateChanged:Fire(nil, proxy)
|
||||
dataStore.__public.StateChanged:DisconnectAll()
|
||||
dataStore.__public.Saving:DisconnectAll()
|
||||
dataStore.__public.Saved:DisconnectAll()
|
||||
dataStore.__public.AttemptsChanged:DisconnectAll()
|
||||
dataStore.__public.ProcessQueue:DisconnectAll()
|
||||
bindToClose[dataStore.__public.UniqueId] = nil
|
||||
if response == "Saved" then
|
||||
for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
|
||||
else
|
||||
for thread in runningTask:Iterate() do task.defer(thread, "Success") end
|
||||
end
|
||||
end
|
||||
|
||||
Lock = function(dataStore, attempts)
|
||||
local success, value, id, lockTime, lockInterval, lockAttempts = nil, nil, nil, nil, dataStore.__public.LockInterval, dataStore.__public.LockAttempts
|
||||
for i = 1, attempts do
|
||||
if i > 1 then task.wait(1) end
|
||||
lockTime = os.clock()
|
||||
success, value = pcall(dataStore.MemoryStore.UpdateAsync, dataStore.MemoryStore, "Id", function(value) id = value return if id == nil or id == dataStore.__public.UniqueId then dataStore.__public.UniqueId else nil end, lockInterval * lockAttempts + 30)
|
||||
if success == true then break end
|
||||
end
|
||||
if success == false then return "Error", value end
|
||||
if value == nil then return "Locked", id end
|
||||
dataStore.LockTime = lockTime + lockInterval * lockAttempts
|
||||
dataStore.ActiveLockInterval = lockInterval
|
||||
dataStore.__public.AttemptsRemaining = lockAttempts
|
||||
return "Success"
|
||||
end
|
||||
|
||||
Unlock = function(dataStore, attempts)
|
||||
local success, value, id = nil, nil, nil
|
||||
for i = 1, attempts do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, value = pcall(dataStore.MemoryStore.UpdateAsync, dataStore.MemoryStore, "Id", function(value) id = value return if id == dataStore.__public.UniqueId then dataStore.__public.UniqueId else nil end, 0)
|
||||
if success == true then break end
|
||||
end
|
||||
if success == false then return "Error", value end
|
||||
if value == nil and id ~= nil then return "Locked", id end
|
||||
return "Success"
|
||||
end
|
||||
|
||||
Load = function(dataStore, attempts)
|
||||
local success, value, info = nil, nil, nil
|
||||
for i = 1, attempts do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, value, info = pcall(dataStore.DataStore.GetAsync, dataStore.DataStore, dataStore.__public.Key)
|
||||
if success == true then break end
|
||||
end
|
||||
if success == false then return "Error", value end
|
||||
if info == nil then
|
||||
dataStore.__public.Metadata, dataStore.__public.UserIds, dataStore.__public.CreatedTime, dataStore.__public.UpdatedTime, dataStore.__public.Version = {}, {}, 0, 0, ""
|
||||
else
|
||||
dataStore.__public.Metadata, dataStore.__public.UserIds, dataStore.__public.CreatedTime, dataStore.__public.UpdatedTime, dataStore.__public.Version = info:GetMetadata(), info:GetUserIds(), info.CreatedTime, info.UpdatedTime, info.Version
|
||||
end
|
||||
if type(dataStore.__public.Metadata.Compress) ~= "table" then
|
||||
dataStore.__public.Value = value
|
||||
else
|
||||
dataStore.__public.CompressedValue = value
|
||||
local decimals = 10 ^ (dataStore.__public.Metadata.Compress.Decimals or 3)
|
||||
dataStore.__public.Value = Decompress(dataStore.__public.CompressedValue, decimals)
|
||||
end
|
||||
return "Success"
|
||||
end
|
||||
|
||||
Save = function(proxy, attempts)
|
||||
local dataStore = getmetatable(proxy)
|
||||
local deltaTime = os.clock() - dataStore.SaveTime
|
||||
if deltaTime < dataStore.__public.SaveDelay then task.wait(dataStore.__public.SaveDelay - deltaTime) end
|
||||
dataStore.__public.Saving:Fire(dataStore.__public.Value, proxy)
|
||||
local success, value, info = nil, nil, nil
|
||||
if dataStore.__public.Value == nil then
|
||||
for i = 1, attempts do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, value, info = pcall(dataStore.DataStore.RemoveAsync, dataStore.DataStore, dataStore.__public.Key)
|
||||
if success == true then break end
|
||||
end
|
||||
if success == false then dataStore.__public.Saved:Fire("Error", value, proxy) return "Error", value end
|
||||
dataStore.__public.Metadata, dataStore.__public.UserIds, dataStore.__public.CreatedTime, dataStore.__public.UpdatedTime, dataStore.__public.Version = {}, {}, 0, 0, ""
|
||||
elseif type(dataStore.__public.Metadata.Compress) ~= "table" then
|
||||
dataStore.Options:SetMetadata(dataStore.__public.Metadata)
|
||||
for i = 1, attempts do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, value = pcall(dataStore.DataStore.SetAsync, dataStore.DataStore, dataStore.__public.Key, dataStore.__public.Value, dataStore.__public.UserIds, dataStore.Options)
|
||||
if success == true then break end
|
||||
end
|
||||
if success == false then dataStore.__public.Saved:Fire("Error", value, proxy) return "Error", value end
|
||||
dataStore.__public.Version = value
|
||||
else
|
||||
local level = dataStore.__public.Metadata.Compress.Level or 2
|
||||
local decimals = 10 ^ (dataStore.__public.Metadata.Compress.Decimals or 3)
|
||||
local safety = if dataStore.__public.Metadata.Compress.Safety == nil then true else dataStore.__public.Metadata.Compress.Safety
|
||||
dataStore.__public.CompressedValue = Compress(dataStore.__public.Value, level, decimals, safety)
|
||||
dataStore.Options:SetMetadata(dataStore.__public.Metadata)
|
||||
for i = 1, attempts do
|
||||
if i > 1 then task.wait(1) end
|
||||
success, value = pcall(dataStore.DataStore.SetAsync, dataStore.DataStore, dataStore.__public.Key, dataStore.__public.CompressedValue, dataStore.__public.UserIds, dataStore.Options)
|
||||
if success == true then break end
|
||||
end
|
||||
if success == false then dataStore.__public.Saved:Fire("Error", value, proxy) return "Error", value end
|
||||
dataStore.Version = value
|
||||
end
|
||||
dataStore.SaveTime = os.clock()
|
||||
dataStore.__public.Saved:Fire("Saved", dataStore.__public.Value, proxy)
|
||||
return "Saved", dataStore.__public.Value
|
||||
end
|
||||
|
||||
StartSaveTimer = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.SaveThread ~= nil then task.cancel(dataStore.SaveThread) end
|
||||
if dataStore.__public.SaveInterval == 0 then return end
|
||||
dataStore.SaveThread = task.delay(dataStore.__public.SaveInterval, SaveTimerEnded, proxy)
|
||||
end
|
||||
|
||||
StopSaveTimer = function(dataStore)
|
||||
if dataStore.SaveThread == nil then return end
|
||||
task.cancel(dataStore.SaveThread)
|
||||
dataStore.SaveThread = nil
|
||||
end
|
||||
|
||||
SaveTimerEnded = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
dataStore.SaveThread = nil
|
||||
if dataStore.TaskManager:FindLast(SaveTask) ~= nil then return end
|
||||
dataStore.TaskManager:InsertBack(SaveTask, proxy)
|
||||
end
|
||||
|
||||
StartLockTimer = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.LockThread ~= nil then task.cancel(dataStore.LockThread) end
|
||||
local startTime = dataStore.LockTime - dataStore.__public.AttemptsRemaining * dataStore.ActiveLockInterval
|
||||
dataStore.LockThread = task.delay(startTime - os.clock() + dataStore.ActiveLockInterval, LockTimerEnded, proxy)
|
||||
end
|
||||
|
||||
StopLockTimer = function(dataStore)
|
||||
if dataStore.LockThread == nil then return end
|
||||
task.cancel(dataStore.LockThread)
|
||||
dataStore.LockThread = nil
|
||||
end
|
||||
|
||||
LockTimerEnded = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
dataStore.LockThread = nil
|
||||
if dataStore.TaskManager:FindFirst(LockTask) ~= nil then return end
|
||||
dataStore.TaskManager:InsertBack(LockTask, proxy)
|
||||
end
|
||||
|
||||
ProcessQueue = function(proxy)
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.__public.State ~= true then return end
|
||||
if dataStore.__public.ProcessQueue.Connections == 0 then return end
|
||||
if dataStore.ProcessingQueue == true then return end
|
||||
dataStore.ProcessingQueue = true
|
||||
while true do
|
||||
local success, values, id = pcall(dataStore.Queue.ReadAsync, dataStore.Queue, 100, false, 30)
|
||||
if dataStore.__public.State ~= true then break end
|
||||
if dataStore.__public.ProcessQueue.Connections == 0 then break end
|
||||
if success == true and id ~= nil then dataStore.__public.ProcessQueue:Fire(id, values, proxy) end
|
||||
end
|
||||
dataStore.ProcessingQueue = false
|
||||
end
|
||||
|
||||
SignalConnected = function(connected, signal)
|
||||
if connected == false then return end
|
||||
ProcessQueue(signal.DataStore)
|
||||
end
|
||||
|
||||
Clone = function(original)
|
||||
if type(original) ~= "table" then return original end
|
||||
local clone = {}
|
||||
for index, value in original do clone[index] = Clone(value) end
|
||||
return clone
|
||||
end
|
||||
|
||||
Reconcile = function(target, template)
|
||||
for index, value in template do
|
||||
if type(index) == "number" then continue end
|
||||
if target[index] == nil then
|
||||
target[index] = Clone(value)
|
||||
elseif type(target[index]) == "table" and type(value) == "table" then
|
||||
Reconcile(target[index], value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Compress = function(value, level, decimals, safety)
|
||||
local data = {}
|
||||
if type(value) == "boolean" then
|
||||
table.insert(data, if value == false then "-" else "+")
|
||||
elseif type(value) == "number" then
|
||||
if value % 1 == 0 then
|
||||
table.insert(data, if value < 0 then "<" .. Encode(-value) else ">" .. Encode(value))
|
||||
else
|
||||
table.insert(data, if value < 0 then "(" .. Encode(math.round(-value * decimals)) else ")" .. Encode(math.round(value * decimals)))
|
||||
end
|
||||
elseif type(value) == "string" then
|
||||
if safety == true then value = value:gsub("", " ") end
|
||||
table.insert(data, "#" .. value .. "")
|
||||
elseif type(value) == "table" then
|
||||
if #value > 0 and level == 2 then
|
||||
table.insert(data, "|")
|
||||
for i = 1, #value do table.insert(data, Compress(value[i], level, decimals, safety)) end
|
||||
table.insert(data, "")
|
||||
else
|
||||
table.insert(data, "*")
|
||||
for key, tableValue in value do table.insert(data, Compress(key, level, decimals, safety)) table.insert(data, Compress(tableValue, level, decimals, safety)) end
|
||||
table.insert(data, "")
|
||||
end
|
||||
end
|
||||
return table.concat(data)
|
||||
end
|
||||
|
||||
Decompress = function(value, decimals, index)
|
||||
local i1, i2, dataType, data = value:find("([-+<>()#|*])", index or 1)
|
||||
if dataType == "-" then
|
||||
return false, i2
|
||||
elseif dataType == "+" then
|
||||
return true, i2
|
||||
elseif dataType == "<" then
|
||||
i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
|
||||
return -Decode(data), i2
|
||||
elseif dataType == ">" then
|
||||
i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
|
||||
return Decode(data), i2
|
||||
elseif dataType == "(" then
|
||||
i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
|
||||
return -Decode(data) / decimals, i2
|
||||
elseif dataType == ")" then
|
||||
i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
|
||||
return Decode(data) / decimals, i2
|
||||
elseif dataType == "#" then
|
||||
i1, i2, data = value:find("(.-)", i2 + 1)
|
||||
return data, i2
|
||||
elseif dataType == "|" then
|
||||
local array = {}
|
||||
while true do
|
||||
data, i2 = Decompress(value, decimals, i2 + 1)
|
||||
if data == nil then break end
|
||||
table.insert(array, data)
|
||||
end
|
||||
return array, i2
|
||||
elseif dataType == "*" then
|
||||
local dictionary, key = {}, nil
|
||||
while true do
|
||||
key, i2 = Decompress(value, decimals, i2 + 1)
|
||||
if key == nil then break end
|
||||
data, i2 = Decompress(value, decimals, i2 + 1)
|
||||
dictionary[key] = data
|
||||
end
|
||||
return dictionary, i2
|
||||
end
|
||||
return nil, i2
|
||||
end
|
||||
|
||||
Encode = function(value)
|
||||
if value == 0 then return "0" end
|
||||
local data = {}
|
||||
while value > 0 do
|
||||
table.insert(data, characters[value % base])
|
||||
value = math.floor(value / base)
|
||||
end
|
||||
return table.concat(data)
|
||||
end
|
||||
|
||||
Decode = function(value)
|
||||
local number, power, data = 0, 1, {string.byte(value, 1, #value)}
|
||||
for i, code in data do
|
||||
number += bytes[code] * power
|
||||
power *= base
|
||||
end
|
||||
return number
|
||||
end
|
||||
|
||||
BindToClose = function()
|
||||
active = false
|
||||
for uniqueId, proxy in bindToClose do
|
||||
local dataStore = getmetatable(proxy)
|
||||
if dataStore.__public.State == nil then continue end
|
||||
dataStores[dataStore.__public.Id] = nil
|
||||
StopLockTimer(dataStore)
|
||||
StopSaveTimer(dataStore)
|
||||
if dataStore.TaskManager:FindFirst(DestroyTask) == nil then dataStore.TaskManager:InsertBack(DestroyTask, proxy) end
|
||||
end
|
||||
while next(bindToClose) ~= nil do task.wait() end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-- Events
|
||||
game:BindToClose(BindToClose)
|
||||
|
||||
|
||||
|
||||
|
||||
return table.freeze(Constructor) :: Constructor
|
3
src/Chemical/Packages/Datastore/init.meta.json
Normal file
3
src/Chemical/Packages/Datastore/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
2716
src/Chemical/Packages/JECS.lua
Normal file
2716
src/Chemical/Packages/JECS.lua
Normal file
File diff suppressed because it is too large
Load diff
160
src/Chemical/Packages/LinkedList.lua
Normal file
160
src/Chemical/Packages/LinkedList.lua
Normal file
|
@ -0,0 +1,160 @@
|
|||
local listClass, linkClass = {}, {}
|
||||
listClass.__index, linkClass.__index = listClass, linkClass
|
||||
|
||||
export type List<T> = typeof(listClass.new((1 :: any) :: T))
|
||||
|
||||
function listClass.new()
|
||||
local self = setmetatable({}, listClass)
|
||||
self.List = self
|
||||
self.Links = {}
|
||||
self.Length = 0
|
||||
self.Next = self
|
||||
self.Previous = self
|
||||
return self
|
||||
end
|
||||
|
||||
function listClass:InsertFront(value)
|
||||
assert(self.Links[value] == nil, "Value already exists")
|
||||
local link = {Value = value, List = self, Previous = self, Next = self.Next}
|
||||
self.Links[value] = link
|
||||
self.Length += 1
|
||||
self.Next.Previous = link
|
||||
self.Next = link
|
||||
return setmetatable(link, linkClass)
|
||||
end
|
||||
|
||||
function listClass:InsertAfter(value)
|
||||
assert(self.Links[value] == nil, "Value already exists")
|
||||
local link = {Value = value, List = self, Previous = self, Next = self.Next}
|
||||
self.Links[value] = link
|
||||
self.Length += 1
|
||||
self.Next.Previous = link
|
||||
self.Next = link
|
||||
return setmetatable(link, linkClass)
|
||||
end
|
||||
|
||||
function listClass:InsertBack(value)
|
||||
assert(self.Links[value] == nil, "Value already exists")
|
||||
local link = {Value = value, List = self, Next = self, Previous = self.Previous}
|
||||
self.Links[value] = link
|
||||
self.Length += 1
|
||||
self.Previous.Next = link
|
||||
self.Previous = link
|
||||
return setmetatable(link, linkClass)
|
||||
end
|
||||
|
||||
function listClass:InsertBefore(value)
|
||||
assert(self.Links[value] == nil, "Value already exists")
|
||||
local link = {Value = value, List = self, Next = self, Previous = self.Previous}
|
||||
self.Links[value] = link
|
||||
self.Length += 1
|
||||
self.Previous.Next = link
|
||||
self.Previous = link
|
||||
return setmetatable(link, linkClass)
|
||||
end
|
||||
|
||||
function listClass:GetNext(link)
|
||||
link = (link or self).Next
|
||||
if link ~= self then return link, link.Value end
|
||||
end
|
||||
|
||||
function listClass:GetPrevious(link)
|
||||
link = (link or self).Previous
|
||||
if link ~= self then return link, link.Value end
|
||||
end
|
||||
|
||||
function listClass:IterateForward(link)
|
||||
return listClass.GetNext, self, link
|
||||
end
|
||||
|
||||
function listClass:IterateBackward(link)
|
||||
return listClass.GetPrevious, self, link
|
||||
end
|
||||
|
||||
function listClass:Remove(value, clean: boolean?)
|
||||
local link = self.Links[value]
|
||||
if link ~= nil then
|
||||
self.Links[value] = nil
|
||||
self.Length -= 1
|
||||
link.List = nil
|
||||
link.Previous.Next = link.Next
|
||||
link.Next.Previous = link.Previous
|
||||
|
||||
if clean then setmetatable(link, nil) return end
|
||||
return link
|
||||
end
|
||||
end
|
||||
|
||||
function listClass:RemoveFirst()
|
||||
local link = self.Next
|
||||
if link ~= self then
|
||||
self.Links[link.Value] = nil
|
||||
self.Length -= 1
|
||||
link.List = nil
|
||||
link.Previous.Next = link.Next
|
||||
link.Next.Previous = link.Previous
|
||||
return link.Value, link
|
||||
end
|
||||
end
|
||||
|
||||
function listClass:RemoveLast()
|
||||
local link = self.Previous
|
||||
if link ~= self then
|
||||
self.Links[link.Value] = nil
|
||||
self.Length -= 1
|
||||
link.List = nil
|
||||
link.Previous.Next = link.Next
|
||||
link.Next.Previous = link.Previous
|
||||
return link.Value, link
|
||||
end
|
||||
end
|
||||
|
||||
function listClass:Destroy()
|
||||
local l = self.List.Length
|
||||
for link, value in self:IterateForward() do
|
||||
self:Remove(value, true)
|
||||
end
|
||||
setmetatable(self, nil)
|
||||
table.clear(self)
|
||||
end
|
||||
|
||||
function linkClass:InsertAfter(value)
|
||||
assert(self.List.Links[value] == nil, "Value already exists")
|
||||
local link = {Value = value, List = self.List, Previous = self, Next = self.Next}
|
||||
self.List.Links[value] = link
|
||||
self.List.Length += 1
|
||||
self.Next.Previous = link
|
||||
self.Next = link
|
||||
return setmetatable(link, linkClass)
|
||||
end
|
||||
|
||||
function linkClass:InsertBefore(value)
|
||||
assert(self.List.Links[value] == nil, "Value already exists")
|
||||
local link = {Value = value, List = self.List, Next = self, Previous = self.Previous}
|
||||
self.List.Links[value] = link
|
||||
self.List.Length += 1
|
||||
self.Previous.Next = link
|
||||
self.Previous = link
|
||||
return setmetatable(link, linkClass)
|
||||
end
|
||||
|
||||
function linkClass:GetNext()
|
||||
local link = self.Next
|
||||
if link ~= link.List then return link end
|
||||
end
|
||||
|
||||
function linkClass:GetPrevious()
|
||||
local link = self.Previous
|
||||
if link ~= link.List then return link end
|
||||
end
|
||||
|
||||
function linkClass:Remove()
|
||||
assert(self.List ~= nil, "Link is not in a list")
|
||||
self.List.Links[self.Value] = nil
|
||||
self.List.Length -= 1
|
||||
self.List = nil
|
||||
self.Previous.Next = self.Next
|
||||
self.Next.Previous = self.Previous
|
||||
end
|
||||
|
||||
return listClass
|
101
src/Chemical/Packages/Packet/Signal.lua
Normal file
101
src/Chemical/Packages/Packet/Signal.lua
Normal file
|
@ -0,0 +1,101 @@
|
|||
--!strict
|
||||
|
||||
|
||||
-- Requires
|
||||
local Task = require(script.Parent.Task)
|
||||
|
||||
|
||||
-- Types
|
||||
export type Signal<A... = ()> = {
|
||||
Type: "Signal",
|
||||
Previous: Connection<A...>,
|
||||
Next: Connection<A...>,
|
||||
Fire: (self: Signal<A...>, A...) -> (),
|
||||
Connect: (self: Signal<A...>, func: (A...) -> ()) -> Connection<A...>,
|
||||
Once: (self: Signal<A...>, func: (A...) -> ()) -> Connection<A...>,
|
||||
Wait: (self: Signal<A...>) -> A...,
|
||||
}
|
||||
|
||||
export type Connection<A... = ()> = {
|
||||
Type: "Connection",
|
||||
Previous: Connection<A...>,
|
||||
Next: Connection<A...>,
|
||||
Once: boolean,
|
||||
Function: (player: Player, A...) -> (),
|
||||
Thread: thread,
|
||||
Disconnect: (self: Connection<A...>) -> (),
|
||||
}
|
||||
|
||||
|
||||
-- Varables
|
||||
local Signal = {} :: Signal<...any>
|
||||
local Connection = {} :: Connection<...any>
|
||||
|
||||
|
||||
-- Constructor
|
||||
local function Constructor<A...>()
|
||||
local signal = (setmetatable({}, Signal) :: any) :: Signal<A...>
|
||||
signal.Previous = signal :: any
|
||||
signal.Next = signal :: any
|
||||
return signal
|
||||
end
|
||||
|
||||
|
||||
-- Signal
|
||||
Signal["__index"] = Signal
|
||||
Signal.Type = "Signal"
|
||||
|
||||
function Signal:Connect(func)
|
||||
local connection = (setmetatable({}, Connection) :: any) :: Connection
|
||||
connection.Previous = self.Previous
|
||||
connection.Next = self :: any
|
||||
connection.Once = false
|
||||
connection.Function = func
|
||||
self.Previous.Next = connection
|
||||
self.Previous = connection
|
||||
return connection
|
||||
end
|
||||
|
||||
function Signal:Once(func)
|
||||
local connection = (setmetatable({}, Connection) :: any) :: Connection
|
||||
connection.Previous = self.Previous
|
||||
connection.Next = self :: any
|
||||
connection.Once = true
|
||||
connection.Function = func
|
||||
self.Previous.Next = connection
|
||||
self.Previous = connection
|
||||
return connection
|
||||
end
|
||||
|
||||
function Signal:Wait()
|
||||
local connection = (setmetatable({}, Connection) :: any) :: Connection
|
||||
connection.Previous = self.Previous
|
||||
connection.Next = self :: any
|
||||
connection.Once = true
|
||||
connection.Thread = coroutine.running()
|
||||
self.Previous.Next = connection
|
||||
self.Previous = connection
|
||||
return coroutine.yield()
|
||||
end
|
||||
|
||||
function Signal:Fire(...)
|
||||
local connection = self.Next
|
||||
while connection.Type == "Connection" do
|
||||
if connection.Function then Task:Defer(connection.Function, ...) else task.defer(connection.Thread, ...) end
|
||||
if connection.Once then connection.Previous.Next = connection.Next connection.Next.Previous = connection.Previous end
|
||||
connection = connection.Next
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Connection
|
||||
Connection["__index"] = Connection
|
||||
Connection.Type = "Connection"
|
||||
|
||||
function Connection:Disconnect()
|
||||
self.Previous.Next = self.Next
|
||||
self.Next.Previous = self.Previous
|
||||
end
|
||||
|
||||
|
||||
return Constructor
|
46
src/Chemical/Packages/Packet/Task.lua
Normal file
46
src/Chemical/Packages/Packet/Task.lua
Normal file
|
@ -0,0 +1,46 @@
|
|||
--!strict
|
||||
|
||||
|
||||
-- Types
|
||||
export type Task = {
|
||||
Type: "Task",
|
||||
Spawn: (self: Task, func: (...any) -> (), ...any) -> thread,
|
||||
Defer: (self: Task, func: (...any) -> (), ...any) -> thread,
|
||||
Delay: (self: Task, duration: number, func: (...any) -> (), ...any) -> thread,
|
||||
}
|
||||
|
||||
|
||||
-- Varables
|
||||
local Call, Thread
|
||||
local Task = {} :: Task
|
||||
local threads = {} :: {thread}
|
||||
|
||||
|
||||
-- Task
|
||||
Task.Type = "Task"
|
||||
|
||||
function Task:Spawn(func, ...)
|
||||
return task.spawn(table.remove(threads) or task.spawn(Thread), func, ...)
|
||||
end
|
||||
|
||||
function Task:Defer(func, ...)
|
||||
return task.defer(table.remove(threads) or task.spawn(Thread), func, ...)
|
||||
end
|
||||
|
||||
function Task:Delay(duration, func, ...)
|
||||
return task.delay(duration, table.remove(threads) or task.spawn(Thread), func, ...)
|
||||
end
|
||||
|
||||
|
||||
-- Functions
|
||||
function Call(func: (...any) -> (), ...)
|
||||
func(...)
|
||||
table.insert(threads, coroutine.running())
|
||||
end
|
||||
|
||||
function Thread()
|
||||
while true do Call(coroutine.yield()) end
|
||||
end
|
||||
|
||||
|
||||
return Task
|
6
src/Chemical/Packages/Packet/Types/Characters.lua
Normal file
6
src/Chemical/Packages/Packet/Types/Characters.lua
Normal file
|
@ -0,0 +1,6 @@
|
|||
return {[0] = -- Recommended character array lengths: 2, 4, 8, 16, 32, 64, 128, 256
|
||||
" ", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D",
|
||||
"E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
||||
}
|
12
src/Chemical/Packages/Packet/Types/Enums.lua
Normal file
12
src/Chemical/Packages/Packet/Types/Enums.lua
Normal file
|
@ -0,0 +1,12 @@
|
|||
return { -- Add any enum [Max: 255]
|
||||
Enum.AccessoryType,
|
||||
Enum.Axis,
|
||||
Enum.BodyPart,
|
||||
Enum.BodyPartR15,
|
||||
Enum.EasingDirection,
|
||||
Enum.EasingStyle,
|
||||
Enum.KeyCode,
|
||||
Enum.Material,
|
||||
Enum.NormalId,
|
||||
Enum.HumanoidStateType
|
||||
}
|
8
src/Chemical/Packages/Packet/Types/Static1.lua
Normal file
8
src/Chemical/Packages/Packet/Types/Static1.lua
Normal file
|
@ -0,0 +1,8 @@
|
|||
return {
|
||||
"DataStore Failed To Load",
|
||||
"Another Static String",
|
||||
math.pi,
|
||||
123456789,
|
||||
Vector3.new(1, 2, 3),
|
||||
"You can have upto 255 static values of any type"
|
||||
}
|
8
src/Chemical/Packages/Packet/Types/Static2.lua
Normal file
8
src/Chemical/Packages/Packet/Types/Static2.lua
Normal file
|
@ -0,0 +1,8 @@
|
|||
return {
|
||||
"DataStore Failed To Load",
|
||||
"Another Static String",
|
||||
math.pi,
|
||||
123456789,
|
||||
Vector3.new(1, 2, 3),
|
||||
"You can have upto 255 static values of any type"
|
||||
}
|
8
src/Chemical/Packages/Packet/Types/Static3.lua
Normal file
8
src/Chemical/Packages/Packet/Types/Static3.lua
Normal file
|
@ -0,0 +1,8 @@
|
|||
return {
|
||||
"DataStore Failed To Load",
|
||||
"Another Static String",
|
||||
math.pi,
|
||||
123456789,
|
||||
Vector3.new(1, 2, 3),
|
||||
"You can have upto 255 static values of any type"
|
||||
}
|
705
src/Chemical/Packages/Packet/Types/init.lua
Normal file
705
src/Chemical/Packages/Packet/Types/init.lua
Normal file
|
@ -0,0 +1,705 @@
|
|||
--!strict
|
||||
--!optimize 2
|
||||
|
||||
--[[
|
||||
S8 Minimum: -128 Maximum: 127
|
||||
S16 Minimum: -32768 Maximum: 32767
|
||||
S24 Minimum: -8388608 Maximum: 8388607
|
||||
S32 Minimum: -2147483648 Maximum: 2147483647
|
||||
|
||||
U8 Minimum: 0 Maximum: 255
|
||||
U16 Minimum: 0 Maximum: 65535
|
||||
U24 Minimum: 0 Maximum: 16777215
|
||||
U32 Minimum: 0 Maximum: 4294967295
|
||||
|
||||
F16 ±2048 [65520]
|
||||
F24 ±262144 [4294959104]
|
||||
F32 ±16777216 [170141183460469231731687303715884105728]
|
||||
F64 ±9007199254740992 [huge]
|
||||
]]
|
||||
|
||||
|
||||
-- Types
|
||||
export type Cursor = {
|
||||
Buffer: buffer,
|
||||
BufferLength: number,
|
||||
BufferOffset: number,
|
||||
Instances: {Instance},
|
||||
InstancesOffset: number,
|
||||
}
|
||||
|
||||
|
||||
-- Varables
|
||||
local activeCursor : Cursor
|
||||
local activeBuffer : buffer
|
||||
local bufferLength : number
|
||||
local bufferOffset : number
|
||||
local instances : {Instance}
|
||||
local instancesOffset : number
|
||||
local types = {}
|
||||
local reads = {}
|
||||
local writes = {}
|
||||
local anyReads = {} :: {[any]: () -> any}
|
||||
local anyWrites = {} :: {[any]: (any) -> ()}
|
||||
|
||||
|
||||
-- Functions
|
||||
local function Allocate(bytes: number)
|
||||
local targetLength = bufferOffset + bytes
|
||||
if bufferLength < targetLength then
|
||||
while bufferLength < targetLength do bufferLength *= 2 end
|
||||
local newBuffer = buffer.create(bufferLength)
|
||||
buffer.copy(newBuffer, 0, activeBuffer, 0, bufferOffset)
|
||||
activeCursor.Buffer = newBuffer
|
||||
activeBuffer = newBuffer
|
||||
end
|
||||
end
|
||||
|
||||
local function ReadS8(): number local value = buffer.readi8(activeBuffer, bufferOffset) bufferOffset += 1 return value end
|
||||
local function WriteS8(value: number) buffer.writei8(activeBuffer, bufferOffset, value) bufferOffset += 1 end
|
||||
local function ReadS16(): number local value = buffer.readi16(activeBuffer, bufferOffset) bufferOffset += 2 return value end
|
||||
local function WriteS16(value: number) buffer.writei16(activeBuffer, bufferOffset, value) bufferOffset += 2 end
|
||||
local function ReadS24(): number local value = buffer.readbits(activeBuffer, bufferOffset * 8, 24) - 8388608 bufferOffset += 3 return value end
|
||||
local function WriteS24(value: number) buffer.writebits(activeBuffer, bufferOffset * 8, 24, value + 8388608) bufferOffset += 3 end
|
||||
local function ReadS32(): number local value = buffer.readi32(activeBuffer, bufferOffset) bufferOffset += 4 return value end
|
||||
local function WriteS32(value: number) buffer.writei32(activeBuffer, bufferOffset, value) bufferOffset += 4 end
|
||||
local function ReadU8(): number local value = buffer.readu8(activeBuffer, bufferOffset) bufferOffset += 1 return value end
|
||||
local function WriteU8(value: number) buffer.writeu8(activeBuffer, bufferOffset, value) bufferOffset += 1 end
|
||||
local function ReadU16(): number local value = buffer.readu16(activeBuffer, bufferOffset) bufferOffset += 2 return value end
|
||||
local function WriteU16(value: number) buffer.writeu16(activeBuffer, bufferOffset, value) bufferOffset += 2 end
|
||||
local function ReadU24(): number local value = buffer.readbits(activeBuffer, bufferOffset * 8, 24) bufferOffset += 3 return value end
|
||||
local function WriteU24(value: number) buffer.writebits(activeBuffer, bufferOffset * 8, 24, value) bufferOffset += 3 end
|
||||
local function ReadU32(): number local value = buffer.readu32(activeBuffer, bufferOffset) bufferOffset += 4 return value end
|
||||
local function WriteU32(value: number) buffer.writeu32(activeBuffer, bufferOffset, value) bufferOffset += 4 end
|
||||
local function ReadF32(): number local value = buffer.readf32(activeBuffer, bufferOffset) bufferOffset += 4 return value end
|
||||
local function WriteF32(value: number) buffer.writef32(activeBuffer, bufferOffset, value) bufferOffset += 4 end
|
||||
local function ReadF64(): number local value = buffer.readf64(activeBuffer, bufferOffset) bufferOffset += 8 return value end
|
||||
local function WriteF64(value: number) buffer.writef64(activeBuffer, bufferOffset, value) bufferOffset += 8 end
|
||||
local function ReadString(length: number) local value = buffer.readstring(activeBuffer, bufferOffset, length) bufferOffset += length return value end
|
||||
local function WriteString(value: string) buffer.writestring(activeBuffer, bufferOffset, value) bufferOffset += #value end
|
||||
local function ReadBuffer(length: number) local value = buffer.create(length) buffer.copy(value, 0, activeBuffer, bufferOffset, length) bufferOffset += length return value end
|
||||
local function WriteBuffer(value: buffer) buffer.copy(activeBuffer, bufferOffset, value) bufferOffset += buffer.len(value) end
|
||||
local function ReadInstance() instancesOffset += 1 return instances[instancesOffset] end
|
||||
local function WriteInstance(value) instancesOffset += 1 instances[instancesOffset] = value end
|
||||
|
||||
local function ReadF16(): number
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 2
|
||||
local mantissa = buffer.readbits(activeBuffer, bitOffset + 0, 10)
|
||||
local exponent = buffer.readbits(activeBuffer, bitOffset + 10, 5)
|
||||
local sign = buffer.readbits(activeBuffer, bitOffset + 15, 1)
|
||||
if mantissa == 0b0000000000 then
|
||||
if exponent == 0b00000 then return 0 end
|
||||
if exponent == 0b11111 then return if sign == 0 then math.huge else -math.huge end
|
||||
elseif exponent == 0b11111 then return 0/0 end
|
||||
if sign == 0 then
|
||||
return (mantissa / 1024 + 1) * 2 ^ (exponent - 15)
|
||||
else
|
||||
return -(mantissa / 1024 + 1) * 2 ^ (exponent - 15)
|
||||
end
|
||||
end
|
||||
local function WriteF16(value: number)
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 2
|
||||
if value == 0 then
|
||||
buffer.writebits(activeBuffer, bitOffset, 16, 0b0_00000_0000000000)
|
||||
elseif value >= 65520 then
|
||||
buffer.writebits(activeBuffer, bitOffset, 16, 0b0_11111_0000000000)
|
||||
elseif value <= -65520 then
|
||||
buffer.writebits(activeBuffer, bitOffset, 16, 0b1_11111_0000000000)
|
||||
elseif value ~= value then
|
||||
buffer.writebits(activeBuffer, bitOffset, 16, 0b0_11111_0000000001)
|
||||
else
|
||||
local sign = 0
|
||||
if value < 0 then sign = 1 value = -value end
|
||||
local mantissa, exponent = math.frexp(value)
|
||||
buffer.writebits(activeBuffer, bitOffset + 0, 10, mantissa * 2048 - 1023.5)
|
||||
buffer.writebits(activeBuffer, bitOffset + 10, 5, exponent + 14)
|
||||
buffer.writebits(activeBuffer, bitOffset + 15, 1, sign)
|
||||
end
|
||||
end
|
||||
|
||||
local function ReadF24(): number
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 3
|
||||
local mantissa = buffer.readbits(activeBuffer, bitOffset + 0, 17)
|
||||
local exponent = buffer.readbits(activeBuffer, bitOffset + 17, 6)
|
||||
local sign = buffer.readbits(activeBuffer, bitOffset + 23, 1)
|
||||
if mantissa == 0b00000000000000000 then
|
||||
if exponent == 0b000000 then return 0 end
|
||||
if exponent == 0b111111 then return if sign == 0 then math.huge else -math.huge end
|
||||
elseif exponent == 0b111111 then return 0/0 end
|
||||
if sign == 0 then
|
||||
return (mantissa / 131072 + 1) * 2 ^ (exponent - 31)
|
||||
else
|
||||
return -(mantissa / 131072 + 1) * 2 ^ (exponent - 31)
|
||||
end
|
||||
end
|
||||
local function WriteF24(value: number)
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 3
|
||||
if value == 0 then
|
||||
buffer.writebits(activeBuffer, bitOffset, 24, 0b0_000000_00000000000000000)
|
||||
elseif value >= 4294959104 then
|
||||
buffer.writebits(activeBuffer, bitOffset, 24, 0b0_111111_00000000000000000)
|
||||
elseif value <= -4294959104 then
|
||||
buffer.writebits(activeBuffer, bitOffset, 24, 0b1_111111_00000000000000000)
|
||||
elseif value ~= value then
|
||||
buffer.writebits(activeBuffer, bitOffset, 24, 0b0_111111_00000000000000001)
|
||||
else
|
||||
local sign = 0
|
||||
if value < 0 then sign = 1 value = -value end
|
||||
local mantissa, exponent = math.frexp(value)
|
||||
buffer.writebits(activeBuffer, bitOffset + 0, 17, mantissa * 262144 - 131071.5)
|
||||
buffer.writebits(activeBuffer, bitOffset + 17, 6, exponent + 30)
|
||||
buffer.writebits(activeBuffer, bitOffset + 23, 1, sign)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Types
|
||||
types.Any = "Any" :: any
|
||||
reads.Any = function() return anyReads[ReadU8()]() end
|
||||
writes.Any = function(value: any) anyWrites[typeof(value)](value) end
|
||||
|
||||
types.Nil = ("Nil" :: any) :: nil
|
||||
reads.Nil = function() return nil end
|
||||
writes.Nil = function(value: nil) end
|
||||
|
||||
types.NumberS8 = ("NumberS8" :: any) :: number
|
||||
reads.NumberS8 = function() return ReadS8() end
|
||||
writes.NumberS8 = function(value: number) Allocate(1) WriteS8(value) end
|
||||
|
||||
types.NumberS16 = ("NumberS16" :: any) :: number
|
||||
reads.NumberS16 = function() return ReadS16() end
|
||||
writes.NumberS16 = function(value: number) Allocate(2) WriteS16(value) end
|
||||
|
||||
types.NumberS24 = ("NumberS24" :: any) :: number
|
||||
reads.NumberS24 = function() return ReadS24() end
|
||||
writes.NumberS24 = function(value: number) Allocate(3) WriteS24(value) end
|
||||
|
||||
types.NumberS32 = ("NumberS32" :: any) :: number
|
||||
reads.NumberS32 = function() return ReadS32() end
|
||||
writes.NumberS32 = function(value: number) Allocate(4) WriteS32(value) end
|
||||
|
||||
types.NumberU8 = ("NumberU8" :: any) :: number
|
||||
reads.NumberU8 = function() return ReadU8() end
|
||||
writes.NumberU8 = function(value: number) Allocate(1) WriteU8(value) end
|
||||
|
||||
types.NumberU16 = ("NumberU16" :: any) :: number
|
||||
reads.NumberU16 = function() return ReadU16() end
|
||||
writes.NumberU16 = function(value: number) Allocate(2) WriteU16(value) end
|
||||
|
||||
types.NumberU24 = ("NumberU24" :: any) :: number
|
||||
reads.NumberU24 = function() return ReadU24() end
|
||||
writes.NumberU24 = function(value: number) Allocate(3) WriteU24(value) end
|
||||
|
||||
types.NumberU32 = ("NumberU32" :: any) :: number
|
||||
reads.NumberU32 = function() return ReadU32() end
|
||||
writes.NumberU32 = function(value: number) Allocate(4) WriteU32(value) end
|
||||
|
||||
types.NumberF16 = ("NumberF16" :: any) :: number
|
||||
reads.NumberF16 = function() return ReadF16() end
|
||||
writes.NumberF16 = function(value: number) Allocate(2) WriteF16(value) end
|
||||
|
||||
types.NumberF24 = ("NumberF24" :: any) :: number
|
||||
reads.NumberF24 = function() return ReadF24() end
|
||||
writes.NumberF24 = function(value: number) Allocate(3) WriteF24(value) end
|
||||
|
||||
types.NumberF32 = ("NumberF32" :: any) :: number
|
||||
reads.NumberF32 = function() return ReadF32() end
|
||||
writes.NumberF32 = function(value: number) Allocate(4) WriteF32(value) end
|
||||
|
||||
types.NumberF64 = ("NumberF64" :: any) :: number
|
||||
reads.NumberF64 = function() return ReadF64() end
|
||||
writes.NumberF64 = function(value: number) Allocate(8) WriteF64(value) end
|
||||
|
||||
types.String = ("String" :: any) :: string
|
||||
reads.String = function() return ReadString(ReadU8()) end
|
||||
writes.String = function(value: string) local length = #value Allocate(1 + length) WriteU8(length) WriteString(value) end
|
||||
|
||||
types.StringLong = ("StringLong" :: any) :: string
|
||||
reads.StringLong = function() return ReadString(ReadU16()) end
|
||||
writes.StringLong = function(value: string) local length = #value Allocate(2 + length) WriteU16(length) WriteString(value) end
|
||||
|
||||
types.Buffer = ("Buffer" :: any) :: buffer
|
||||
reads.Buffer = function() return ReadBuffer(ReadU8()) end
|
||||
writes.Buffer = function(value: buffer) local length = buffer.len(value) Allocate(1 + length) WriteU8(length) WriteBuffer(value) end
|
||||
|
||||
types.BufferLong = ("BufferLong" :: any) :: buffer
|
||||
reads.BufferLong = function() return ReadBuffer(ReadU16()) end
|
||||
writes.BufferLong = function(value: buffer) local length = buffer.len(value) Allocate(2 + length) WriteU16(length) WriteBuffer(value) end
|
||||
|
||||
types.Instance = ("Instance" :: any) :: Instance
|
||||
reads.Instance = function() return ReadInstance() end
|
||||
writes.Instance = function(value: Instance) WriteInstance(value) end
|
||||
|
||||
types.Boolean8 = ("Boolean8" :: any) :: boolean
|
||||
reads.Boolean8 = function() return ReadU8() == 1 end
|
||||
writes.Boolean8 = function(value: boolean) Allocate(1) WriteU8(if value then 1 else 0) end
|
||||
|
||||
types.NumberRange = ("NumberRange" :: any) :: NumberRange
|
||||
reads.NumberRange = function() return NumberRange.new(ReadF32(), ReadF32()) end
|
||||
writes.NumberRange = function(value: NumberRange) Allocate(8) WriteF32(value.Min) WriteF32(value.Max) end
|
||||
|
||||
types.BrickColor = ("BrickColor" :: any) :: BrickColor
|
||||
reads.BrickColor = function() return BrickColor.new(ReadU16()) end
|
||||
writes.BrickColor = function(value: BrickColor) Allocate(2) WriteU16(value.Number) end
|
||||
|
||||
types.Color3 = ("Color3" :: any) :: Color3
|
||||
reads.Color3 = function() return Color3.fromRGB(ReadU8(), ReadU8(), ReadU8()) end
|
||||
writes.Color3 = function(value: Color3) Allocate(3) WriteU8(value.R * 255 + 0.5) WriteU8(value.G * 255 + 0.5) WriteU8(value.B * 255 + 0.5) end
|
||||
|
||||
types.UDim = ("UDim" :: any) :: UDim
|
||||
reads.UDim = function() return UDim.new(ReadS16() / 1000, ReadS16()) end
|
||||
writes.UDim = function(value: UDim) Allocate(4) WriteS16(value.Scale * 1000) WriteS16(value.Offset) end
|
||||
|
||||
types.UDim2 = ("UDim2" :: any) :: UDim2
|
||||
reads.UDim2 = function() return UDim2.new(ReadS16() / 1000, ReadS16(), ReadS16() / 1000, ReadS16()) end
|
||||
writes.UDim2 = function(value: UDim2) Allocate(8) WriteS16(value.X.Scale * 1000) WriteS16(value.X.Offset) WriteS16(value.Y.Scale * 1000) WriteS16(value.Y.Offset) end
|
||||
|
||||
types.Rect = ("Rect" :: any) :: Rect
|
||||
reads.Rect = function() return Rect.new(ReadF32(), ReadF32(), ReadF32(), ReadF32()) end
|
||||
writes.Rect = function(value: Rect) Allocate(16) WriteF32(value.Min.X) WriteF32(value.Min.Y) WriteF32(value.Max.X) WriteF32(value.Max.Y) end
|
||||
|
||||
types.Vector2S16 = ("Vector2S16" :: any) :: Vector2
|
||||
reads.Vector2S16 = function() return Vector2.new(ReadS16(), ReadS16()) end
|
||||
writes.Vector2S16 = function(value: Vector2) Allocate(4) WriteS16(value.X) WriteS16(value.Y) end
|
||||
|
||||
types.Vector2F24 = ("Vector2F24" :: any) :: Vector2
|
||||
reads.Vector2F24 = function() return Vector2.new(ReadF24(), ReadF24()) end
|
||||
writes.Vector2F24 = function(value: Vector2) Allocate(6) WriteF24(value.X) WriteF24(value.Y) end
|
||||
|
||||
types.Vector2F32 = ("Vector2F32" :: any) :: Vector2
|
||||
reads.Vector2F32 = function() return Vector2.new(ReadF32(), ReadF32()) end
|
||||
writes.Vector2F32 = function(value: Vector2) Allocate(8) WriteF32(value.X) WriteF32(value.Y) end
|
||||
|
||||
types.Vector3S16 = ("Vector3S16" :: any) :: Vector3
|
||||
reads.Vector3S16 = function() return Vector3.new(ReadS16(), ReadS16(), ReadS16()) end
|
||||
writes.Vector3S16 = function(value: Vector3) Allocate(6) WriteS16(value.X) WriteS16(value.Y) WriteS16(value.Z) end
|
||||
|
||||
types.Vector3F24 = ("Vector3F24" :: any) :: Vector3
|
||||
reads.Vector3F24 = function() return Vector3.new(ReadF24(), ReadF24(), ReadF24()) end
|
||||
writes.Vector3F24 = function(value: Vector3) Allocate(9) WriteF24(value.X) WriteF24(value.Y) WriteF24(value.Z) end
|
||||
|
||||
types.Vector3F32 = ("Vector3F32" :: any) :: Vector3
|
||||
reads.Vector3F32 = function() return Vector3.new(ReadF32(), ReadF32(), ReadF32()) end
|
||||
writes.Vector3F32 = function(value: Vector3) Allocate(12) WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z) end
|
||||
|
||||
types.NumberU4 = ("NumberU4" :: any) :: {number}
|
||||
reads.NumberU4 = function()
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 1
|
||||
return {
|
||||
buffer.readbits(activeBuffer, bitOffset + 0, 4),
|
||||
buffer.readbits(activeBuffer, bitOffset + 4, 4)
|
||||
}
|
||||
end
|
||||
writes.NumberU4 = function(value: {number})
|
||||
Allocate(1)
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 1
|
||||
buffer.writebits(activeBuffer, bitOffset + 0, 4, value[1])
|
||||
buffer.writebits(activeBuffer, bitOffset + 4, 4, value[2])
|
||||
end
|
||||
|
||||
types.BooleanNumber = ("BooleanNumber" :: any) :: {Boolean: boolean, Number: number}
|
||||
reads.BooleanNumber = function()
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 1
|
||||
return {
|
||||
Boolean = buffer.readbits(activeBuffer, bitOffset + 0, 1) == 1,
|
||||
Number = buffer.readbits(activeBuffer, bitOffset + 1, 7),
|
||||
}
|
||||
end
|
||||
writes.BooleanNumber = function(value: {Boolean: boolean, Number: number})
|
||||
Allocate(1)
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 1
|
||||
buffer.writebits(activeBuffer, bitOffset + 0, 1, if value.Boolean then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 1, 7, value.Number)
|
||||
end
|
||||
|
||||
types.Boolean1 = ("Boolean1" :: any) :: {boolean}
|
||||
reads.Boolean1 = function()
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 1
|
||||
return {
|
||||
buffer.readbits(activeBuffer, bitOffset + 0, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 1, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 2, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 3, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 4, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 5, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 6, 1) == 1,
|
||||
buffer.readbits(activeBuffer, bitOffset + 7, 1) == 1,
|
||||
}
|
||||
end
|
||||
writes.Boolean1 = function(value: {boolean})
|
||||
Allocate(1)
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += 1
|
||||
buffer.writebits(activeBuffer, bitOffset + 0, 1, if value[1] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 1, 1, if value[2] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 2, 1, if value[3] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 3, 1, if value[4] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 4, 1, if value[5] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 5, 1, if value[6] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 6, 1, if value[7] then 1 else 0)
|
||||
buffer.writebits(activeBuffer, bitOffset + 7, 1, if value[8] then 1 else 0)
|
||||
end
|
||||
|
||||
types.CFrameF24U8 = ("CFrameF24U8" :: any) :: CFrame
|
||||
reads.CFrameF24U8 = function()
|
||||
return CFrame.fromEulerAnglesXYZ(ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331)
|
||||
+ Vector3.new(ReadF24(), ReadF24(), ReadF24())
|
||||
end
|
||||
writes.CFrameF24U8 = function(value: CFrame)
|
||||
local rx, ry, rz = value:ToEulerAnglesXYZ()
|
||||
Allocate(12)
|
||||
WriteU8(rx * 40.58451048843331 + 0.5) WriteU8(ry * 40.58451048843331 + 0.5) WriteU8(rz * 40.58451048843331 + 0.5)
|
||||
WriteF24(value.X) WriteF24(value.Y) WriteF24(value.Z)
|
||||
end
|
||||
|
||||
types.CFrameF32U8 = ("CFrameF32U8" :: any) :: CFrame
|
||||
reads.CFrameF32U8 = function()
|
||||
return CFrame.fromEulerAnglesXYZ(ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331)
|
||||
+ Vector3.new(ReadF32(), ReadF32(), ReadF32())
|
||||
end
|
||||
writes.CFrameF32U8 = function(value: CFrame)
|
||||
local rx, ry, rz = value:ToEulerAnglesXYZ()
|
||||
Allocate(15)
|
||||
WriteU8(rx * 40.58451048843331 + 0.5) WriteU8(ry * 40.58451048843331 + 0.5) WriteU8(rz * 40.58451048843331 + 0.5)
|
||||
WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z)
|
||||
end
|
||||
|
||||
types.CFrameF32U16 = ("CFrameF32U16" :: any) :: CFrame
|
||||
reads.CFrameF32U16 = function()
|
||||
return CFrame.fromEulerAnglesXYZ(ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361)
|
||||
+ Vector3.new(ReadF32(), ReadF32(), ReadF32())
|
||||
end
|
||||
writes.CFrameF32U16 = function(value: CFrame)
|
||||
local rx, ry, rz = value:ToEulerAnglesXYZ()
|
||||
Allocate(18)
|
||||
WriteU16(rx * 10430.219195527361 + 0.5) WriteU16(ry * 10430.219195527361 + 0.5) WriteU16(rz * 10430.219195527361 + 0.5)
|
||||
WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z)
|
||||
end
|
||||
|
||||
types.Region3 = ("Region3" :: any) :: Region3
|
||||
reads.Region3 = function()
|
||||
return Region3.new(
|
||||
Vector3.new(ReadF32(), ReadF32(), ReadF32()),
|
||||
Vector3.new(ReadF32(), ReadF32(), ReadF32())
|
||||
)
|
||||
end
|
||||
writes.Region3 = function(value: Region3)
|
||||
local halfSize = value.Size / 2
|
||||
local minimum = value.CFrame.Position - halfSize
|
||||
local maximum = value.CFrame.Position + halfSize
|
||||
Allocate(24)
|
||||
WriteF32(minimum.X) WriteF32(minimum.Y) WriteF32(minimum.Z)
|
||||
WriteF32(maximum.X) WriteF32(maximum.Y) WriteF32(maximum.Z)
|
||||
end
|
||||
|
||||
types.NumberSequence = ("NumberSequence" :: any) :: NumberSequence
|
||||
reads.NumberSequence = function()
|
||||
local length = ReadU8()
|
||||
local keypoints = table.create(length)
|
||||
for index = 1, length do
|
||||
table.insert(keypoints, NumberSequenceKeypoint.new(ReadU8() / 255, ReadU8() / 255, ReadU8() / 255))
|
||||
end
|
||||
return NumberSequence.new(keypoints)
|
||||
end
|
||||
writes.NumberSequence = function(value: NumberSequence)
|
||||
local length = #value.Keypoints
|
||||
Allocate(1 + length * 3)
|
||||
WriteU8(length)
|
||||
for index, keypoint in value.Keypoints do
|
||||
WriteU8(keypoint.Time * 255 + 0.5) WriteU8(keypoint.Value * 255 + 0.5) WriteU8(keypoint.Envelope * 255 + 0.5)
|
||||
end
|
||||
end
|
||||
|
||||
types.ColorSequence = ("ColorSequence" :: any) :: ColorSequence
|
||||
reads.ColorSequence = function()
|
||||
local length = ReadU8()
|
||||
local keypoints = table.create(length)
|
||||
for index = 1, length do
|
||||
table.insert(keypoints, ColorSequenceKeypoint.new(ReadU8() / 255, Color3.fromRGB(ReadU8(), ReadU8(), ReadU8())))
|
||||
end
|
||||
return ColorSequence.new(keypoints)
|
||||
end
|
||||
writes.ColorSequence = function(value: ColorSequence)
|
||||
local length = #value.Keypoints
|
||||
Allocate(1 + length * 4)
|
||||
WriteU8(length)
|
||||
for index, keypoint in value.Keypoints do
|
||||
WriteU8(keypoint.Time * 255 + 0.5)
|
||||
WriteU8(keypoint.Value.R * 255 + 0.5) WriteU8(keypoint.Value.G * 255 + 0.5) WriteU8(keypoint.Value.B * 255 + 0.5)
|
||||
end
|
||||
end
|
||||
|
||||
local characterIndices = {}
|
||||
local characters = require(script.Characters)
|
||||
for index, value in characters do characterIndices[value] = index end
|
||||
local characterBits = math.ceil(math.log(#characters + 1, 2))
|
||||
local characterBytes = characterBits / 8
|
||||
types.Characters = ("Characters" :: any) :: string
|
||||
reads.Characters = function()
|
||||
local length = ReadU8()
|
||||
local characterArray = table.create(length)
|
||||
local bitOffset = bufferOffset * 8
|
||||
bufferOffset += math.ceil(length * characterBytes)
|
||||
for index = 1, length do
|
||||
table.insert(characterArray, characters[buffer.readbits(activeBuffer, bitOffset, characterBits)])
|
||||
bitOffset += characterBits
|
||||
end
|
||||
return table.concat(characterArray)
|
||||
end
|
||||
writes.Characters = function(value: string)
|
||||
local length = #value
|
||||
local bytes = math.ceil(length * characterBytes)
|
||||
Allocate(1 + bytes)
|
||||
WriteU8(length)
|
||||
local bitOffset = bufferOffset * 8
|
||||
for index = 1, length do
|
||||
buffer.writebits(activeBuffer, bitOffset, characterBits, characterIndices[value:sub(index, index)])
|
||||
bitOffset += characterBits
|
||||
end
|
||||
bufferOffset += bytes
|
||||
end
|
||||
|
||||
local enumIndices = {}
|
||||
local enums = require(script.Enums)
|
||||
for index, static in enums do enumIndices[static] = index end
|
||||
types.EnumItem = ("EnumItem" :: any) :: EnumItem
|
||||
reads.EnumItem = function() return enums[ReadU8()]:FromValue(ReadU16()) end
|
||||
writes.EnumItem = function(value: EnumItem) Allocate(3) WriteU8(enumIndices[value.EnumType]) WriteU16(value.Value) end
|
||||
|
||||
local staticIndices = {}
|
||||
local statics = require(script.Static1)
|
||||
for index, static in statics do staticIndices[static] = index end
|
||||
types.Static1 = ("Static1" :: any) :: any
|
||||
reads.Static1 = function() return statics[ReadU8()] end
|
||||
writes.Static1 = function(value: any) Allocate(1) WriteU8(staticIndices[value] or 0) end
|
||||
|
||||
local staticIndices = {}
|
||||
local statics = require(script.Static2)
|
||||
for index, static in statics do staticIndices[static] = index end
|
||||
types.Static2 = ("Static2" :: any) :: any
|
||||
reads.Static2 = function() return statics[ReadU8()] end
|
||||
writes.Static2 = function(value: any) Allocate(1) WriteU8(staticIndices[value] or 0) end
|
||||
|
||||
local staticIndices = {}
|
||||
local statics = require(script.Static3)
|
||||
for index, static in statics do staticIndices[static] = index end
|
||||
types.Static3 = ("Static3" :: any) :: any
|
||||
reads.Static3 = function() return statics[ReadU8()] end
|
||||
writes.Static3 = function(value: any) Allocate(1) WriteU8(staticIndices[value] or 0) end
|
||||
|
||||
|
||||
-- Any Types
|
||||
anyReads[0] = function() return nil end
|
||||
anyWrites["nil"] = function(value: nil) Allocate(1) WriteU8(0) end
|
||||
|
||||
anyReads[1] = function() return -ReadU8() end
|
||||
anyReads[2] = function() return -ReadU16() end
|
||||
anyReads[3] = function() return -ReadU24() end
|
||||
anyReads[4] = function() return -ReadU32() end
|
||||
anyReads[5] = function() return ReadU8() end
|
||||
anyReads[6] = function() return ReadU16() end
|
||||
anyReads[7] = function() return ReadU24() end
|
||||
anyReads[8] = function() return ReadU32() end
|
||||
anyReads[9] = function() return ReadF32() end
|
||||
anyReads[10] = function() return ReadF64() end
|
||||
anyWrites.number = function(value: number)
|
||||
if value % 1 == 0 then
|
||||
if value < 0 then
|
||||
if value > -256 then
|
||||
Allocate(2) WriteU8(1) WriteU8(-value)
|
||||
elseif value > -65536 then
|
||||
Allocate(3) WriteU8(2) WriteU16(-value)
|
||||
elseif value > -16777216 then
|
||||
Allocate(4) WriteU8(3) WriteU24(-value)
|
||||
elseif value > -4294967296 then
|
||||
Allocate(5) WriteU8(4) WriteU32(-value)
|
||||
else
|
||||
Allocate(9) WriteU8(10) WriteF64(value)
|
||||
end
|
||||
else
|
||||
if value < 256 then
|
||||
Allocate(2) WriteU8(5) WriteU8(value)
|
||||
elseif value < 65536 then
|
||||
Allocate(3) WriteU8(6) WriteU16(value)
|
||||
elseif value < 16777216 then
|
||||
Allocate(4) WriteU8(7) WriteU24(value)
|
||||
elseif value < 4294967296 then
|
||||
Allocate(5) WriteU8(8) WriteU32(value)
|
||||
else
|
||||
Allocate(9) WriteU8(10) WriteF64(value)
|
||||
end
|
||||
end
|
||||
elseif value > -1048576 and value < 1048576 then
|
||||
Allocate(5) WriteU8(9) WriteF32(value)
|
||||
else
|
||||
Allocate(9) WriteU8(10) WriteF64(value)
|
||||
end
|
||||
end
|
||||
|
||||
anyReads[11] = function() return ReadString(ReadU8()) end
|
||||
anyWrites.string = function(value: string) local length = #value Allocate(2 + length) WriteU8(11) WriteU8(length) WriteString(value) end
|
||||
|
||||
anyReads[12] = function() return ReadBuffer(ReadU8()) end
|
||||
anyWrites.buffer = function(value: buffer) local length = buffer.len(value) Allocate(2 + length) WriteU8(12) WriteU8(length) WriteBuffer(value) end
|
||||
|
||||
anyReads[13] = function() return ReadInstance() end
|
||||
anyWrites.Instance = function(value: Instance) Allocate(1) WriteU8(13) WriteInstance(value) end
|
||||
|
||||
anyReads[14] = function() return ReadU8() == 1 end
|
||||
anyWrites.boolean = function(value: boolean) Allocate(2) WriteU8(14) WriteU8(if value then 1 else 0) end
|
||||
|
||||
anyReads[15] = function() return NumberRange.new(ReadF32(), ReadF32()) end
|
||||
anyWrites.NumberRange = function(value: NumberRange) Allocate(9) WriteU8(15) WriteF32(value.Min) WriteF32(value.Max) end
|
||||
|
||||
anyReads[16] = function() return BrickColor.new(ReadU16()) end
|
||||
anyWrites.BrickColor = function(value: BrickColor) Allocate(3) WriteU8(16) WriteU16(value.Number) end
|
||||
|
||||
anyReads[17] = function() return Color3.fromRGB(ReadU8(), ReadU8(), ReadU8()) end
|
||||
anyWrites.Color3 = function(value: Color3) Allocate(4) WriteU8(17) WriteU8(value.R * 255 + 0.5) WriteU8(value.G * 255 + 0.5) WriteU8(value.B * 255 + 0.5) end
|
||||
|
||||
anyReads[18] = function() return UDim.new(ReadS16() / 1000, ReadS16()) end
|
||||
anyWrites.UDim = function(value: UDim) Allocate(5) WriteU8(18) WriteS16(value.Scale * 1000) WriteS16(value.Offset) end
|
||||
|
||||
anyReads[19] = function() return UDim2.new(ReadS16() / 1000, ReadS16(), ReadS16() / 1000, ReadS16()) end
|
||||
anyWrites.UDim2 = function(value: UDim2) Allocate(9) WriteU8(19) WriteS16(value.X.Scale * 1000) WriteS16(value.X.Offset) WriteS16(value.Y.Scale * 1000) WriteS16(value.Y.Offset) end
|
||||
|
||||
anyReads[20] = function() return Rect.new(ReadF32(), ReadF32(), ReadF32(), ReadF32()) end
|
||||
anyWrites.Rect = function(value: Rect) Allocate(17) WriteU8(20) WriteF32(value.Min.X) WriteF32(value.Min.Y) WriteF32(value.Max.X) WriteF32(value.Max.Y) end
|
||||
|
||||
anyReads[21] = function() return Vector2.new(ReadF32(), ReadF32()) end
|
||||
anyWrites.Vector2 = function(value: Vector2) Allocate(9) WriteU8(21) WriteF32(value.X) WriteF32(value.Y) end
|
||||
|
||||
anyReads[22] = function() return Vector3.new(ReadF32(), ReadF32(), ReadF32()) end
|
||||
anyWrites.Vector3 = function(value: Vector3) Allocate(13) WriteU8(22) WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z) end
|
||||
|
||||
anyReads[23] = function()
|
||||
return CFrame.fromEulerAnglesXYZ(ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361)
|
||||
+ Vector3.new(ReadF32(), ReadF32(), ReadF32())
|
||||
end
|
||||
anyWrites.CFrame = function(value: CFrame)
|
||||
local rx, ry, rz = value:ToEulerAnglesXYZ()
|
||||
Allocate(19)
|
||||
WriteU8(23)
|
||||
WriteU16(rx * 10430.219195527361 + 0.5) WriteU16(ry * 10430.219195527361 + 0.5) WriteU16(rz * 10430.219195527361 + 0.5)
|
||||
WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z)
|
||||
end
|
||||
|
||||
anyReads[24] = function()
|
||||
return Region3.new(
|
||||
Vector3.new(ReadF32(), ReadF32(), ReadF32()),
|
||||
Vector3.new(ReadF32(), ReadF32(), ReadF32())
|
||||
)
|
||||
end
|
||||
anyWrites.Region3 = function(value: Region3)
|
||||
local halfSize = value.Size / 2
|
||||
local minimum = value.CFrame.Position - halfSize
|
||||
local maximum = value.CFrame.Position + halfSize
|
||||
Allocate(25)
|
||||
WriteU8(24)
|
||||
WriteF32(minimum.X) WriteF32(minimum.Y) WriteF32(minimum.Z)
|
||||
WriteF32(maximum.X) WriteF32(maximum.Y) WriteF32(maximum.Z)
|
||||
end
|
||||
|
||||
anyReads[25] = function()
|
||||
local length = ReadU8()
|
||||
local keypoints = table.create(length)
|
||||
for index = 1, length do
|
||||
table.insert(keypoints, NumberSequenceKeypoint.new(ReadU8() / 255, ReadU8() / 255, ReadU8() / 255))
|
||||
end
|
||||
return NumberSequence.new(keypoints)
|
||||
end
|
||||
anyWrites.NumberSequence = function(value: NumberSequence)
|
||||
local length = #value.Keypoints
|
||||
Allocate(2 + length * 3)
|
||||
WriteU8(25)
|
||||
WriteU8(length)
|
||||
for index, keypoint in value.Keypoints do
|
||||
WriteU8(keypoint.Time * 255 + 0.5) WriteU8(keypoint.Value * 255 + 0.5) WriteU8(keypoint.Envelope * 255 + 0.5)
|
||||
end
|
||||
end
|
||||
|
||||
anyReads[26] = function()
|
||||
local length = ReadU8()
|
||||
local keypoints = table.create(length)
|
||||
for index = 1, length do
|
||||
table.insert(keypoints, ColorSequenceKeypoint.new(ReadU8() / 255, Color3.fromRGB(ReadU8(), ReadU8(), ReadU8())))
|
||||
end
|
||||
return ColorSequence.new(keypoints)
|
||||
end
|
||||
anyWrites.ColorSequence = function(value: ColorSequence)
|
||||
local length = #value.Keypoints
|
||||
Allocate(2 + length * 4)
|
||||
WriteU8(26)
|
||||
WriteU8(length)
|
||||
for index, keypoint in value.Keypoints do
|
||||
WriteU8(keypoint.Time * 255 + 0.5)
|
||||
WriteU8(keypoint.Value.R * 255 + 0.5) WriteU8(keypoint.Value.G * 255 + 0.5) WriteU8(keypoint.Value.B * 255 + 0.5)
|
||||
end
|
||||
end
|
||||
|
||||
anyReads[27] = function()
|
||||
return enums[ReadU8()]:FromValue(ReadU16())
|
||||
end
|
||||
anyWrites.EnumItem = function(value: EnumItem)
|
||||
Allocate(4)
|
||||
WriteU8(27)
|
||||
WriteU8(enumIndices[value.EnumType])
|
||||
WriteU16(value.Value)
|
||||
end
|
||||
|
||||
anyReads[28] = function()
|
||||
local value = {}
|
||||
while true do
|
||||
local typeId = ReadU8()
|
||||
if typeId == 0 then return value else value[anyReads[typeId]()] = anyReads[ReadU8()]() end
|
||||
end
|
||||
end
|
||||
anyWrites.table = function(value: {[any]: any})
|
||||
Allocate(1)
|
||||
WriteU8(28)
|
||||
for index, value in value do anyWrites[typeof(index)](index) anyWrites[typeof(value)](value) end
|
||||
Allocate(1)
|
||||
WriteU8(0)
|
||||
end
|
||||
|
||||
|
||||
return {
|
||||
Import = function(cursor: Cursor)
|
||||
activeCursor = cursor
|
||||
activeBuffer = cursor.Buffer
|
||||
bufferLength = cursor.BufferLength
|
||||
bufferOffset = cursor.BufferOffset
|
||||
instances = cursor.Instances
|
||||
instancesOffset = cursor.InstancesOffset
|
||||
end,
|
||||
|
||||
Export = function()
|
||||
activeCursor.BufferLength = bufferLength
|
||||
activeCursor.BufferOffset = bufferOffset
|
||||
activeCursor.InstancesOffset = instancesOffset
|
||||
return activeCursor
|
||||
end,
|
||||
|
||||
Truncate = function()
|
||||
local truncatedBuffer = buffer.create(bufferOffset)
|
||||
buffer.copy(truncatedBuffer, 0, activeBuffer, 0, bufferOffset)
|
||||
if instancesOffset == 0 then return truncatedBuffer else return truncatedBuffer, instances end
|
||||
end,
|
||||
|
||||
Ended = function()
|
||||
return bufferOffset >= bufferLength
|
||||
end,
|
||||
|
||||
Types = types,
|
||||
Reads = reads,
|
||||
Writes = writes,
|
||||
}
|
368
src/Chemical/Packages/Packet/init.lua
Normal file
368
src/Chemical/Packages/Packet/init.lua
Normal file
|
@ -0,0 +1,368 @@
|
|||
--!strict
|
||||
|
||||
|
||||
-- Requires
|
||||
local Signal = require(script.Signal)
|
||||
local Task = require(script.Task)
|
||||
local Types = require(script.Types)
|
||||
|
||||
|
||||
-- Types
|
||||
export type Packet<A... = (), B... = ()> = {
|
||||
Type: "Packet",
|
||||
Id: number,
|
||||
Name: string,
|
||||
Reads: {() -> any},
|
||||
Writes: {(any) -> ()},
|
||||
ResponseTimeout: number,
|
||||
ResponseTimeoutValue: any,
|
||||
ResponseReads: {() -> any},
|
||||
ResponseWrites: {(any) -> ()},
|
||||
OnServerEvent: Signal.Signal<(Player, A...)>,
|
||||
OnClientEvent: Signal.Signal<A...>,
|
||||
OnServerInvoke: nil | (player: Player, A...) -> B...,
|
||||
OnClientInvoke: nil | (A...) -> B...,
|
||||
Response: (self: Packet<A..., B...>, B...) -> Packet<A..., B...>,
|
||||
Fire: (self: Packet<A..., B...>, A...) -> B...,
|
||||
FireClient: (self: Packet<A..., B...>, player: Player, A...) -> B...,
|
||||
Serialize: (self: Packet<A..., B...>, A...) -> (buffer, {Instance}?),
|
||||
Deserialize: (self: Packet<A..., B...>, serializeBuffer: buffer, instances: {Instance}?) -> A...,
|
||||
}
|
||||
|
||||
|
||||
-- Varables
|
||||
local ParametersToFunctions, TableToFunctions, ReadParameters, WriteParameters, Timeout
|
||||
local RunService = game:GetService("RunService")
|
||||
local PlayersService = game:GetService("Players")
|
||||
local reads, writes, Import, Export, Truncate, Ended = Types.Reads, Types.Writes, Types.Import, Types.Export, Types.Truncate, Types.Ended
|
||||
local ReadU8, WriteU8, ReadU16, WriteU16 = reads.NumberU8, writes.NumberU8, reads.NumberU16, writes.NumberU16
|
||||
local Packet = {} :: Packet<...any, ...any>
|
||||
local packets = {} :: {[string | number]: Packet<...any, ...any>}
|
||||
local playerCursors : {[Player]: Types.Cursor}
|
||||
local playerThreads : {[Player]: {[number]: {Yielded: thread, Timeout: thread}, Index: number}}
|
||||
local threads : {[number]: {Yielded: thread, Timeout: thread}, Index: number}
|
||||
local remoteEvent : RemoteEvent
|
||||
local packetCounter : number
|
||||
local cursor = {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0}
|
||||
|
||||
|
||||
-- Constructor
|
||||
local function Constructor<A..., B...>(_, name: string, ...: A...)
|
||||
local packet = packets[name] :: Packet<A..., B...>
|
||||
if packet then return packet end
|
||||
local packet = (setmetatable({}, Packet) :: any) :: Packet<A..., B...>
|
||||
packet.Name = name
|
||||
if RunService:IsServer() then
|
||||
packet.Id = packetCounter
|
||||
packet.OnServerEvent = Signal() :: Signal.Signal<(Player, A...)>
|
||||
remoteEvent:SetAttribute(name, packetCounter)
|
||||
packets[packetCounter] = packet
|
||||
packetCounter += 1
|
||||
else
|
||||
packet.Id = remoteEvent:GetAttribute(name)
|
||||
packet.OnClientEvent = Signal() :: Signal.Signal<A...>
|
||||
if packet.Id then packets[packet.Id] = packet end
|
||||
end
|
||||
packet.Reads, packet.Writes = ParametersToFunctions(table.pack(...))
|
||||
packets[packet.Name] = packet
|
||||
return packet
|
||||
end
|
||||
|
||||
|
||||
-- Packet
|
||||
Packet["__index"] = Packet
|
||||
Packet.Type = "Packet"
|
||||
|
||||
function Packet:Response(...)
|
||||
self.ResponseTimeout = self.ResponseTimeout or 10
|
||||
self.ResponseReads, self.ResponseWrites = ParametersToFunctions(table.pack(...))
|
||||
return self
|
||||
end
|
||||
|
||||
function Packet:Fire(...)
|
||||
Import(cursor)
|
||||
WriteU8(self.Id)
|
||||
if self.ResponseReads then
|
||||
WriteU8(threads.Index)
|
||||
threads[threads.Index] = {Yielded = coroutine.running(), Timeout = Task:Delay(self.ResponseTimeout, Timeout, coroutine.running(), self.ResponseTimeoutValue)}
|
||||
threads.Index = (threads.Index + 1) % 128
|
||||
WriteParameters(self.Writes, {...})
|
||||
cursor = Export()
|
||||
return coroutine.yield()
|
||||
else
|
||||
WriteParameters(self.Writes, {...})
|
||||
cursor = Export()
|
||||
end
|
||||
end
|
||||
|
||||
function Packet:FireClient(player, ...)
|
||||
if player.Parent == nil then return end
|
||||
Import(playerCursors[player] or {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0})
|
||||
WriteU8(self.Id)
|
||||
if self.ResponseReads then
|
||||
local threads = playerThreads[player]
|
||||
if threads == nil then threads = {Index = 0} playerThreads[player] = threads end
|
||||
WriteU8(threads.Index)
|
||||
threads[threads.Index] = {Yielded = coroutine.running(), Timeout = Task:Delay(self.ResponseTimeout, Timeout, coroutine.running(), self.ResponseTimeoutValue)}
|
||||
threads.Index = (threads.Index + 1) % 128
|
||||
WriteParameters(self.Writes, {...})
|
||||
playerCursors[player] = Export()
|
||||
return coroutine.yield()
|
||||
else
|
||||
WriteParameters(self.Writes, {...})
|
||||
playerCursors[player] = Export()
|
||||
end
|
||||
end
|
||||
|
||||
function Packet:Serialize(...)
|
||||
Import({Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0})
|
||||
WriteParameters(self.Writes, {...})
|
||||
return Truncate()
|
||||
end
|
||||
|
||||
function Packet:Deserialize(serializeBuffer, instances)
|
||||
Import({Buffer = serializeBuffer, BufferLength = buffer.len(serializeBuffer), BufferOffset = 0, Instances = instances or {}, InstancesOffset = 0})
|
||||
return ReadParameters(self.Reads)
|
||||
end
|
||||
|
||||
|
||||
-- Functions
|
||||
function ParametersToFunctions(parameters: {any})
|
||||
local readFunctions, writeFunctions = table.create(#parameters), table.create(#parameters)
|
||||
for index, parameter in ipairs(parameters) do
|
||||
if type(parameter) == "table" then
|
||||
readFunctions[index], writeFunctions[index] = TableToFunctions(parameter)
|
||||
else
|
||||
readFunctions[index], writeFunctions[index] = reads[parameter], writes[parameter]
|
||||
end
|
||||
end
|
||||
return readFunctions, writeFunctions
|
||||
end
|
||||
|
||||
function TableToFunctions(parameters: {any})
|
||||
if #parameters == 1 then
|
||||
local parameter = parameters[1]
|
||||
local ReadFunction, WriteFunction
|
||||
if type(parameter) == "table" then
|
||||
ReadFunction, WriteFunction = TableToFunctions(parameter)
|
||||
else
|
||||
ReadFunction, WriteFunction = reads[parameter], writes[parameter]
|
||||
end
|
||||
local Read = function()
|
||||
local length = ReadU16()
|
||||
local values = table.create(length)
|
||||
for index = 1, length do values[index] = ReadFunction() end
|
||||
return values
|
||||
end
|
||||
local Write = function(values: {any})
|
||||
WriteU16(#values)
|
||||
for index, value in values do WriteFunction(value) end
|
||||
end
|
||||
return Read, Write
|
||||
else
|
||||
local keys = {} for key, value in parameters do table.insert(keys, key) end table.sort(keys)
|
||||
local readFunctions, writeFunctions = table.create(#keys), table.create(#keys)
|
||||
for index, key in keys do
|
||||
local parameter = parameters[key]
|
||||
if type(parameter) == "table" then
|
||||
readFunctions[index], writeFunctions[index] = TableToFunctions(parameter)
|
||||
else
|
||||
readFunctions[index], writeFunctions[index] = reads[parameter], writes[parameter]
|
||||
end
|
||||
end
|
||||
local Read = function()
|
||||
local values = {}
|
||||
for index, ReadFunction in readFunctions do values[keys[index]] = ReadFunction() end
|
||||
return values
|
||||
end
|
||||
local Write = function(values: {[any]: any})
|
||||
for index, WriteFunction in writeFunctions do WriteFunction(values[keys[index]]) end
|
||||
end
|
||||
return Read, Write
|
||||
end
|
||||
end
|
||||
|
||||
function ReadParameters(reads: {() -> any})
|
||||
local values = table.create(#reads)
|
||||
for index, func in reads do values[index] = func() end
|
||||
return table.unpack(values)
|
||||
end
|
||||
|
||||
function WriteParameters(writes: {(any) -> ()}, values: {any})
|
||||
for index, func in writes do func(values[index]) end
|
||||
end
|
||||
|
||||
function Timeout(thread: thread, value: any)
|
||||
task.defer(thread, value)
|
||||
end
|
||||
|
||||
|
||||
-- Initialize
|
||||
if RunService:IsServer() then
|
||||
playerCursors = {}
|
||||
playerThreads = {}
|
||||
packetCounter = 0
|
||||
remoteEvent = Instance.new("RemoteEvent", script)
|
||||
|
||||
local playerBytes = {}
|
||||
|
||||
local thread = task.spawn(function()
|
||||
while true do
|
||||
coroutine.yield()
|
||||
if cursor.BufferOffset > 0 then
|
||||
local truncatedBuffer = buffer.create(cursor.BufferOffset)
|
||||
buffer.copy(truncatedBuffer, 0, cursor.Buffer, 0, cursor.BufferOffset)
|
||||
if cursor.InstancesOffset == 0 then
|
||||
remoteEvent:FireAllClients(truncatedBuffer)
|
||||
else
|
||||
remoteEvent:FireAllClients(truncatedBuffer, cursor.Instances)
|
||||
cursor.InstancesOffset = 0
|
||||
table.clear(cursor.Instances)
|
||||
end
|
||||
cursor.BufferOffset = 0
|
||||
end
|
||||
for player, cursor in playerCursors do
|
||||
local truncatedBuffer = buffer.create(cursor.BufferOffset)
|
||||
buffer.copy(truncatedBuffer, 0, cursor.Buffer, 0, cursor.BufferOffset)
|
||||
if cursor.InstancesOffset == 0 then
|
||||
remoteEvent:FireClient(player, truncatedBuffer)
|
||||
else
|
||||
remoteEvent:FireClient(player, truncatedBuffer, cursor.Instances)
|
||||
end
|
||||
end
|
||||
table.clear(playerCursors)
|
||||
table.clear(playerBytes)
|
||||
end
|
||||
end)
|
||||
|
||||
local respond = function(packet: Packet, player: Player, threadIndex: number, ...)
|
||||
if packet.OnServerInvoke == nil then if RunService:IsStudio() then warn("OnServerInvoke not found for packet:", packet.Name, "discarding event:", ...) end return end
|
||||
local values = {packet.OnServerInvoke(player, ...)}
|
||||
if player.Parent == nil then return end
|
||||
Import(playerCursors[player] or {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0})
|
||||
WriteU8(packet.Id)
|
||||
WriteU8(threadIndex + 128)
|
||||
WriteParameters(packet.ResponseWrites, values)
|
||||
playerCursors[player] = Export()
|
||||
end
|
||||
|
||||
local onServerEvent = function(player: Player, receivedBuffer: buffer, instances: {Instance}?)
|
||||
local bytes = (playerBytes[player] or 0) + math.max(buffer.len(receivedBuffer), 800)
|
||||
if bytes > 8_000 then if RunService:IsStudio() then warn(player.Name, "is exceeding the data/rate limit; some events may be dropped") end return end
|
||||
playerBytes[player] = bytes
|
||||
Import({Buffer = receivedBuffer, BufferLength = buffer.len(receivedBuffer), BufferOffset = 0, Instances = instances or {}, InstancesOffset = 0})
|
||||
while Ended() == false do
|
||||
local packet = packets[ReadU8()]
|
||||
if packet.ResponseReads then
|
||||
local threadIndex = ReadU8()
|
||||
if threadIndex < 128 then
|
||||
Task:Defer(respond, packet, player, threadIndex, ReadParameters(packet.Reads))
|
||||
else
|
||||
threadIndex -= 128
|
||||
local threads = playerThreads[player][threadIndex]
|
||||
if threads then
|
||||
task.cancel(threads.Timeout)
|
||||
task.defer(threads.Yielded, ReadParameters(packet.ResponseReads))
|
||||
playerThreads[player][threadIndex] = nil
|
||||
elseif RunService:IsStudio() then
|
||||
warn("Response thread not found for packet:", packet.Name, "discarding response:", ReadParameters(packet.ResponseReads))
|
||||
else
|
||||
ReadParameters(packet.ResponseReads)
|
||||
end
|
||||
end
|
||||
else
|
||||
packet.OnServerEvent:Fire(player, ReadParameters(packet.Reads))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
remoteEvent.OnServerEvent:Connect(function(player: Player, ...)
|
||||
local success, errorMessage: string? = pcall(onServerEvent, player, ...)
|
||||
if errorMessage and RunService:IsStudio() then warn(player.Name, errorMessage) end
|
||||
end)
|
||||
|
||||
PlayersService.PlayerRemoving:Connect(function(player)
|
||||
playerCursors[player] = nil
|
||||
playerThreads[player] = nil
|
||||
playerBytes[player] = nil
|
||||
end)
|
||||
|
||||
RunService.Heartbeat:Connect(function(deltaTime) task.defer(thread) end)
|
||||
else
|
||||
threads = {Index = 0}
|
||||
remoteEvent = script:WaitForChild("RemoteEvent")
|
||||
local totalTime = 0
|
||||
|
||||
local thread = task.spawn(function()
|
||||
while true do
|
||||
coroutine.yield()
|
||||
if cursor.BufferOffset > 0 then
|
||||
local truncatedBuffer = buffer.create(cursor.BufferOffset)
|
||||
buffer.copy(truncatedBuffer, 0, cursor.Buffer, 0, cursor.BufferOffset)
|
||||
if cursor.InstancesOffset == 0 then
|
||||
remoteEvent:FireServer(truncatedBuffer)
|
||||
else
|
||||
remoteEvent:FireServer(truncatedBuffer, cursor.Instances)
|
||||
cursor.InstancesOffset = 0
|
||||
table.clear(cursor.Instances)
|
||||
end
|
||||
cursor.BufferOffset = 0
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local respond = function(packet: Packet, threadIndex: number, ...)
|
||||
if packet.OnClientInvoke == nil then warn("OnClientInvoke not found for packet:", packet.Name, "discarding event:", ...) return end
|
||||
local values = {packet.OnClientInvoke(...)}
|
||||
Import(cursor)
|
||||
WriteU8(packet.Id)
|
||||
WriteU8(threadIndex + 128)
|
||||
WriteParameters(packet.ResponseWrites, values)
|
||||
cursor = Export()
|
||||
end
|
||||
|
||||
remoteEvent.OnClientEvent:Connect(function(receivedBuffer: buffer, instances: {Instance}?)
|
||||
Import({Buffer = receivedBuffer, BufferLength = buffer.len(receivedBuffer), BufferOffset = 0, Instances = instances or {}, InstancesOffset = 0})
|
||||
while Ended() == false do
|
||||
local packet = packets[ReadU8()]
|
||||
if packet.ResponseReads then
|
||||
local threadIndex = ReadU8()
|
||||
if threadIndex < 128 then
|
||||
Task:Defer(respond, packet, threadIndex, ReadParameters(packet.Reads))
|
||||
else
|
||||
threadIndex -= 128
|
||||
local threads = threads[threadIndex]
|
||||
if threads then
|
||||
task.cancel(threads.Timeout)
|
||||
task.defer(threads.Yielded, ReadParameters(packet.ResponseReads))
|
||||
threads[threadIndex] = nil
|
||||
else
|
||||
warn("Response thread not found for packet:", packet.Name, "discarding response:", ReadParameters(packet.ResponseReads))
|
||||
end
|
||||
end
|
||||
else
|
||||
packet.OnClientEvent:Fire(ReadParameters(packet.Reads))
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
remoteEvent.AttributeChanged:Connect(function(name)
|
||||
local packet = packets[name]
|
||||
if packet then
|
||||
if packet.Id then packets[packet.Id] = nil end
|
||||
packet.Id = remoteEvent:GetAttribute(name)
|
||||
if packet.Id then packets[packet.Id] = packet end
|
||||
end
|
||||
end)
|
||||
|
||||
RunService.Heartbeat:Connect(function(deltaTime)
|
||||
totalTime += deltaTime
|
||||
if totalTime > 0.016666666666666666 then
|
||||
totalTime %= 0.016666666666666666
|
||||
task.defer(thread)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
return setmetatable(Types.Types, {__call = Constructor})
|
2203
src/Chemical/Packages/Promise.lua
Normal file
2203
src/Chemical/Packages/Promise.lua
Normal file
File diff suppressed because it is too large
Load diff
53
src/Chemical/Packages/Queue.lua
Normal file
53
src/Chemical/Packages/Queue.lua
Normal file
|
@ -0,0 +1,53 @@
|
|||
--!strict
|
||||
|
||||
--From Roblox docs
|
||||
|
||||
local Queue = {}
|
||||
Queue.__index = Queue
|
||||
|
||||
export type Queue<T> = typeof(setmetatable(
|
||||
{} :: {
|
||||
_first: number,
|
||||
_last: number,
|
||||
_queue: { T },
|
||||
},
|
||||
Queue
|
||||
))
|
||||
|
||||
function Queue.new<T>(): Queue<T>
|
||||
local self = setmetatable({
|
||||
_first = 0,
|
||||
_last = -1,
|
||||
_queue = {},
|
||||
}, Queue)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- Check if the queue is empty
|
||||
function Queue.isEmpty<T>(self: Queue<T>)
|
||||
return self._first > self._last
|
||||
end
|
||||
|
||||
-- Add a value to the queue
|
||||
function Queue.enqueue<T>(self: Queue<T>, value: T)
|
||||
local last = self._last + 1
|
||||
self._last = last
|
||||
self._queue[last] = value
|
||||
end
|
||||
|
||||
-- Remove a value from the queue
|
||||
function Queue.dequeue<T>(self: Queue<T>): T?
|
||||
if self:isEmpty() then
|
||||
return nil
|
||||
end
|
||||
|
||||
local first = self._first
|
||||
local value = self._queue[first]
|
||||
self._queue[first] = nil
|
||||
self._first = first + 1
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
return Queue
|
113
src/Chemical/Packages/Signals.lua
Normal file
113
src/Chemical/Packages/Signals.lua
Normal file
|
@ -0,0 +1,113 @@
|
|||
--- Lua-side duplication of the API of events on Roblox objects.
|
||||
-- Signals are needed for to ensure that for local events objects are passed by
|
||||
-- reference rather than by value where possible, as the BindableEvent objects
|
||||
-- always pass signal arguments by value, meaning tables will be deep copied.
|
||||
-- Roblox's deep copy method parses to a non-lua table compatable format.
|
||||
-- @classmod Signal
|
||||
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local ENABLE_TRACEBACK = false
|
||||
|
||||
local Signal = {}
|
||||
Signal.__index = Signal
|
||||
Signal.ClassName = "Signal"
|
||||
|
||||
--- Constructs a new signal.
|
||||
-- @constructor Signal.new()
|
||||
-- @treturn Signal
|
||||
function Signal.new()
|
||||
local self = setmetatable({}, Signal)
|
||||
|
||||
self._bindableEvent = Instance.new("BindableEvent")
|
||||
self._argMap = {}
|
||||
self._source = ENABLE_TRACEBACK and debug.traceback() or ""
|
||||
|
||||
-- Events in Roblox execute in reverse order as they are stored in a linked list and
|
||||
-- new connections are added at the head. This event will be at the tail of the list to
|
||||
-- clean up memory.
|
||||
self._bindableEvent.Event:Connect(function(key)
|
||||
self._argMap[key] = nil
|
||||
|
||||
-- We've been destroyed here and there's nothing left in flight.
|
||||
-- Let's remove the argmap too.
|
||||
-- This code may be slower than leaving this table allocated.
|
||||
if (not self._bindableEvent) and (not next(self._argMap)) then
|
||||
self._argMap = nil
|
||||
end
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Fire the event with the given arguments. All handlers will be invoked. Handlers follow
|
||||
-- Roblox signal conventions.
|
||||
-- @param ... Variable arguments to pass to handler
|
||||
-- @treturn nil
|
||||
function Signal:Fire(...)
|
||||
if not self._bindableEvent then
|
||||
warn(("Signal is already destroyed. %s"):format(self._source))
|
||||
return
|
||||
end
|
||||
|
||||
local args = table.pack(...)
|
||||
|
||||
-- TODO: Replace with a less memory/computationally expensive key generation scheme
|
||||
local key = HttpService:GenerateGUID(false)
|
||||
self._argMap[key] = args
|
||||
|
||||
-- Queues each handler onto the queue.
|
||||
self._bindableEvent:Fire(key)
|
||||
end
|
||||
|
||||
--- Connect a new handler to the event. Returns a connection object that can be disconnected.
|
||||
-- @tparam function handler Function handler called with arguments passed when `:Fire(...)` is called
|
||||
-- @treturn Connection Connection object that can be disconnected
|
||||
function Signal:Connect(handler)
|
||||
if not (type(handler) == "function") then
|
||||
error(("connect(%s)"):format(typeof(handler)), 2)
|
||||
end
|
||||
|
||||
return self._bindableEvent.Event:Connect(function(key)
|
||||
-- note we could queue multiple events here, but we'll do this just as Roblox events expect
|
||||
-- to behave.
|
||||
|
||||
local args = self._argMap[key]
|
||||
if args then
|
||||
handler(table.unpack(args, 1, args.n))
|
||||
else
|
||||
error("Missing arg data, probably due to reentrance.")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Wait for fire to be called, and return the arguments it was given.
|
||||
-- @treturn ... Variable arguments from connection
|
||||
function Signal:Wait()
|
||||
local key = self._bindableEvent.Event:Wait()
|
||||
local args = self._argMap[key]
|
||||
if args then
|
||||
return table.unpack(args, 1, args.n)
|
||||
else
|
||||
error("Missing arg data, probably due to reentrance.")
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Disconnects all connected events to the signal. Voids the signal as unusable.
|
||||
-- @treturn nil
|
||||
function Signal:Destroy()
|
||||
if self._bindableEvent then
|
||||
-- This should disconnect all events, but in-flight events should still be
|
||||
-- executed.
|
||||
|
||||
self._bindableEvent:Destroy()
|
||||
self._bindableEvent = nil
|
||||
end
|
||||
|
||||
-- Do not remove the argmap. It will be cleaned up by the cleanup connection.
|
||||
|
||||
setmetatable(self, nil)
|
||||
end
|
||||
|
||||
return Signal
|
237
src/Chemical/Packages/Trees.lua
Normal file
237
src/Chemical/Packages/Trees.lua
Normal file
|
@ -0,0 +1,237 @@
|
|||
-- Tree Library == Vibe coded by Sovereignty
|
||||
|
||||
export type Node<value = any> = {
|
||||
Key: string,
|
||||
Value: value,
|
||||
Children: {Node<value>},
|
||||
Parent: Node<value>?,
|
||||
FullPath: string,
|
||||
|
||||
new: (key: string, value: any?) -> Node<value>,
|
||||
AddChild: (self: Node<value>, key: string, value: any?) -> Node<value>,
|
||||
GetChild: (self: Node<value>, key: string) -> Node<value>?,
|
||||
GetChildren: (self: Node<value>) -> {Node<value>},
|
||||
GetAllDescendants: (self: Node<value>) -> {Node<value>},
|
||||
GetPath: (self: Node<value>) -> {string},
|
||||
SetValue: (self: Node<value>, value: any?) -> (),
|
||||
TraverseDFS: (self: Node<value>, callback: (node: Node<value>) -> ()) -> (),
|
||||
TraverseBFS: (self: Node<value>, callback: (node: Node<value>) -> ()) -> (),
|
||||
}
|
||||
|
||||
export type Tree<nodeValue = any> = {
|
||||
Root: Node<nodeValue>,
|
||||
|
||||
new: (rootKey: string?, rootValue: any?) -> (Tree<nodeValue>),
|
||||
AddNode: (self: Tree<nodeValue>, pathParts: {string}, value: any?) -> Node<nodeValue>,
|
||||
GetNode: (self: Tree<nodeValue>, pathParts: {string}) -> Node<nodeValue>?,
|
||||
GetNodeChildrenByPath: (self: Tree<nodeValue>, pathParts: {string}) -> {Node<nodeValue>},
|
||||
GetDescendantsByPath: (self: Tree<nodeValue>, pathParts: {string}) -> {Node<nodeValue>},
|
||||
FindNode: (self: Tree<nodeValue>, predicate: (node: Node<nodeValue>) -> boolean) -> Node<nodeValue>?,
|
||||
RemoveNode: (self: Tree<nodeValue>, pathParts: {string}) -> boolean,
|
||||
UpdateNode: (self: Tree<nodeValue>, pathParts: {string}, newValue: any) -> boolean,
|
||||
GetPathString: (self: Tree<nodeValue>, pathParts: {string}) -> string,
|
||||
Traverse: (self: Tree<nodeValue>, method: "DFS" | "BFS", callback: (node: Node<nodeValue>) -> ()) -> (),
|
||||
Print: (self: Tree<nodeValue>) -> (),
|
||||
}
|
||||
|
||||
local Node = {} :: Node
|
||||
Node.__index = Node
|
||||
|
||||
function Node.new(key: string, value: any?): Node
|
||||
return setmetatable({
|
||||
Key = key,
|
||||
Value = value,
|
||||
Children = {} :: {Node},
|
||||
Parent = nil :: Node?,
|
||||
FullPath = ""
|
||||
}, Node) :: any
|
||||
end
|
||||
|
||||
function Node:AddChild(key: string, value: any?): Node
|
||||
local child = Node.new(key, value)
|
||||
child.Parent = self
|
||||
|
||||
if self.FullPath == "/" then
|
||||
child.FullPath = "/" .. key
|
||||
else
|
||||
child.FullPath = self.FullPath .. "/" .. key
|
||||
end
|
||||
|
||||
table.insert(self.Children, child)
|
||||
return child
|
||||
end
|
||||
|
||||
function Node:GetChild(key: string): Node?
|
||||
for _, child in ipairs(self.Children) do
|
||||
if child.Key == key then
|
||||
return child
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Node:GetChildren(): {Node}
|
||||
return self.Children
|
||||
end
|
||||
|
||||
function Node:GetAllDescendants(): {Node}
|
||||
local descendants = {} :: {Node}
|
||||
local function traverse(node: Node)
|
||||
for _, child in ipairs(node.Children) do
|
||||
table.insert(descendants, child)
|
||||
traverse(child)
|
||||
end
|
||||
end
|
||||
traverse(self)
|
||||
return descendants
|
||||
end
|
||||
|
||||
function Node:GetPath(): {string}
|
||||
local parts = {} :: {string}
|
||||
local current: Node? = self
|
||||
while current do
|
||||
table.insert(parts, 1, current.Key)
|
||||
current = current.Parent
|
||||
end
|
||||
return parts
|
||||
end
|
||||
|
||||
function Node:SetValue(value: any?)
|
||||
self.Value = value
|
||||
end
|
||||
|
||||
function Node:TraverseDFS(callback: (node: Node) -> ())
|
||||
callback(self)
|
||||
for _, child in ipairs(self.Children) do
|
||||
child:TraverseDFS(callback)
|
||||
end
|
||||
end
|
||||
|
||||
function Node:TraverseBFS(callback: (node: Node) -> ())
|
||||
local queue = {self} :: {Node}
|
||||
while #queue > 0 do
|
||||
local current = table.remove(queue, 1)
|
||||
callback(current)
|
||||
for _, child in ipairs(current.Children) do
|
||||
table.insert(queue, child)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local Tree = {} :: Tree
|
||||
Tree.__index = Tree
|
||||
|
||||
function Tree.new(rootKey: string?, rootValue: any?): Tree
|
||||
local rootKey = rootKey or "/"
|
||||
local root = Node.new(rootKey, rootValue)
|
||||
|
||||
root.FullPath = rootKey == "/" and "/" or "/" .. rootKey
|
||||
|
||||
return setmetatable({
|
||||
Root = root
|
||||
}, Tree) :: Tree
|
||||
end
|
||||
|
||||
function Tree:AddNode(pathParts: {string}, value: any?): Node
|
||||
local current: Node = self.Root
|
||||
|
||||
for _, part in ipairs(pathParts) do
|
||||
local child = current:GetChild(part)
|
||||
|
||||
if not child then
|
||||
child = current:AddChild(part, nil)
|
||||
end
|
||||
|
||||
current = child
|
||||
end
|
||||
|
||||
current.Value = value
|
||||
return current
|
||||
end
|
||||
|
||||
function Tree:GetNode(pathParts: {string}): Node?
|
||||
local current: Node? = self.Root
|
||||
for _, part in ipairs(pathParts) do
|
||||
local nextNode = current:GetChild(part)
|
||||
if not nextNode then break end
|
||||
current = nextNode
|
||||
end
|
||||
return current ~= self.Root and current
|
||||
end
|
||||
|
||||
function Tree:GetNodeChildrenByPath(pathParts: {string}): {Node}
|
||||
local node = self:GetNode(pathParts)
|
||||
return node and node:GetChildren() or {}
|
||||
end
|
||||
|
||||
function Tree:GetDescendantsByPath(pathParts: {string}): {Node}
|
||||
local node = self:GetNode(pathParts)
|
||||
return node and node:GetAllDescendants() or {}
|
||||
end
|
||||
|
||||
function Tree:FindNode(predicate: (node: Node) -> boolean): Node?
|
||||
local found: Node? = nil
|
||||
self.Root:TraverseDFS(function(node)
|
||||
if predicate(node) then
|
||||
found = node
|
||||
end
|
||||
end)
|
||||
return found
|
||||
end
|
||||
|
||||
function Tree:RemoveNode(pathParts: {string}): boolean
|
||||
local node = self:GetNode(pathParts)
|
||||
if not node or node == self.Root then return false end
|
||||
|
||||
local parent = node.Parent
|
||||
if not parent then return false end
|
||||
|
||||
for i, child in ipairs(parent.Children) do
|
||||
if child == node then
|
||||
table.remove(parent.Children, i)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Tree:UpdateNode(pathParts: {string}, newValue: any): boolean
|
||||
local node = self:GetNode(pathParts)
|
||||
if node then
|
||||
node.Value = newValue
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Tree:GetPathString(pathParts: {string}): string
|
||||
return "/" .. table.concat(pathParts, "/")
|
||||
end
|
||||
|
||||
function Tree:Traverse(method: "DFS" | "BFS", callback: (node: Node) -> ())
|
||||
if method == "DFS" then
|
||||
self.Root:TraverseDFS(callback)
|
||||
elseif method == "BFS" then
|
||||
self.Root:TraverseBFS(callback)
|
||||
else
|
||||
error("Invalid traversal method. Use 'DFS' or 'BFS'")
|
||||
end
|
||||
end
|
||||
|
||||
function Tree:Print()
|
||||
print("Tree Structure:")
|
||||
self.Root:TraverseDFS(function(node)
|
||||
local indent = string.rep(" ", #node:GetPath() - 1)
|
||||
|
||||
-- Fix: Format root path correctly
|
||||
local displayPath = node.FullPath
|
||||
if node == self.Root and node.Key == "/" then
|
||||
displayPath = "/"
|
||||
end
|
||||
|
||||
print(indent .. node.Key .. " (" .. displayPath .. ")")
|
||||
end)
|
||||
end
|
||||
|
||||
return Tree
|
3
src/Chemical/Packages/init.meta.json
Normal file
3
src/Chemical/Packages/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
323
src/Chemical/Singletons/Reactor.lua
Normal file
323
src/Chemical/Singletons/Reactor.lua
Normal file
|
@ -0,0 +1,323 @@
|
|||
--[[
|
||||
@module Reactor
|
||||
|
||||
This module facilitates the creation and replication of stateful objects, called "Reactions,"
|
||||
from the server to clients. It is designed to be used in an ECS (Entity-Component System)
|
||||
environment.
|
||||
|
||||
- A "Reactor" is a factory for creating "Reactions" of a specific type.
|
||||
- A "Reaction" is a state object identified by a name and a unique key.
|
||||
- State changes within a Reaction are automatically replicated to the appropriate clients.
|
||||
- It uses a tokenization system to minimize network bandwidth for property names and paths.
|
||||
|
||||
Yes, the documentation was generated.
|
||||
]]
|
||||
|
||||
|
||||
local RunService = game:GetService("RunService")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
|
||||
local Packet = require(RootFolder.Packages.Packet)
|
||||
local Signal = require(RootFolder.Packages.Signals)
|
||||
local Promise = require(RootFolder.Packages.Promise)
|
||||
local Queue = require(RootFolder.Packages.Queue)
|
||||
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
local Cache = require(RootFolder.Cache)
|
||||
local Symbols = require(RootFolder.Symbols)
|
||||
local Effect = require(RootFolder.Factories.Effect)
|
||||
local Reaction = require(RootFolder.Factories.Reaction)
|
||||
local Array = require(RootFolder.Functions.Array)
|
||||
local Blueprint = require(RootFolder.Functions.Blueprint)
|
||||
local Is = require(RootFolder.Functions.Is)
|
||||
|
||||
|
||||
type Player = { UserId: number }
|
||||
type Symbol = Symbols.Symbol
|
||||
type Reaction<T> = Reaction.Reaction<T> & { To: { Player } | Symbol, Name: string, Key: string }
|
||||
|
||||
|
||||
local network = {
|
||||
Create = Packet("C__Create", Packet.String, Packet.String, { Packet.NumberU8 }, Packet.Any, Packet.Any),
|
||||
Update = Packet("C__Update", { Packet.NumberU8 }, { Packet.NumberU8 }, Packet.Any),
|
||||
UpdateChanges = Packet("C__UpdateChange", { Packet.NumberU8 }, { Packet.NumberU8 }, Packet.Any), -- TODO
|
||||
Destroy = Packet("C__Destroy", { Packet.NumberU8 }),
|
||||
}
|
||||
|
||||
local Tokens = Cache.Tokens.new()
|
||||
local Reactions: { [string]: { [string]: Reaction<any> } } = {}
|
||||
local ReactionQueues: { [string]: { [string]: Queue.Queue<{ { number } | any }> } } = {}
|
||||
local OnReactionCreated = Signal.new()
|
||||
|
||||
|
||||
|
||||
--- Recursively walks a table structure and creates a map of string keys to numerical tokens.
|
||||
-- This is used to prepare a state object for replication, ensuring the client can reconstruct it.
|
||||
local function createPathTokenMap(
|
||||
snapshotTable: { [any]: any },
|
||||
originalTable: { [any]: any }
|
||||
): { [string]: number }
|
||||
local result = {}
|
||||
|
||||
for key, snapshotValue in pairs(snapshotTable) do
|
||||
local originalValue = originalTable[key]
|
||||
|
||||
|
||||
if typeof(key) ~= "string" then
|
||||
continue
|
||||
end
|
||||
|
||||
|
||||
result[key] = Tokens:ToToken(key)
|
||||
|
||||
|
||||
if Is.Array(snapshotValue) and Is.Array(originalValue) then
|
||||
for k, token in createPathTokenMap(snapshotValue, originalValue) do
|
||||
result[k] = token
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- Sends a network packet to a specified target (all players or a list of players).
|
||||
local AllPlayers = Symbols.All("Players")
|
||||
local function sendToTarget(target: { Player } | Symbol, packet: any, ...: any)
|
||||
if target == AllPlayers then
|
||||
packet:Fire(...)
|
||||
else
|
||||
for _, player in target :: { Player } do
|
||||
packet:FireClient(player, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Handles the server-side logic for replicating a Reaction to clients.
|
||||
local function replicate(reaction: Reaction<any>)
|
||||
local name = reaction.Name
|
||||
local key = reaction.Key
|
||||
local target = reaction.To
|
||||
|
||||
|
||||
local nameToken = Tokens:ToToken(name)
|
||||
local keyToken = Tokens:ToToken(key)
|
||||
local reactionIdTokens = { nameToken, keyToken }
|
||||
|
||||
local blueprint = reaction:blueprint()
|
||||
local pathTokenMap = createPathTokenMap(reaction:snapshot(), reaction:get())
|
||||
|
||||
|
||||
sendToTarget(target, network.Create, name, key, reactionIdTokens, pathTokenMap, blueprint)
|
||||
|
||||
|
||||
Array.Walk(reaction:get(), function(path: { string }, value: any)
|
||||
if Is.Stateful(value) then
|
||||
local pathTokens = Tokens:ToTokenPath(path)
|
||||
|
||||
local eff = Effect(function()
|
||||
sendToTarget(target, network.Update, reactionIdTokens, pathTokens, value:get())
|
||||
end)
|
||||
|
||||
|
||||
ECS.World:add(eff.entity, ECS.JECS.pair(ECS.Tags.InScope, reaction.entity))
|
||||
end
|
||||
end)
|
||||
|
||||
-- Patch the reaction's cleanup to notify clients of its destruction.
|
||||
ECS.World:set(reaction.entity, ECS.Components.CleanupFn, function()
|
||||
if Reactions[name] and Reactions[name][key] then
|
||||
Reactions[name][key] = nil
|
||||
end
|
||||
sendToTarget(target, network.Destroy, reactionIdTokens)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
if RunService:IsClient() then
|
||||
--- Reconstructs a Reaction on the client based on data from the server.
|
||||
local function reconstruct(name: string, key: string, reactionIdTokens: { number }, pathTokenMap: { [string]: number }, blueprint: { string: {T: number, V: any} }): ()
|
||||
if Reactions[name] and Reactions[name][key] then
|
||||
return
|
||||
end
|
||||
|
||||
-- Map the incoming tokens so we can translate them back to strings later.
|
||||
Tokens:Map({ [name] = reactionIdTokens[1], [key] = reactionIdTokens[2] })
|
||||
Tokens:Map(pathTokenMap)
|
||||
|
||||
-- Create the local version of the Reaction
|
||||
local reaction = Reaction(name, key, Blueprint:Read(blueprint :: any))
|
||||
reaction.Name = name
|
||||
reaction.Key = key
|
||||
|
||||
if not Reactions[name] then
|
||||
Reactions[name] = {}
|
||||
end
|
||||
Reactions[name][key] = reaction
|
||||
|
||||
-- Process any queued updates that arrived before this creation packet.
|
||||
if ReactionQueues[name] and ReactionQueues[name][key] then
|
||||
local queue = ReactionQueues[name][key]
|
||||
while not queue:isEmpty() do
|
||||
local args = queue:dequeue()
|
||||
local pathTokens, value = table.unpack(args)
|
||||
local path = Tokens:FromPath(pathTokens)
|
||||
local statefulValue = Array.FindOnPath(reaction:get(), path)
|
||||
if statefulValue and statefulValue.set then
|
||||
statefulValue:set(value)
|
||||
end
|
||||
end
|
||||
ReactionQueues[name][key] = nil -- Clear the queue
|
||||
end
|
||||
|
||||
OnReactionCreated:Fire(name, key, reaction)
|
||||
end
|
||||
|
||||
--- Applies a state update from the server to a local Reaction.
|
||||
local function update(reactionIdTokens: { number }, pathTokens: { number }, value: any)
|
||||
local name = Tokens:From(reactionIdTokens[1])
|
||||
local key = Tokens:From(reactionIdTokens[2])
|
||||
local path = Tokens:FromPath(pathTokens)
|
||||
|
||||
if not name or not key then return end
|
||||
|
||||
local reaction = Reactions[name] and Reactions[name][key]
|
||||
|
||||
-- If the reaction doesn't exist yet, queue the update.
|
||||
if not reaction then
|
||||
if not ReactionQueues[name] then ReactionQueues[name] = {} end
|
||||
if not ReactionQueues[name][key] then ReactionQueues[name][key] = Queue.new() end
|
||||
|
||||
ReactionQueues[name][key]:enqueue({ pathTokens, value })
|
||||
return
|
||||
end
|
||||
|
||||
if reaction.__destroyed then return end
|
||||
|
||||
-- Apply the update
|
||||
local container = reaction:get()
|
||||
local statefulValue = Array.FindOnPath(container, path)
|
||||
if statefulValue and statefulValue.set then
|
||||
statefulValue:set(value)
|
||||
end
|
||||
end
|
||||
|
||||
--- Destroys a local Reaction when notified by the server.
|
||||
local function destroy(reactionIdTokens: { number })
|
||||
local name = Tokens:From(reactionIdTokens[1])
|
||||
local key = Tokens:From(reactionIdTokens[2])
|
||||
|
||||
if not name or not key then return end
|
||||
|
||||
local reaction = Reactions[name] and Reactions[name][key]
|
||||
|
||||
if not reaction or not reaction.entity then return end
|
||||
|
||||
reaction:destroy()
|
||||
Reactions[name][key] = nil
|
||||
end
|
||||
|
||||
-- Connect client network events to their handler functions
|
||||
network.Create.OnClientEvent:Connect(reconstruct)
|
||||
network.Update.OnClientEvent:Connect(update)
|
||||
network.Destroy.OnClientEvent:Connect(destroy)
|
||||
else
|
||||
Players.PlayerAdded:Connect(function(player: Player)
|
||||
for _, keyedReactions in Reactions do
|
||||
for _, reaction in keyedReactions do
|
||||
if reaction.To == Symbols.All("Players") or table.find(reaction.To, player) then
|
||||
replicate(reaction)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--// Public API
|
||||
local api = {}
|
||||
|
||||
--- Awaits the creation of a specific Reaction on the client.
|
||||
-- @param name The name of the Reactor that creates the Reaction.
|
||||
-- @param key The unique key of the Reaction.
|
||||
-- @return A promise that resolves with the Reaction once it's created.
|
||||
function api.await<T>(name: string, key: string): Reaction<T>
|
||||
if Reactions[name] and Reactions[name][key] then
|
||||
return Reactions[name][key]
|
||||
end
|
||||
|
||||
return Promise.fromEvent(OnReactionCreated, function(n: string, k: string, _)
|
||||
return name == n and key == k
|
||||
end):andThen(function(...)
|
||||
return select(3, ...) -- Return the reaction object
|
||||
end):expect()
|
||||
end
|
||||
|
||||
--- Listens for the creation of any Reaction from a specific Reactor.
|
||||
-- @param name The name of the Reactor.
|
||||
-- @param callback A function to call with the key and Reaction object.
|
||||
-- @return A connection object with a :Disconnect() method.
|
||||
function api.onCreate(name: string, callback: (key: string, reaction: Reaction<any>) -> ())
|
||||
return OnReactionCreated:Connect(function(n: string, k: string, reaction: Reaction<any>)
|
||||
if n == name then
|
||||
task.spawn(callback, k, reaction)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Creates a "Reactor" factory.
|
||||
-- @param config A configuration table with a 'Name' and optional 'Subjects' (players).
|
||||
-- @param constructor A function that returns the initial state for a new Reaction.
|
||||
-- @return A Reactor object with `create` and `await` methods.
|
||||
return function<T, U...>(
|
||||
config: { Name: string, Subjects: { Player } | Symbol? },
|
||||
constructor: (key: string, U...) -> T
|
||||
)
|
||||
local name = config.Name
|
||||
assert(name, "Reactor config must include a 'Name'.")
|
||||
|
||||
local to = config.Subjects or Symbols.All("Players")
|
||||
local reactor = {}
|
||||
|
||||
--- Creates and replicates a new Reaction. [SERVER-ONLY]
|
||||
-- @param self The reactor object.
|
||||
-- @param key A unique key for this Reaction.
|
||||
-- @param ... Additional arguments to be passed to the constructor.
|
||||
-- @return The created Reaction instance.
|
||||
function reactor:create(key: string, ...: U...): Reaction<T>
|
||||
assert(not RunService:IsClient(), "Reactions can only be created on the server.")
|
||||
if Reactions[name] and Reactions[name][key] then
|
||||
warn(string.format("Reactor '%s' is overwriting an existing reaction with key '%s'", name, key))
|
||||
end
|
||||
|
||||
local reaction = Reaction(name, key, constructor(key, ...))
|
||||
reaction.To = to
|
||||
reaction.Name = name
|
||||
reaction.Key = key
|
||||
|
||||
if not Reactions[name] then
|
||||
Reactions[name] = {}
|
||||
end
|
||||
Reactions[name][key] = reaction
|
||||
|
||||
-- The new, encapsulated replicate function handles all server-side logic.
|
||||
replicate(reaction)
|
||||
|
||||
return reaction
|
||||
end
|
||||
|
||||
--- Awaits a specific Reaction from this Reactor. [CLIENT-ONLY]
|
||||
function reactor:await(key: string): Reaction<T>
|
||||
return api.await(name, key)
|
||||
end
|
||||
|
||||
--- Listens for new Reactions created by this Reactor. [CLIENT-ONLY]
|
||||
function reactor:onCreate(callback: (key: string, reaction: Reaction<T>) -> ())
|
||||
return api.onCreate(name, callback)
|
||||
end
|
||||
|
||||
return reactor
|
||||
end
|
95
src/Chemical/Singletons/Scheduler.lua
Normal file
95
src/Chemical/Singletons/Scheduler.lua
Normal file
|
@ -0,0 +1,95 @@
|
|||
--!native
|
||||
--!optimize 2
|
||||
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local RootFolder = script.Parent.Parent
|
||||
|
||||
local ECS = require(RootFolder.ECS)
|
||||
|
||||
local Components = ECS.Components
|
||||
local Tags = ECS.Tags
|
||||
local JECS = ECS.JECS
|
||||
local World = ECS.World
|
||||
|
||||
local MAX_COMPUTATION_DEPTH = 100
|
||||
|
||||
local dirtySourceQuery = World:query(Components.Object)
|
||||
:with(Tags.IsStateful, Tags.IsDirty)
|
||||
:without(Tags.IsEffect, Tags.IsComputed)
|
||||
:cached()
|
||||
|
||||
local dirtyComputedsQuery = World:query(Components.Object)
|
||||
:with(Tags.IsComputed, Tags.IsDirty)
|
||||
:cached()
|
||||
|
||||
local dirtyEffectsQuery = World:query(Components.Object)
|
||||
:with(Tags.IsEffect, Tags.IsDirty)
|
||||
:cached()
|
||||
|
||||
|
||||
local GetSubscribers = require(RootFolder.Functions.GetSubscribers)
|
||||
|
||||
|
||||
local Scheduler = {}
|
||||
|
||||
function Scheduler:Update()
|
||||
-- PROPAGATE DIRTINESS
|
||||
for sourceEntity, _ in dirtySourceQuery:iter() do
|
||||
local subscribers = GetSubscribers(sourceEntity)
|
||||
|
||||
for _, subscriberEntity in ipairs(subscribers) do
|
||||
if not World:has(subscriberEntity, Tags.IsDirty) then
|
||||
World:add(subscriberEntity, Tags.IsDirty)
|
||||
end
|
||||
end
|
||||
|
||||
World:remove(sourceEntity, Tags.IsDirty)
|
||||
end
|
||||
|
||||
-- RE-RUN COMPUTED VALUES
|
||||
for i = 1, MAX_COMPUTATION_DEPTH do
|
||||
local computedsToProcess = {}
|
||||
|
||||
for entity, computable in dirtyComputedsQuery:iter() do
|
||||
table.insert(computedsToProcess, computable)
|
||||
end
|
||||
|
||||
if #computedsToProcess == 0 then
|
||||
break
|
||||
end
|
||||
|
||||
for _, computable in ipairs(computedsToProcess) do
|
||||
|
||||
computable:compute()
|
||||
World:remove(computable.entity, Tags.IsDirty)
|
||||
|
||||
for _, subscriber in ipairs(GetSubscribers(computable.entity)) do
|
||||
World:add(subscriber, Tags.IsDirty)
|
||||
end
|
||||
end
|
||||
|
||||
if i == MAX_COMPUTATION_DEPTH then
|
||||
warn("Chemical: Max computation depth exceeded. Check for a circular dependency in your Computed values.")
|
||||
end
|
||||
end
|
||||
|
||||
-- RUN EFFECTS & OBSERVERS
|
||||
for _, runnable in dirtyEffectsQuery:iter() do
|
||||
runnable:run()
|
||||
World:remove(runnable.entity, Tags.IsDirty)
|
||||
end
|
||||
end
|
||||
|
||||
if RunService:IsServer() then
|
||||
RunService.Heartbeat:Connect(function()
|
||||
Scheduler:Update()
|
||||
end)
|
||||
else
|
||||
RunService.RenderStepped:Connect(function()
|
||||
Scheduler:Update()
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
return Scheduler
|
3
src/Chemical/Singletons/init.meta.json
Normal file
3
src/Chemical/Singletons/init.meta.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
26
src/Chemical/Symbols.lua
Normal file
26
src/Chemical/Symbols.lua
Normal file
|
@ -0,0 +1,26 @@
|
|||
local RootFolder = script.Parent
|
||||
|
||||
local GuiTypes = require(RootFolder.Types.Gui)
|
||||
local GetSymbol = require(RootFolder.Functions.GetSymbol)
|
||||
|
||||
export type Symbol<S = string, T = string> = GetSymbol.Symbol<S, T>
|
||||
|
||||
local module = {}
|
||||
|
||||
module.OnEvent = function(eventName: GuiTypes.EventNames)
|
||||
return GetSymbol(eventName, "Event")
|
||||
end
|
||||
|
||||
module.OnChange = function(eventName: GuiTypes.PropertyNames)
|
||||
return GetSymbol(eventName, "Change")
|
||||
end
|
||||
|
||||
module.Children = GetSymbol("All", "Children")
|
||||
|
||||
module.All = function<S>(subjects: S)
|
||||
return GetSymbol(subjects, "All") :: Symbol<S, "All">
|
||||
end
|
||||
|
||||
--OnEvent symbols are handled by the OnEvent function.
|
||||
|
||||
return module
|
360
src/Chemical/Types/Gui.lua
Normal file
360
src/Chemical/Types/Gui.lua
Normal file
|
@ -0,0 +1,360 @@
|
|||
type Stateful<T> = { set: (T) -> (), get: () -> (T), __entity: number }
|
||||
|
||||
export type GuiBaseProperties = {
|
||||
Name: (Stateful<string> | string)?,
|
||||
Visible: (Stateful<boolean> | boolean)?,
|
||||
Active: (Stateful<boolean> | boolean)?,
|
||||
AnchorPoint: (Stateful<Vector2> | Vector2)?,
|
||||
Position: (Stateful<UDim2> | UDim2)?,
|
||||
Size: (Stateful<UDim2> | UDim2)?,
|
||||
Rotation: (Stateful<number> | number)?,
|
||||
ZIndex: (Stateful<number> | number)?,
|
||||
LayoutOrder: (Stateful<number> | number)?,
|
||||
BackgroundTransparency: (Stateful<number> | number)?,
|
||||
BackgroundColor3: (Stateful<Color3> | Color3)?,
|
||||
BorderSizePixel: (Stateful<number> | number)?,
|
||||
BorderColor3: (Stateful<Color3> | Color3)?,
|
||||
ClipsDescendants: (Stateful<boolean> | boolean)?,
|
||||
Selectable: (Stateful<boolean> | boolean)?,
|
||||
Parent: GuiObject?,
|
||||
Children: { [number]: Instance | Stateful<GuiObject> },
|
||||
|
||||
[any]: (() -> ())?
|
||||
}
|
||||
|
||||
type GuiBaseEvents = {
|
||||
InputBegan: (input: InputObject, gameProcessed: boolean) -> (),
|
||||
InputEnded: (input: InputObject, gameProcessed: boolean) -> (),
|
||||
InputChanged: (input: InputObject, gameProcessed: boolean) -> (),
|
||||
|
||||
-- Mouse Events
|
||||
MouseEnter: () -> (),
|
||||
MouseLeave: () -> (),
|
||||
MouseMoved: (deltaX: number, deltaY: number) -> (),
|
||||
MouseWheelForward: (scrollDelta: number) -> (),
|
||||
MouseWheelBackward: (scrollDelta: number) -> (),
|
||||
|
||||
-- Touch Events
|
||||
TouchTap: (touchPositions: {Vector2}, state: Enum.UserInputState) -> (),
|
||||
TouchPinch: (scale: number, velocity: number, state: Enum.UserInputState) -> (),
|
||||
TouchPan: (pan: Vector2, velocity: Vector2, state: Enum.UserInputState) -> (),
|
||||
TouchSwipe: (direction: Enum.SwipeDirection, touches: number) -> (),
|
||||
TouchRotate: (rotation: number, velocity: number, state: Enum.UserInputState) -> (),
|
||||
TouchLongPress: (duration: number) -> (),
|
||||
|
||||
-- Console/Selection Events
|
||||
SelectionGained: () -> (),
|
||||
SelectionLost: () -> (),
|
||||
SelectionChanged: (newSelection: Instance) -> (),
|
||||
}
|
||||
|
||||
type ImageGuiProperties = GuiBaseProperties & {
|
||||
Image: (Stateful<string> | string)?,
|
||||
ImageColor3: (Stateful<Color3> | Color3)?,
|
||||
ImageTransparency: (Stateful<number> | number)?,
|
||||
ScaleType: (Stateful<Enum.ScaleType> | Enum.ScaleType)?,
|
||||
SliceCenter: (Stateful<Rect> | Rect)?,
|
||||
TileSize: (Stateful<UDim2> | UDim2)?,
|
||||
ResampleMode: (Stateful<Enum.ResamplerMode> | Enum.ResamplerMode)?,
|
||||
}
|
||||
|
||||
type TextGuiProperties = GuiBaseProperties & {
|
||||
Text: (Stateful<string> | string)?,
|
||||
TextColor3: (Stateful<Color3> | Color3)?,
|
||||
TextTransparency: (Stateful<number> | number)?,
|
||||
TextStrokeColor3: (Stateful<Color3> | Color3)?,
|
||||
TextStrokeTransparency: (Stateful<number> | number)?,
|
||||
TextScaled: (Stateful<boolean> | boolean)?,
|
||||
TextSize: (Stateful<number> | number)?,
|
||||
TextWrapped: (Stateful<boolean> | boolean)?,
|
||||
FontFace: (Stateful<Font> | Font)?,
|
||||
LineHeight: (Stateful<number> | number)?,
|
||||
RichText: (Stateful<boolean> | boolean)?,
|
||||
TextXAlignment: (Stateful<Enum.TextXAlignment> | Enum.TextXAlignment)?,
|
||||
TextYAlignment: (Stateful<Enum.TextYAlignment> | Enum.TextYAlignment)?,
|
||||
TextTruncate: (Stateful<Enum.TextTruncate> | Enum.TextTruncate)?,
|
||||
[any]: (() -> ())?,
|
||||
}
|
||||
|
||||
export type FrameProperties = GuiBaseProperties
|
||||
export type TextLabelProperties = TextGuiProperties
|
||||
export type ImageLabelProperties = ImageGuiProperties
|
||||
|
||||
-- Interactive Elements
|
||||
type ButtonEvents = GuiBaseEvents & {
|
||||
Activated: (inputType: Enum.UserInputType?) -> (),
|
||||
MouseButton1Click: () -> (),
|
||||
MouseButton2Click: () -> (),
|
||||
MouseButton2Down: () -> (),
|
||||
MouseButton2Up: () -> (),
|
||||
|
||||
MouseWheelForward: nil,
|
||||
MouseWheelBackward: nil,
|
||||
}
|
||||
|
||||
export type ButtonProperties = {
|
||||
AutoButtonColor: (Stateful<boolean> | boolean)?,
|
||||
Modal: (Stateful<boolean> | boolean)?,
|
||||
Selected: (Stateful<boolean> | boolean)?,
|
||||
|
||||
ButtonHoverStyle: (Stateful<Enum.ButtonStyle> | Enum.ButtonStyle)?,
|
||||
ButtonPressStyle: (Stateful<Enum.ButtonStyle> | Enum.ButtonStyle)?,
|
||||
ActivationBehavior: (Stateful<Enum.ActivationBehavior> | Enum.ActivationBehavior)?,
|
||||
|
||||
SelectionGroup: (Stateful<number> | number)?,
|
||||
SelectionBehaviorUp: (Stateful<Enum.SelectionBehavior> | Enum.SelectionBehavior)?,
|
||||
SelectionBehaviorDown: (Stateful<Enum.SelectionBehavior> | Enum.SelectionBehavior)?,
|
||||
SelectionBehaviorLeft: (Stateful<Enum.SelectionBehavior> | Enum.SelectionBehavior)?,
|
||||
SelectionBehaviorRight: (Stateful<Enum.SelectionBehavior> | Enum.SelectionBehavior)?,
|
||||
GamepadPriority: (Stateful<number> | number)?,
|
||||
}
|
||||
|
||||
|
||||
export type TextButtonProperties = TextGuiProperties & ButtonProperties
|
||||
export type ImageButtonProperties = ImageGuiProperties & ButtonProperties
|
||||
|
||||
type TextBoxEvents = GuiBaseEvents & {
|
||||
FocusLost: (enterPressed: boolean) -> (),
|
||||
FocusGained: () -> (),
|
||||
TextChanged: (text: string) -> (),
|
||||
}
|
||||
|
||||
export type TextBoxProperties = TextGuiProperties & {
|
||||
ClearTextOnFocus: (Stateful<boolean> | boolean)?,
|
||||
MultiLine: (Stateful<boolean> | boolean)?,
|
||||
PlaceholderText: (Stateful<string> | string)?,
|
||||
PlaceholderColor3: (Stateful<Color3> | Color3)?,
|
||||
CursorPosition: (Stateful<number> | number)?,
|
||||
SelectionStart: (Stateful<number> | number)?,
|
||||
ShowNativeInput: (Stateful<boolean> | boolean)?,
|
||||
TextInputType: (Stateful<Enum.TextInputType> | Enum.TextInputType)?,
|
||||
}
|
||||
|
||||
|
||||
-- Containers
|
||||
type ScrollingFrameEvents = GuiBaseEvents & {
|
||||
Scrolled: (scrollVelocity: Vector2) -> (),
|
||||
}
|
||||
|
||||
export type ScrollingFrameProperties = FrameProperties & {
|
||||
ScrollBarImageColor3: (Stateful<Color3> | Color3)?,
|
||||
ScrollBarThickness: (Stateful<number> | number)?,
|
||||
ScrollingDirection: (Stateful<Enum.ScrollingDirection> | Enum.ScrollingDirection)?,
|
||||
CanvasSize: (Stateful<UDim2> | UDim2)?,
|
||||
CanvasPosition: (Stateful<Vector2> | Vector2)?,
|
||||
AutomaticCanvasSize: (Stateful<Enum.AutomaticSize> | Enum.AutomaticSize)?,
|
||||
VerticalScrollBarInset: (Stateful<Enum.ScrollBarInset> | Enum.ScrollBarInset)?,
|
||||
HorizontalScrollBarInset: (Stateful<Enum.ScrollBarInset> | Enum.ScrollBarInset)?,
|
||||
ScrollBarImageTransparency: (Stateful<number> | number)?,
|
||||
ElasticBehavior: (Stateful<Enum.ElasticBehavior> | Enum.ElasticBehavior)?,
|
||||
VerticalScrollBarPosition: (Stateful<Enum.VerticalScrollBarPosition> | Enum.VerticalScrollBarPosition)?,
|
||||
}
|
||||
|
||||
type ViewportFrameEvents = GuiBaseEvents & {
|
||||
ViewportResized: (newSize: Vector2) -> (),
|
||||
CameraChanged: (newCamera: Camera) -> (),
|
||||
}
|
||||
|
||||
export type ViewportFrameProperties = FrameProperties & {
|
||||
CurrentCamera: (Stateful<Camera> | Camera)?,
|
||||
ImageColor3: (Stateful<Color3> | Color3)?,
|
||||
LightColor: (Stateful<Color3> | Color3)?,
|
||||
LightDirection: (Stateful<Vector3> | Vector3)?,
|
||||
Ambient: (Stateful<Color3> | Color3)?,
|
||||
LightAngularInfluence: (Stateful<number> | number)?,
|
||||
}
|
||||
|
||||
-- Layouts
|
||||
export type UIListLayoutProperties = {
|
||||
Padding: (Stateful<UDim> | UDim)?,
|
||||
FillDirection: (Stateful<Enum.FillDirection> | Enum.FillDirection)?,
|
||||
HorizontalAlignment: (Stateful<Enum.HorizontalAlignment> | Enum.HorizontalAlignment)?,
|
||||
VerticalAlignment: (Stateful<Enum.VerticalAlignment> | Enum.VerticalAlignment)?,
|
||||
SortOrder: (Stateful<Enum.SortOrder> | Enum.SortOrder)?,
|
||||
Appearance: (Stateful<Enum.Appearance> | Enum.Appearance)?,
|
||||
}
|
||||
|
||||
export type UIGridLayoutProperties = {
|
||||
CellSize: (Stateful<UDim2> | UDim2)?,
|
||||
CellPadding: (Stateful<UDim2> | UDim2)?,
|
||||
StartCorner: (Stateful<Enum.StartCorner> | Enum.StartCorner)?,
|
||||
FillDirection: (Stateful<Enum.FillDirection> | Enum.FillDirection)?,
|
||||
HorizontalAlignment: (Stateful<Enum.HorizontalAlignment> | Enum.HorizontalAlignment)?,
|
||||
VerticalAlignment: (Stateful<Enum.VerticalAlignment> | Enum.VerticalAlignment)?,
|
||||
SortOrder: (Stateful<Enum.SortOrder> | Enum.SortOrder)?,
|
||||
}
|
||||
|
||||
-- Style Elements
|
||||
export type UICornerProperties = {
|
||||
CornerRadius: (Stateful<UDim> | UDim)?,
|
||||
}
|
||||
|
||||
export type UIStrokeProperties = {
|
||||
Color: (Stateful<Color3> | Color3)?,
|
||||
Thickness: (Stateful<number> | number)?,
|
||||
Transparency: (Stateful<number> | number)?,
|
||||
Enabled: (Stateful<boolean> | boolean)?,
|
||||
ApplyStrokeMode: (Stateful<Enum.ApplyStrokeMode> | Enum.ApplyStrokeMode)?,
|
||||
LineJoinMode: (Stateful<Enum.LineJoinMode> | Enum.LineJoinMode)?,
|
||||
}
|
||||
|
||||
export type UIGradientProperties = {
|
||||
Color: (Stateful<ColorSequence> | ColorSequence)?,
|
||||
Transparency: (Stateful<NumberSequence> | NumberSequence)?,
|
||||
Offset: (Stateful<Vector2> | Vector2)?,
|
||||
Rotation: (Stateful<number> | number)?,
|
||||
Enabled: (Stateful<boolean> | boolean)?,
|
||||
}
|
||||
|
||||
export type UIPaddingProperties = {
|
||||
PaddingTop: (Stateful<UDim> | UDim)?,
|
||||
PaddingBottom: (Stateful<UDim> | UDim)?,
|
||||
PaddingLeft: (Stateful<UDim> | UDim)?,
|
||||
PaddingRight: (Stateful<UDim> | UDim)?,
|
||||
}
|
||||
|
||||
export type UIScaleProperties = {
|
||||
Scale: (Stateful<number> | number)?,
|
||||
}
|
||||
|
||||
|
||||
type CanvasMouseEvents = GuiBaseEvents & {
|
||||
MouseWheel: (direction: Enum.MouseWheelDirection, delta: number) -> (),
|
||||
}
|
||||
|
||||
export type CanvasGroupProperties = {
|
||||
GroupTransparency: (Stateful<number> | number)?,
|
||||
GroupColor3: (Stateful<Color3> | Color3)?,
|
||||
} & CanvasMouseEvents
|
||||
|
||||
-- Constraints
|
||||
export type UIAspectRatioConstraintProperties = {
|
||||
AspectRatio: (Stateful<number> | number)?,
|
||||
AspectType: (Stateful<Enum.AspectType> | Enum.AspectType)?,
|
||||
DominantAxis: (Stateful<Enum.DominantAxis> | Enum.DominantAxis)?,
|
||||
}
|
||||
|
||||
export type UISizeConstraintProperties = {
|
||||
MinSize: (Stateful<Vector2> | Vector2)?,
|
||||
MaxSize: (Stateful<Vector2> | Vector2)?,
|
||||
}
|
||||
|
||||
-- Specialized
|
||||
export type BillboardGuiProperties = GuiBaseProperties & {
|
||||
Active: (Stateful<boolean> | boolean)?,
|
||||
AlwaysOnTop: (Stateful<boolean> | boolean)?,
|
||||
LightInfluence: (Stateful<number> | number)?,
|
||||
MaxDistance: (Stateful<number> | number)?,
|
||||
SizeOffset: (Stateful<Vector2> | Vector2)?,
|
||||
StudsOffset: (Stateful<Vector3> | Vector3)?,
|
||||
ExtentsOffset: (Stateful<Vector3> | Vector3)?,
|
||||
}
|
||||
|
||||
export type SurfaceGuiProperties = GuiBaseProperties & {
|
||||
Active: (Stateful<boolean> | boolean)?,
|
||||
AlwaysOnTop: (Stateful<boolean> | boolean)?,
|
||||
Brightness: (Stateful<number> | number)?,
|
||||
CanvasSize: (Stateful<Vector2> | Vector2)?,
|
||||
Face: (Stateful<Enum.NormalId> | Enum.NormalId)?,
|
||||
LightInfluence: (Stateful<number> | number)?,
|
||||
PixelsPerStud: (Stateful<number> | number)?,
|
||||
SizingMode: (Stateful<Enum.SurfaceGuiSizingMode> | Enum.SurfaceGuiSizingMode)?,
|
||||
ToolPunchThroughDistance: (Stateful<number> | number)?,
|
||||
}
|
||||
|
||||
export type ScreenGuiProperties = GuiBaseProperties & {
|
||||
Active: (Stateful<boolean> | boolean)?,
|
||||
AlwaysOnTop: (Stateful<boolean> | boolean)?,
|
||||
Brightness: (Stateful<number> | number)?,
|
||||
DisplayOrder: (Stateful<number> | number)?,
|
||||
IgnoreGuiInset: (Stateful<boolean> | boolean)?,
|
||||
OnTopOfCoreBlur: (Stateful<boolean> | boolean)?,
|
||||
ScreenInsets: (Stateful<Enum.ScreenInsets> | Enum.ScreenInsets)?,
|
||||
ZIndexBehavior: (Stateful<Enum.ZIndexBehavior> | Enum.ZIndexBehavior)?,
|
||||
}
|
||||
|
||||
export type EventNames = (
|
||||
"InputBegan" | "InputEnded" | "InputChanged" |
|
||||
"MouseEnter" | "MouseLeave" | "MouseMoved" |
|
||||
"MouseButton1Down" | "MouseButton1Up" |
|
||||
"MouseWheelForward" | "MouseWheelBackward" |
|
||||
|
||||
"TouchTap" | "TouchPinch" | "TouchPan" |
|
||||
"TouchSwipe" | "TouchRotate" | "TouchLongPress" |
|
||||
|
||||
"SelectionGained" | "SelectionLost" | "SelectionChanged" |
|
||||
|
||||
"Activated" | "MouseButton1Click" | "MouseButton2Click" |
|
||||
"MouseButton2Down" | "MouseButton2Up" |
|
||||
|
||||
"FocusLost" | "FocusGained" | "TextChanged" |
|
||||
|
||||
"Scrolled" |
|
||||
|
||||
"ViewportResized" | "CameraChanged" |
|
||||
|
||||
"BillboardTransformed" |
|
||||
|
||||
"SurfaceChanged" |
|
||||
|
||||
"GroupTransparencyChanged" |
|
||||
|
||||
"StrokeUpdated" |
|
||||
|
||||
"GradientOffsetChanged" |
|
||||
|
||||
"ChildAdded" | "ChildRemoved" | "AncestryChanged"
|
||||
)
|
||||
|
||||
export type PropertyNames = (
|
||||
"Name" | "Visible" | "Active" | "AnchorPoint" | "Position" | "Size" |
|
||||
"Rotation" | "ZIndex" | "LayoutOrder" | "BackgroundTransparency" |
|
||||
"BackgroundColor3" | "BorderSizePixel" | "BorderColor3" |
|
||||
"ClipsDescendants" | "Selectable" |
|
||||
|
||||
"Image" | "ImageColor3" | "ImageTransparency" | "ScaleType" |
|
||||
"SliceCenter" | "TileSize" | "ResampleMode" |
|
||||
|
||||
"Text" | "TextColor3" | "TextTransparency" | "TextStrokeColor3" |
|
||||
"TextStrokeTransparency" | "TextScaled" | "TextSize" | "TextWrapped" |
|
||||
"FontFace" | "LineHeight" | "RichText" | "TextXAlignment" |
|
||||
"TextYAlignment" | "TextTruncate" |
|
||||
|
||||
"AutoButtonColor" | "Modal" | "Selected" | "ButtonHoverStyle" |
|
||||
"ButtonPressStyle" | "ActivationBehavior" | "SelectionGroup" |
|
||||
"SelectionBehaviorUp" | "SelectionBehaviorDown" |
|
||||
"SelectionBehaviorLeft" | "SelectionBehaviorRight" | "GamepadPriority" |
|
||||
|
||||
"ClearTextOnFocus" | "MultiLine" | "PlaceholderText" |
|
||||
"PlaceholderColor3" | "CursorPosition" | "SelectionStart" |
|
||||
"ShowNativeInput" | "TextInputType" |
|
||||
|
||||
"ScrollBarImageColor3" | "ScrollBarThickness" | "ScrollingDirection" |
|
||||
"CanvasSize" | "CanvasPosition" | "AutomaticCanvasSize" |
|
||||
"VerticalScrollBarInset" | "HorizontalScrollBarInset" |
|
||||
"ScrollBarImageTransparency" | "ElasticBehavior" | "VerticalScrollBarPosition" |
|
||||
|
||||
"CurrentCamera" | "LightColor" | "LightDirection" | "Ambient" |
|
||||
"LightAngularInfluence" |
|
||||
|
||||
"Padding" | "FillDirection" | "HorizontalAlignment" | "VerticalAlignment" |
|
||||
"SortOrder" | "Appearance" | "CellSize" | "CellPadding" | "StartCorner" |
|
||||
|
||||
"CornerRadius" | "Color" | "Thickness" | "Transparency" | "Enabled" |
|
||||
"ApplyStrokeMode" | "LineJoinMode" | "Offset" | "Rotation" |
|
||||
"PaddingTop" | "PaddingBottom" | "PaddingLeft" | "PaddingRight" | "Scale" |
|
||||
|
||||
"GroupTransparency" | "GroupColor3" |
|
||||
|
||||
"AspectRatio" | "AspectType" | "DominantAxis" | "MinSize" | "MaxSize" |
|
||||
|
||||
"AlwaysOnTop" | "LightInfluence" | "MaxDistance" | "SizeOffset" |
|
||||
"StudsOffset" | "ExtentsOffset" |
|
||||
|
||||
"Brightness" | "Face" | "PixelsPerStud" | "SizingMode" | "ToolPunchThroughDistance" |
|
||||
|
||||
"Parent" | "Children"
|
||||
)
|
||||
|
||||
|
||||
return {}
|
66
src/Chemical/Types/Overrides.lua
Normal file
66
src/Chemical/Types/Overrides.lua
Normal file
|
@ -0,0 +1,66 @@
|
|||
--!strict
|
||||
local Gui = require(script.Parent.Gui)
|
||||
|
||||
-- Define the custom method we're adding.
|
||||
type CompositionHandle = {
|
||||
Destroy: (self: CompositionHandle) -> ()
|
||||
}
|
||||
|
||||
-- The factory function now returns an intersection type.
|
||||
-- It tells Luau "this object has all the properties of P AND all the properties of CompositionHandle".
|
||||
type ComposerFactory<P> = (blueprint: P) -> (P & CompositionHandle)
|
||||
|
||||
-- The overloads remain the same, but their return type is now more powerful.
|
||||
export type ComposeFunction = (
|
||||
-- Overloads for creating new instances via class name strings
|
||||
((target: "Frame") -> ComposerFactory<Gui.FrameProperties>) &
|
||||
((target: "TextLabel") -> ComposerFactory<Gui.TextLabelProperties>) &
|
||||
((target: "ImageLabel") -> ComposerFactory<Gui.ImageLabelProperties>) &
|
||||
((target: "TextButton") -> ComposerFactory<Gui.TextButtonProperties>) &
|
||||
((target: "ImageButton") -> ComposerFactory<Gui.ImageButtonProperties>) &
|
||||
((target: "TextBox") -> ComposerFactory<Gui.TextBoxProperties>) &
|
||||
((target: "ScrollingFrame") -> ComposerFactory<Gui.ScrollingFrameProperties>) &
|
||||
((target: "ViewportFrame") -> ComposerFactory<Gui.ViewportFrameProperties>) &
|
||||
((target: "CanvasGroup") -> ComposerFactory<Gui.CanvasGroupProperties>) &
|
||||
((target: "UIListLayout") -> ComposerFactory<Gui.UIListLayoutProperties>) &
|
||||
((target: "UIGridLayout") -> ComposerFactory<Gui.UIGridLayoutProperties>) &
|
||||
((target: "UICorner") -> ComposerFactory<Gui.UICornerProperties>) &
|
||||
((target: "UIStroke") -> ComposerFactory<Gui.UIStrokeProperties>) &
|
||||
((target: "UIGradient") -> ComposerFactory<Gui.UIGradientProperties>) &
|
||||
((target: "UIPadding") -> ComposerFactory<Gui.UIPaddingProperties>) &
|
||||
((target: "UIScale") -> ComposerFactory<Gui.UIScaleProperties>) &
|
||||
((target: "UIAspectRatioConstraint") -> ComposerFactory<Gui.UIAspectRatioConstraintProperties>) &
|
||||
((target: "UISizeConstraint") -> ComposerFactory<Gui.UISizeConstraintProperties>) &
|
||||
((target: "BillboardGui") -> ComposerFactory<Gui.BillboardGuiProperties>) &
|
||||
((target: "SurfaceGui") -> ComposerFactory<Gui.SurfaceGuiProperties>) &
|
||||
((target: "ScreenGui") -> ComposerFactory<Gui.ScreenGuiProperties>) &
|
||||
|
||||
-- Overloads for adopting existing instances
|
||||
((target: Frame) -> ComposerFactory<Gui.FrameProperties>) &
|
||||
((target: TextLabel) -> ComposerFactory<Gui.TextLabelProperties>) &
|
||||
((target: ImageLabel) -> ComposerFactory<Gui.ImageLabelProperties>) &
|
||||
((target: TextButton) -> ComposerFactory<Gui.TextButtonProperties>) &
|
||||
((target: ImageButton) -> ComposerFactory<Gui.ImageButtonProperties>) &
|
||||
((target: TextBox) -> ComposerFactory<Gui.TextBoxProperties>) &
|
||||
((target: ScrollingFrame) -> ComposerFactory<Gui.ScrollingFrameProperties>) &
|
||||
((target: ViewportFrame) -> ComposerFactory<Gui.ViewportFrameProperties>) &
|
||||
((target: CanvasGroup) -> ComposerFactory<Gui.CanvasGroupProperties>) &
|
||||
((target: UIListLayout) -> ComposerFactory<Gui.UIListLayoutProperties>) &
|
||||
((target: UIGridLayout) -> ComposerFactory<Gui.UIGridLayoutProperties>) &
|
||||
((target: UICorner) -> ComposerFactory<Gui.UICornerProperties>) &
|
||||
((target: UIStroke) -> ComposerFactory<Gui.UIStrokeProperties>) &
|
||||
((target: UIGradient) -> ComposerFactory<Gui.UIGradientProperties>) &
|
||||
((target: UIPadding) -> ComposerFactory<Gui.UIPaddingProperties>) &
|
||||
((target: UIScale) -> ComposerFactory<Gui.UIScaleProperties>) &
|
||||
((target: UIAspectRatioConstraint) -> ComposerFactory<Gui.UIAspectRatioConstraintProperties>) &
|
||||
((target: UISizeConstraint) -> ComposerFactory<Gui.UISizeConstraintProperties>) &
|
||||
((target: BillboardGui) -> ComposerFactory<Gui.BillboardGuiProperties>) &
|
||||
((target: SurfaceGui) -> ComposerFactory<Gui.SurfaceGuiProperties>) &
|
||||
((target: ScreenGui) -> ComposerFactory<Gui.ScreenGuiProperties>) &
|
||||
|
||||
-- Fallback overloads for generic/unspecified types
|
||||
((target: string) -> ComposerFactory<Gui.GuiBaseProperties>) &
|
||||
((target: GuiObject) -> ComposerFactory<Gui.GuiBaseProperties>)
|
||||
)
|
||||
|
||||
return {}
|
17
src/Chemical/Types/init.lua
Normal file
17
src/Chemical/Types/init.lua
Normal file
|
@ -0,0 +1,17 @@
|
|||
local ECS = require(script.Parent.Packages.JECS)
|
||||
|
||||
local module = {}
|
||||
|
||||
export type HasEntity = {
|
||||
entity: ECS.Entity
|
||||
}
|
||||
|
||||
export type MaybeDestroyable = {
|
||||
__internalDestroy: (MaybeDestroyable) -> (),
|
||||
}
|
||||
|
||||
export type MaybeCleanable = {
|
||||
clean: (MaybeCleanable) -> ()
|
||||
}
|
||||
|
||||
return module
|
66
src/Chemical/init.lua
Normal file
66
src/Chemical/init.lua
Normal file
|
@ -0,0 +1,66 @@
|
|||
--!strict
|
||||
|
||||
--version 0.2.5 == Sovereignty
|
||||
|
||||
--TODO:
|
||||
-- Reactors and Reactions: Tables and Maps change networking, rather than full value networking on change.
|
||||
-- Export Types
|
||||
-- Improve file organization
|
||||
-- Templating
|
||||
-- Entity recycling - if possible
|
||||
|
||||
local ECS = require(script.ECS)
|
||||
local Overrides = require(script.Types.Overrides)
|
||||
|
||||
local Value = require(script.Factories.Value)
|
||||
local Table = require(script.Factories.Table)
|
||||
local Map = require(script.Factories.Map)
|
||||
local Computed = require(script.Factories.Computed)
|
||||
local Observer = require(script.Factories.Observer)
|
||||
local Watch = require(script.Factories.Watch)
|
||||
local Effect = require(script.Factories.Effect)
|
||||
local Reaction = require(script.Factories.Reaction)
|
||||
|
||||
|
||||
local Compose = require(script.Functions.Compose)
|
||||
|
||||
|
||||
local Is = require(script.Functions.Is)
|
||||
local Peek = require(script.Functions.Peek)
|
||||
local Array = require(script.Functions.Array)
|
||||
local Alive = require(script.Functions.Alive)
|
||||
local Destroy = require(script.Functions:FindFirstChild("Destroy"))
|
||||
local Blueprint = require(script.Functions.Blueprint)
|
||||
|
||||
local Symbols = require(script.Symbols)
|
||||
|
||||
local Scheduler = require(script.Singletons.Scheduler)
|
||||
local Reactor = require(script.Singletons.Reactor)
|
||||
|
||||
|
||||
return {
|
||||
Value = (Value :: any) :: Value.ValueFactory,
|
||||
Table = Table,
|
||||
Map = Map,
|
||||
Computed = Computed,
|
||||
Observer = Observer,
|
||||
Watch = Watch,
|
||||
Effect = Effect,
|
||||
Reaction = Reaction,
|
||||
|
||||
|
||||
Compose = (Compose :: any) :: Overrides.ComposeFunction,
|
||||
Reactor = Reactor,
|
||||
|
||||
Is = Is,
|
||||
Peek = Peek,
|
||||
Array = Array,
|
||||
Alive = Alive,
|
||||
Destroy = Destroy,
|
||||
Blueprint = Blueprint,
|
||||
|
||||
|
||||
OnEvent = Symbols.OnEvent,
|
||||
OnChange = Symbols.OnChange,
|
||||
Children = Symbols.Children
|
||||
}
|
Loading…
Reference in a new issue