├── TestRunner.rbxl ├── lib ├── Chaining │ ├── Constants.lua │ ├── init.lua │ ├── Types.lua │ └── Chainer.lua ├── Tags │ ├── init.lua │ └── TagWatcher.lua ├── Replication │ ├── init.lua │ ├── BuiltInSerializers.lua │ ├── Util.lua │ └── Replicator.lua ├── init.lua ├── Core │ ├── Constants.lua │ ├── Operators │ │ ├── Comparators.lua │ │ ├── Comparators.spec.lua │ │ └── Reducers.lua │ ├── Types.lua │ ├── Pipeline.lua │ ├── Lens │ │ ├── Lens.lua │ │ └── LensCollection.lua │ ├── init.lua │ └── init.spec.lua ├── Query │ ├── Selectors │ │ ├── Util.lua │ │ ├── init.lua │ │ ├── BaseSelector.lua │ │ ├── AnySelector.lua │ │ ├── LayerSelector.lua │ │ ├── AllSelector.lua │ │ └── ComponentSelector.lua │ ├── init.lua │ └── System.lua └── Shared │ ├── Util.lua │ ├── Inspect.lua │ └── t.lua ├── rotriever.toml ├── .gitmodules ├── examples ├── client │ ├── Rocs.lua │ ├── Replication.client.lua │ └── Red.client.lua ├── server │ ├── Rocs.lua │ ├── Red.server.lua │ ├── Button.server.lua │ └── Replication.server.lua └── system.lua ├── .editorconfig ├── CHANGELOG.md ├── README.md ├── .gitignore ├── default.project.json ├── LICENSE ├── docs └── index.html.md └── .luacheckrc /TestRunner.rbxl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evaera/Rocs/HEAD/TestRunner.rbxl -------------------------------------------------------------------------------- /lib/Chaining/Constants.lua: -------------------------------------------------------------------------------- 1 | return { 2 | TAG = "Rocs_Chaining_%s"; 3 | } 4 | -------------------------------------------------------------------------------- /rotriever.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "Rocs" 3 | author = "rocs-rbx" 4 | content_root = "lib" 5 | version = "0.2.0" 6 | -------------------------------------------------------------------------------- /lib/Chaining/init.lua: -------------------------------------------------------------------------------- 1 | local Chainer = require(script.Chainer) 2 | 3 | return function (rocs) 4 | rocs.chaining = Chainer.new(rocs) 5 | end 6 | -------------------------------------------------------------------------------- /lib/Tags/init.lua: -------------------------------------------------------------------------------- 1 | local TagWatcher = require(script.TagWatcher) 2 | 3 | return function (rocs) 4 | rocs.tags = TagWatcher.new(rocs) 5 | end 6 | -------------------------------------------------------------------------------- /lib/Replication/init.lua: -------------------------------------------------------------------------------- 1 | local Replicator = require(script.Replicator) 2 | 3 | return function (rocs) 4 | rocs.replicator = Replicator.new(rocs) 5 | end 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/testez"] 2 | path = vendor/testez 3 | url = https://github.com/Roblox/testez.git 4 | [submodule "vendor/t"] 5 | path = vendor/t 6 | url = https://github.com/osyrisrblx/t 7 | -------------------------------------------------------------------------------- /examples/client/Rocs.lua: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 2 | local Rocs = require(ReplicatedStorage:WaitForChild("Rocs")) 3 | 4 | local rocs = Rocs.new() 5 | Rocs.useReplication(rocs) 6 | 7 | return rocs 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.lua] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.json] 11 | indent_style = spaces 12 | indent_width = 2 -------------------------------------------------------------------------------- /examples/client/Replication.client.lua: -------------------------------------------------------------------------------- 1 | local Rocs = require(script.Parent.Rocs) 2 | 3 | Rocs:registerLayer({ 4 | name = "WalkSpeed"; 5 | 6 | onUpdated = function(self) 7 | print("New speed is", self:getOr("speed", "back to default")) 8 | end 9 | }) 10 | -------------------------------------------------------------------------------- /lib/init.lua: -------------------------------------------------------------------------------- 1 | return { 2 | Core = require(script.Core); 3 | new = require(script.Core).new; 4 | useChaining = require(script.Chaining); 5 | useQuery = require(script.Query); 6 | useReplication = require(script.Replication); 7 | useTags = require(script.Tags); 8 | } 9 | -------------------------------------------------------------------------------- /examples/client/Red.client.lua: -------------------------------------------------------------------------------- 1 | local Rocs = require(script.Parent.Rocs) 2 | 3 | Rocs:registerLayer({ 4 | name = "Red"; 5 | 6 | onUpdated = function(self) 7 | self.instance.BrickColor = BrickColor.new(self:get("color")) 8 | end; 9 | 10 | pipelineCheck = {"BasePart"}; 11 | }) 12 | -------------------------------------------------------------------------------- /examples/server/Rocs.lua: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 2 | local Rocs = require(ReplicatedStorage:WaitForChild("Rocs")) 3 | 4 | local rocs = Rocs.new() 5 | Rocs.useReplication(rocs) 6 | Rocs.useTags(rocs) 7 | Rocs.useChaining(rocs) 8 | 9 | return rocs 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Next 2 | 3 | - Fix a bug with aggregate:set using the wrong base data 4 | - Add listen and dispatch methods on aggregates 5 | - Add reducers.compose 6 | - Add chaining middleware 7 | - Add component registration hooks 8 | - Remove tags from core, make into middleware 9 | - Add registerEntityComponentHook 10 | 11 | # v0.1.0 12 | 13 | - Initial pre-release. -------------------------------------------------------------------------------- /examples/server/Red.server.lua: -------------------------------------------------------------------------------- 1 | local Rocs = require(script.Parent.Rocs) 2 | 3 | Rocs:registerLayer({ 4 | name = "Red"; 5 | tag = "Red"; 6 | 7 | pipelineCheck = {"BasePart"}; 8 | 9 | components = { 10 | Replicated = { 11 | players = {}; 12 | }; 13 | }; 14 | 15 | defaults = { 16 | color = "Really red" 17 | }; 18 | 19 | randomize = function(self) 20 | self:set("color", BrickColor.random().Name) 21 | end; 22 | }) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | Roblox 5 | Component 6 | System 7 |

8 |

9 |

10 | Read the Docs 11 |

12 |

13 | Archived. Succeeded by Fabric 14 |

15 | -------------------------------------------------------------------------------- /lib/Core/Constants.lua: -------------------------------------------------------------------------------- 1 | local Constants = { 2 | 3 | LIFECYCLE_ADDED = "onAdded"; 4 | LIFECYCLE_REMOVED = "onRemoved"; 5 | LIFECYCLE_UPDATED = "onUpdated"; 6 | LIFECYCLE_PARENT_UPDATED = "onParentUpdated"; 7 | 8 | ALL_COMPONENTS = {}; 9 | None = {}; 10 | Internal = {}; 11 | 12 | SCOPE_BASE = "_base"; 13 | SCOPE_REMOTE = "_remote"; 14 | 15 | COMPARATOR_NEAR_DEFAULT = 0.001; 16 | 17 | } 18 | 19 | Constants.RESERVED_SCOPES = { 20 | [Constants.SCOPE_BASE] = true; 21 | [Constants.SCOPE_REMOTE] = true; 22 | } 23 | 24 | return Constants 25 | -------------------------------------------------------------------------------- /examples/server/Button.server.lua: -------------------------------------------------------------------------------- 1 | local Rocs = require(script.Parent.Rocs) 2 | 3 | Rocs:registerLayer({ 4 | name = "Button"; 5 | tag = "Button"; 6 | 7 | pipelineCheck = {"BasePart"}; 8 | 9 | chainingEvents = {"click"}; 10 | 11 | onAdded = function(self) 12 | self._clickDetector = Instance.new("ClickDetector", self.instance) 13 | 14 | self._clickDetector.MouseClick:Connect(function(...) 15 | self:dispatch("click", ...) 16 | end) 17 | end; 18 | 19 | onRemoved = function(self) 20 | self._clickDetector:Destroy() 21 | end 22 | }) 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | .vs/slnx.sqlite 43 | .vscode/settings.json 44 | build -------------------------------------------------------------------------------- /lib/Replication/BuiltInSerializers.lua: -------------------------------------------------------------------------------- 1 | local Lens = require(script.Parent.Parent.Core.Lens.Lens) 2 | 3 | return { 4 | serializers = { 5 | [Lens] = function(lens, rocs) 6 | 7 | return { 8 | type = "_lens"; 9 | name = lens.name; 10 | instance = rocs.replicator:_serialize(lens.instance); 11 | } 12 | end 13 | }; 14 | 15 | deserializers = { 16 | _lens = function(data, rocs) 17 | local instance = rocs.replicator:_deserialize(data.instance) 18 | 19 | return rocs:getPipeline(instance, "replicator"):getLayer(data.name) 20 | end 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /lib/Replication/Util.lua: -------------------------------------------------------------------------------- 1 | local Util = {} 2 | 3 | function Util.clipMask(data, mask) 4 | assert(type(data) == "table", "Attempt to mask on a non-table!") 5 | 6 | local clippedData = {} 7 | 8 | for k, v in pairs(data) do 9 | if mask[k] == true then 10 | clippedData[k] = v 11 | elseif type(mask[k]) == "table" then 12 | clippedData[k] = Util.clipMask(data[k], mask[k]) 13 | end 14 | end 15 | 16 | return clippedData 17 | end 18 | 19 | function Util.find(array, value) 20 | for i, v in ipairs(array) do 21 | if v == value then 22 | return i 23 | end 24 | end 25 | end 26 | 27 | return Util 28 | -------------------------------------------------------------------------------- /lib/Chaining/Types.lua: -------------------------------------------------------------------------------- 1 | local t = require(script.Parent.Parent.Shared.t) 2 | 3 | local section = t.map( 4 | t.string, 5 | t.array( 6 | t.interface({ 7 | event = t.string; 8 | target = t.Instance; 9 | component = t.string; 10 | call = t.intersection(t.string, function(value) 11 | return value:sub(1, 1) ~= "_", "Called method names cannot begin with _" 12 | end) 13 | }) 14 | ) 15 | ) 16 | 17 | local module = t.interface({ 18 | server = t.optional(section), 19 | client = t.optional(section), 20 | shared = t.optional(section) 21 | }) 22 | 23 | return { 24 | module = module; 25 | section = section; 26 | } 27 | -------------------------------------------------------------------------------- /lib/Query/Selectors/Util.lua: -------------------------------------------------------------------------------- 1 | local Util = {} 2 | 3 | local LayerSelector = require(script.Parent.LayerSelector) 4 | local BaseSelector = require(script.Parent.BaseSelector) 5 | 6 | function Util.inheritsBase(object) 7 | local depth = 0 8 | while type(object) == "table" do 9 | object = getmetatable(object) 10 | depth = depth + 1 11 | if object == BaseSelector then 12 | return depth >= 2 13 | end 14 | end 15 | return false 16 | end 17 | 18 | function Util.resolve(rocs, selectorResolvable) 19 | if type(selectorResolvable) == "string" then 20 | return LayerSelector.new(rocs, selectorResolvable) 21 | end 22 | assert(Util.inheritsBase(selectorResolvable)) 23 | return selectorResolvable 24 | end 25 | 26 | return Util 27 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rocs", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ReplicatedStorage": { 6 | "$className": "ReplicatedStorage", 7 | "Rocs": { 8 | "$path": "lib" 9 | } 10 | }, 11 | "TestService": { 12 | "$className": "TestService", 13 | "TestEZ": { 14 | "$path": "vendor/testez/lib" 15 | } 16 | }, 17 | "ServerScriptService": { 18 | "$className": "ServerScriptService", 19 | "Examples": { 20 | "$path": "examples/server" 21 | } 22 | }, 23 | "StarterPlayer": { 24 | "$className": "StarterPlayer", 25 | "StarterPlayerScripts": { 26 | "$className": "StarterPlayerScripts", 27 | "Examples": { 28 | "$path": "examples/client" 29 | } 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /lib/Query/Selectors/init.lua: -------------------------------------------------------------------------------- 1 | local AllSelector = require(script.AllSelector) 2 | local AnySelector = require(script.AnySelector) 3 | local LayerSelector = require(script.LayerSelector) 4 | 5 | return function(rocs) 6 | local Selectors = {} 7 | 8 | function Selectors.isa(class) 9 | assert(type(class) == "string") 10 | 11 | return function(instance) 12 | return instance:IsA(class) 13 | end 14 | end 15 | 16 | function Selectors.any(...) 17 | return AnySelector.new(rocs, ...) 18 | end 19 | 20 | function Selectors.all(...) 21 | return AllSelector.new(rocs, ...) 22 | end 23 | 24 | function Selectors.has(name, properties, metacomponents) 25 | assert(type(name) == "string") 26 | assert(properties == nil or type(properties) == "table") 27 | assert(metacomponents == nil or type(metacomponents) == "table") 28 | 29 | return LayerSelector.new(rocs, name, properties, metacomponents) 30 | end 31 | 32 | return Selectors 33 | end 34 | -------------------------------------------------------------------------------- /lib/Core/Operators/Comparators.lua: -------------------------------------------------------------------------------- 1 | local Constants = require(script.Parent.Parent.Constants) 2 | local Util = require(script.Parent.Parent.Parent.Shared.Util) 3 | 4 | local Comparators = {} 5 | 6 | function Comparators.reference(a, b) 7 | return a ~= b 8 | end 9 | 10 | function Comparators.value(a, b) 11 | return not Util.deepEquals(a, b) 12 | end 13 | 14 | function Comparators.within(epsilon) 15 | return function(a, b) 16 | return math.abs(a - b) > epsilon 17 | end 18 | end 19 | 20 | function Comparators.structure(propertyComparators) 21 | return function (a, b) 22 | for _, item in ipairs({a, b}) do 23 | for key in pairs(item) do 24 | if (propertyComparators[key] or Comparators.default)(a and a[key], b and b[key]) then 25 | return true 26 | end 27 | end 28 | end 29 | 30 | return false 31 | end 32 | end 33 | 34 | Comparators.near = Comparators.within(Constants.COMPARATOR_NEAR_DEFAULT) 35 | 36 | Comparators.default = Comparators.reference 37 | 38 | return Comparators 39 | -------------------------------------------------------------------------------- /examples/server/Replication.server.lua: -------------------------------------------------------------------------------- 1 | local Rocs = require(script.Parent.Rocs) 2 | local Players = game:GetService("Players") 3 | 4 | Rocs:registerLayer({ 5 | name = "WalkSpeed"; 6 | 7 | onUpdated = function(self) 8 | self.instance.WalkSpeed = self:getOr("speed", 16) 9 | end; 10 | 11 | components = { 12 | Replicated = { 13 | mask = { 14 | speed = true; 15 | } 16 | }; 17 | }; 18 | 19 | pipelineCheck = {"Humanoid"}; 20 | 21 | }) 22 | 23 | Players.PlayerAdded:Connect(function(player) 24 | player.CharacterAdded:Connect(function(character) 25 | wait(1) -- magic wait always works 26 | local humanoid = character:WaitForChild("Humanoid") 27 | 28 | local pipeline = Rocs:getPipeline(humanoid, "test") 29 | 30 | while true do 31 | if math.random(1, 10) == 1 then 32 | pipeline:removeLayer("WalkSpeed") 33 | else 34 | pipeline:addLayer("WalkSpeed", { 35 | speed = math.random(2, 40); 36 | secret = "very secret"; 37 | }) 38 | end 39 | 40 | wait(2) 41 | end 42 | end) 43 | end) 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Freshly Squeezed Nerds 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/Core/Types.lua: -------------------------------------------------------------------------------- 1 | local t = require(script.Parent.Parent.Shared.t) 2 | 3 | local Types = {} 4 | 5 | Types.LayerDefinition = t.interface({ 6 | name = t.string; 7 | reducer = t.optional(t.callback); 8 | check = t.optional(t.callback); 9 | defaults = t.optional(t.map(t.string, t.any)); 10 | components = t.optional(t.map(t.string, t.any)); 11 | tag = t.optional(t.string); 12 | pipelineCheck = t.optional(t.union(t.array(t.string), t.callback)); 13 | chainingEvents = t.optional(t.array(t.string)); 14 | 15 | data = t.none; 16 | lastData = t.none; 17 | set = t.none; 18 | get = t.none; 19 | getOr = t.none; 20 | getAnd = t.none; 21 | dispatch = t.none; 22 | listen = t.none; 23 | removeListener = t.none; 24 | _listeners = t.none; 25 | instance = t.none; 26 | 27 | onAdded = t.optional(t.callback); 28 | onUpdated = t.optional(t.callback); 29 | onParentUpdated = t.optional(t.callback); 30 | onRemoved = t.optional(t.callback); 31 | shouldUpdate = t.optional(t.callback); 32 | initialize = t.optional(t.callback); 33 | destroy = t.optional(t.callback); 34 | }) 35 | 36 | Types.staticLens = t.intersection(t.LayerDefinition, t.interface({ 37 | new = t.callback; 38 | })) 39 | 40 | Types.LayerLens = t.interface({ 41 | components = t.table; 42 | data = t.table; 43 | instance = t.Instance; 44 | }) 45 | 46 | Types.LayerResolvable = t.union(t.string, Types.staticLens) 47 | 48 | return Types 49 | -------------------------------------------------------------------------------- /lib/Query/init.lua: -------------------------------------------------------------------------------- 1 | local System = require(script.System) 2 | local Selectors = require(script.Selectors) 3 | local Util = require(script.Selectors.Util) 4 | 5 | return function(rocs) 6 | rocs.selectors = Selectors(rocs) 7 | 8 | rocs.query = function(scope, ...) -- optimized for reuse 9 | assert(type(scope) == "string") 10 | return System.new(rocs, scope, ...):setup() 11 | end 12 | 13 | rocs.get = function(scope, ...) -- single use pipeline list getter 14 | assert(type(scope) == "string") 15 | 16 | local entities = {} 17 | for _, instance in pairs(System.new(rocs, ...):instances()) do 18 | table.insert(entities, rocs:getPipeline(instance, scope)) 19 | end 20 | 21 | return entities 22 | end 23 | 24 | rocs.system = function(scope, selector, props, init) -- syntactic sugar around rocs.query 25 | assert(type(scope) == "string") 26 | assert(Util.inheritsBase(selector)) 27 | assert(props == nil or type(props) == "table") 28 | assert(init == nil or type(init) == "function") 29 | 30 | local system = System.new(rocs, scope, selector) 31 | 32 | if props then 33 | for key, value in pairs(props) do 34 | if system[key] then 35 | error("at least one property already in use by system", 2) 36 | end 37 | system[key] = value 38 | end 39 | end 40 | 41 | if init then 42 | init(system) 43 | end 44 | 45 | return system:setup():catchup() 46 | end 47 | 48 | return rocs 49 | end 50 | -------------------------------------------------------------------------------- /lib/Tags/TagWatcher.lua: -------------------------------------------------------------------------------- 1 | local CollectionService = game:GetService("CollectionService") 2 | 3 | local TagWatcher = {} 4 | TagWatcher.__index = TagWatcher 5 | 6 | function TagWatcher.new(rocs) 7 | local self = setmetatable({ 8 | rocs = rocs; 9 | _tags = {}; 10 | }, TagWatcher) 11 | 12 | rocs:registerLayerRegistrationHook(function(staticLens) 13 | if staticLens.tag then 14 | self:listenForTag(staticLens.tag, staticLens) 15 | end 16 | end) 17 | 18 | return self 19 | end 20 | 21 | function TagWatcher:listenForTag(tag, staticLens) 22 | assert(self._tags[tag] == nil, ("Tag %q is already in use!"):format(tag)) 23 | self._tags[tag] = true 24 | 25 | local function addFromTag(instance) 26 | local data = {} 27 | if 28 | instance:FindFirstChild(staticLens.name) 29 | and instance[staticLens.name].ClassName == "ModuleScript" 30 | then 31 | data = require(instance[staticLens.name]) 32 | end 33 | 34 | self.rocs:getPipeline(instance, "tags"):addBaseLayer(staticLens, data) 35 | end 36 | 37 | local function removeFromTag(instance) 38 | self.rocs:getPipeline(instance, "tags"):removeBaseLayer(staticLens) 39 | end 40 | 41 | CollectionService:GetInstanceRemovedSignal(tag):Connect(removeFromTag) 42 | CollectionService:GetInstanceAddedSignal(tag):Connect(addFromTag) 43 | for _, instance in ipairs(CollectionService:GetTagged(tag)) do 44 | addFromTag(instance) 45 | end 46 | end 47 | 48 | return TagWatcher 49 | -------------------------------------------------------------------------------- /examples/system.lua: -------------------------------------------------------------------------------- 1 | local rocs = require(...) 2 | -- ... 3 | 4 | local system = rocs.system("Example", rocs.selectors.all("Health", "Regen")) 5 | 6 | local entities = system:get() -- all entities with "Health" AND "Regen" 7 | local lenses = system:lenses() -- all "Health" and all "Regen" lenses that are on an pipeline that also has the other 8 | 9 | for _, lens in pairs(lenses) do 10 | -- use case? 11 | if lens.type == "Health" then 12 | -- what do 13 | elseif lens.type == "Regen" then 14 | 15 | end 16 | end 17 | 18 | ------- 19 | 20 | local system = rocs.system("Example", rocs.selectors.all(rocs.selectors.has("Health", {Value = 50}), "Regen")) 21 | 22 | local entities = system:get() -- all entities with "Health" (with Value = 50) AND "Regen" 23 | local lenses = system:lenses() -- 24 | 25 | ------- 26 | 27 | local system = rocs.system("Example", rocs.selectors.any("Health", "Regen")) 28 | 29 | 30 | local data = system:get() -- all entities with "Health" (with Value = 50) AND "Regen" 31 | --[[ 32 | data = { 33 | [lensName] = { 34 | ... lenses ... 35 | }, 36 | ... 37 | } 38 | 39 | data = { 40 | [pipeline] = { 41 | lensName = lenses, 42 | ... 43 | }, 44 | ... 45 | } 46 | 47 | for pipeline in pairs(data) do 48 | 49 | for pipeline, lenses in pairs(data) do 50 | 51 | end 52 | ]] 53 | 54 | local entities = system:getEntities() --> { ... entities ... } 55 | local lenses = system:getLenss() --> { ... lenses ... } 56 | local pipelineMapping = system:get() --> data 57 | 58 | --[[ 59 | rocs.selectors.all(X, Y) --> get the entities, return X and Y from each pipeline 60 | rocs.selectors.any(X, Y) --> get the entities, return every single X and Y that occurs on whatever pipeline in that list 61 | rocs.selectors.has(X, props, metas) --> 62 | --> nothing 63 | ]] 64 | -------------------------------------------------------------------------------- /lib/Query/Selectors/BaseSelector.lua: -------------------------------------------------------------------------------- 1 | local BaseSelector = {} 2 | BaseSelector.__index = BaseSelector 3 | 4 | function BaseSelector.new(rocs) 5 | local self = { 6 | _rocs = rocs, 7 | _hooks = { 8 | onAdded = {}, 9 | onRemoved = {}, 10 | onUpdated = {}, 11 | onParentUpdated = {} 12 | }, 13 | _lookup = {}, 14 | _selectors = {} 15 | } 16 | 17 | return setmetatable(self, BaseSelector) 18 | end 19 | 20 | function BaseSelector:_trigger(lifecycle, ...) 21 | for _, hook in self._hooks[lifecycle] do 22 | hook(...) 23 | end 24 | end 25 | 26 | function BaseSelector:fetch() 27 | if self._ready then 28 | local instances = {} 29 | 30 | for instance in pairs(self._lookup) do 31 | table.insert(instances, instance) 32 | end 33 | 34 | return instances 35 | else 36 | return self:instances() 37 | end 38 | end 39 | 40 | function BaseSelector:setup() 41 | if self._ready then 42 | return 43 | end 44 | self._ready = true 45 | 46 | self:_listen() 47 | 48 | for _, selector in pairs(self._selectors) do 49 | selector:setup() 50 | end 51 | 52 | for _, instance in pairs(self:instances()) do 53 | self._lookup[instance] = true 54 | end 55 | 56 | return self 57 | end 58 | 59 | function BaseSelector:destroy() 60 | if not self._ready then 61 | return 62 | end 63 | self._ready = nil 64 | 65 | for category, _ in pairs(self._hooks) do 66 | self._hooks[category] = {} 67 | end 68 | 69 | for _, selector in pairs(self._selectors) do 70 | selector:destroy() 71 | end 72 | 73 | self._lookup = {} 74 | 75 | return self 76 | end 77 | 78 | function BaseSelector:onAdded(hook) 79 | table.insert(self._hooks.onAdded, hook) 80 | return self 81 | end 82 | 83 | function BaseSelector:onRemoved(hook) 84 | table.insert(self._hooks.onRemoved, hook) 85 | return self 86 | end 87 | 88 | function BaseSelector:onUpdated(hook) 89 | table.insert(self._hooks.onUpdated, hook) 90 | return self 91 | end 92 | 93 | function BaseSelector:onParentUpdated(hook) 94 | table.insert(self._hooks.onParentUpdated, hook) 95 | return self 96 | end 97 | 98 | return BaseSelector 99 | -------------------------------------------------------------------------------- /lib/Core/Operators/Comparators.spec.lua: -------------------------------------------------------------------------------- 1 | local Comparators = require(script.Parent.Comparators) 2 | local Constants = require(script.Parent.Parent.Constants) 3 | local Util = require(script.Parent.Parent.Parent.Shared.Util) 4 | 5 | return function () 6 | describe("Comparators", function() 7 | it("should compare by reference", function() 8 | expect(Comparators.reference(1, 1)).to.equal(false) 9 | expect(Comparators.reference(1, 2)).to.equal(true) 10 | expect(Comparators.reference({}, {})).to.equal(true) 11 | 12 | local a = {} 13 | expect(Comparators.reference(a, a)).to.equal(false) 14 | end) 15 | 16 | it("should compare by value", function() 17 | expect(Comparators.value(1, 1)).to.equal(false) 18 | expect(Comparators.value(1, 2)).to.equal(true) 19 | expect(Comparators.value({}, {})).to.equal(false) 20 | expect(Comparators.value({1}, {1})).to.equal(false) 21 | expect(Comparators.value({1}, {2})).to.equal(true) 22 | 23 | local a = {} 24 | expect(Comparators.value(a, a)).to.equal(false) 25 | 26 | expect(Comparators.value({key = 1}, {key = 1})).to.equal(false) 27 | expect(Comparators.value({key = 1}, {key = 2})).to.equal(true) 28 | 29 | local nested = {key = 1, sub = {subkey = "hello"}} 30 | local nestedCopy = Util.deepCopy(nested) 31 | expect(Comparators.value(nested, nestedCopy)).to.equal(false) 32 | end) 33 | 34 | it("should compare within a range", function() 35 | expect(Comparators.within(1)(1, 5)).to.equal(true) 36 | expect(Comparators.within(1)(1, 1.5)).to.equal(false) 37 | 38 | expect(Constants.COMPARATOR_NEAR_DEFAULT > 0).to.equal(true) 39 | expect(Comparators.near(0, Constants.COMPARATOR_NEAR_DEFAULT/2)).to.equal(false) 40 | expect(Comparators.near(0, Constants.COMPARATOR_NEAR_DEFAULT*2)).to.equal(true) 41 | end) 42 | end) 43 | 44 | describe("Comparator utilities", function() 45 | it("should compare by property", function() 46 | local compare = Comparators.structure({ 47 | reference = Comparators.reference; 48 | value = Comparators.value; 49 | near = Comparators.near; 50 | }) 51 | 52 | local t = { x = 1 } 53 | 54 | expect(compare({ 55 | reference = t; 56 | value = t; 57 | near = 0; 58 | }, { 59 | reference = t; 60 | value = { x = 1 }; 61 | near = 0.0001; 62 | })).to.equal(false) 63 | 64 | expect(compare({ 65 | reference = t; 66 | new = 5; 67 | }, { 68 | reference = t; 69 | })).to.equal(true) 70 | end) 71 | end) 72 | end 73 | -------------------------------------------------------------------------------- /lib/Query/Selectors/AnySelector.lua: -------------------------------------------------------------------------------- 1 | local BaseSelector = require(script.Parent.BaseSelector) 2 | local Util = require(script.Parent.Util) 3 | 4 | local AnySelector = setmetatable({}, BaseSelector) 5 | AnySelector.__index = AnySelector 6 | 7 | function AnySelector.new(rocs, ...) 8 | local self = setmetatable(BaseSelector.new(rocs), AnySelector) 9 | 10 | self._selectors = {} 11 | for _, property in pairs(...) do 12 | if type(property) == "function" then 13 | error("Cannot have functions in selectors.any", 2) 14 | else 15 | table.insert(self._selectors, Util.resolve(rocs, property)) 16 | end 17 | end 18 | 19 | return self 20 | end 21 | 22 | function AnySelector:_listen() 23 | for _, selector in pairs(self._selectors) do 24 | selector:onAdded( 25 | function(lens) 26 | local instance = lens.instance 27 | if not self._lookup[instance] then 28 | self._lookup[instance] = true 29 | self:_trigger("onAdded", lens) 30 | end 31 | end 32 | ) 33 | 34 | selector:onRemoved( 35 | function(lens) 36 | local instance = lens.instance 37 | if self._lookup[instance] and not self:check(instance) then 38 | self._lookup[instance] = nil 39 | self:_trigger("onRemoved", lens) 40 | end 41 | end 42 | ) 43 | 44 | selector:onUpdated( 45 | function(lens) 46 | local instance = lens.instance 47 | if self._lookup[instance] then 48 | self:_trigger("onUpdated", lens) 49 | else 50 | self._lookup[instance] = true 51 | self:_trigger("onAdded", lens) 52 | end 53 | end 54 | ) 55 | 56 | -- TODO: is this right? 57 | selector:onParentUpdated( 58 | function(lens) 59 | if self._lookup[lens.instance] then 60 | self:_trigger("onParentUpdated", lens) 61 | end 62 | end 63 | ) 64 | end 65 | end 66 | 67 | function AnySelector:instances() 68 | local instances = {} 69 | 70 | if #self._selectors > 0 then 71 | -- accumulate entities into cache, only-once 72 | for _, selector in pairs(self._selectors) do 73 | for _, instance in pairs(selector:fetch()) do 74 | instances[instance] = true 75 | end 76 | end 77 | 78 | -- turn lookup into array 79 | for instance in pairs(instances) do 80 | table.insert(instances, instance) 81 | instances[instance] = nil 82 | end 83 | end 84 | 85 | return instances 86 | end 87 | 88 | function AnySelector:check(instance) 89 | for _, selector in pairs(self._selectors) do 90 | if selector:check(instance) then 91 | return true 92 | end 93 | end 94 | 95 | return false 96 | end 97 | 98 | return AnySelector 99 | -------------------------------------------------------------------------------- /lib/Core/Pipeline.lua: -------------------------------------------------------------------------------- 1 | local I = require(script.Parent.Types) 2 | local t = require(script.Parent.Parent.Shared.t) 3 | local Constants = require(script.Parent.Constants) 4 | 5 | local Pipeline = {} 6 | Pipeline.__index = Pipeline 7 | 8 | function Pipeline.new(rocs, instance, scope, overrideReserveCheck) 9 | assert( 10 | overrideReserveCheck == true or 11 | Constants.RESERVED_SCOPES[scope] == nil, 12 | ("Pipeline scope cannot be %q"):format(scope) 13 | ) 14 | 15 | return setmetatable({ 16 | rocs = rocs; 17 | instance = instance; 18 | scope = scope; 19 | }, Pipeline) 20 | end 21 | 22 | function Pipeline:__tostring() 23 | return ("Pipeline(%s)"):format(tostring(self.instance)) 24 | end 25 | 26 | local getLayerOpValuesCheck = t.tuple( 27 | I.LayerResolvable, 28 | t.optional(t.string) 29 | ) 30 | function Pipeline:_getLayerOpValues(componentResolvable, scope, ...) 31 | assert(getLayerOpValuesCheck(componentResolvable, scope)) 32 | return 33 | self.instance, 34 | self.rocs._lenses:getStatic(componentResolvable), 35 | scope or self.scope, 36 | ... 37 | end 38 | 39 | function Pipeline:addLayer(componentResolvable, data, metacomponents) 40 | if self.rocs.debug then 41 | warn("ADD", self.instance, componentResolvable, data) 42 | end 43 | return self.rocs._lenses:addLayer( 44 | self:_getLayerOpValues(componentResolvable, nil, data or {}, metacomponents) 45 | ) 46 | end 47 | 48 | function Pipeline:removeLayer(componentResolvable) 49 | if self.rocs.debug then 50 | warn("REMOVE", self.instance, componentResolvable) 51 | end 52 | return self.rocs._lenses:removeLayer( 53 | self:_getLayerOpValues(componentResolvable) 54 | ) 55 | end 56 | 57 | function Pipeline:addBaseLayer(componentResolvable, data, metacomponents) 58 | return self.rocs._lenses:addLayer( 59 | self:_getLayerOpValues(componentResolvable, Constants.SCOPE_BASE, data or {}, metacomponents) 60 | ) 61 | end 62 | 63 | function Pipeline:removeBaseLayer(componentResolvable) 64 | return self.rocs._lenses:removeLayer( 65 | self:_getLayerOpValues(componentResolvable, Constants.SCOPE_BASE) 66 | ) 67 | end 68 | 69 | function Pipeline:getLayer(componentResolvable) 70 | return self.rocs._lenses:get( 71 | self:_getLayerOpValues(componentResolvable) 72 | ) 73 | end 74 | 75 | function Pipeline:getAllLayers() 76 | return self.rocs._lenses:getAll(self.instance) 77 | end 78 | 79 | function Pipeline:removeAllLayers() 80 | return self.rocs._lenses:removeAllLayers(self.instance) 81 | end 82 | 83 | function Pipeline:getScope(newScope) 84 | return self.rocs:getPipeline(self.instance, newScope) 85 | end 86 | 87 | return Pipeline 88 | -------------------------------------------------------------------------------- /lib/Core/Lens/Lens.lua: -------------------------------------------------------------------------------- 1 | local Constants = require(script.Parent.Parent.Constants) 2 | 3 | local Lens = {} 4 | Lens.__index = Lens 5 | 6 | function Lens:get(...) 7 | local object = self.data 8 | 9 | if object == nil then 10 | return 11 | end 12 | 13 | for _, field in ipairs({...}) do 14 | object = object[field] 15 | 16 | if object == nil then 17 | return 18 | end 19 | end 20 | 21 | return object 22 | end 23 | 24 | function Lens:getOr(...) 25 | local path = {...} 26 | local default = table.remove(path, #path) 27 | 28 | local value = self:get(unpack(path)) 29 | 30 | if value ~= nil then 31 | return value 32 | elseif type(default) == "function" then 33 | return default(unpack(path)) 34 | else 35 | return default 36 | end 37 | end 38 | 39 | function Lens:getAnd(...) 40 | local path = {...} 41 | local callback = table.remove(path, #path) 42 | 43 | local value = self:get(unpack(path)) 44 | 45 | if value ~= nil then 46 | return callback(value) 47 | end 48 | end 49 | 50 | function Lens:set(...) 51 | local path = {...} 52 | local value = table.remove(path, #path) 53 | 54 | assert(value ~= nil, "Must provide a value to set") 55 | 56 | if value == Constants.None then 57 | value = nil 58 | end 59 | 60 | local currentValue = self.components[Constants.SCOPE_BASE] or {} 61 | 62 | while #path > 1 do 63 | currentValue = currentValue[table.remove(path, 1)] 64 | end 65 | 66 | if path[1] then 67 | currentValue[path[1]] = value 68 | else 69 | currentValue = value 70 | end 71 | 72 | return self.rocs._lenses:addLayer(self.instance, getmetatable(self), Constants.SCOPE_BASE, currentValue) 73 | end 74 | 75 | function Lens:listen(eventName, callback) 76 | if not self._listeners then 77 | self._listeners = {} 78 | end 79 | 80 | if not self._listeners[eventName] then 81 | self._listeners[eventName] = {} 82 | end 83 | 84 | table.insert(self._listeners[eventName], callback) 85 | 86 | return callback 87 | end 88 | 89 | function Lens:removeListener(eventName, callback) 90 | if self._listeners and self._listeners[eventName] then 91 | for i, listener in ipairs(self._listeners[eventName]) do 92 | if listener == callback then 93 | table.remove(self._listeners[eventName], i) 94 | break 95 | end 96 | end 97 | 98 | if #self._listeners[eventName] == 0 then 99 | self._listeners[eventName] = nil 100 | 101 | if next(self._listeners) == nil then 102 | self._listeners = nil 103 | end 104 | end 105 | end 106 | end 107 | 108 | function Lens:dispatch(eventName, ...) 109 | if self[eventName] then 110 | self[eventName](self, ...) 111 | end 112 | 113 | if self._listeners and self._listeners[eventName] then 114 | for _, listener in ipairs(self._listeners[eventName]) do 115 | listener(...) 116 | end 117 | end 118 | end 119 | 120 | function Lens:__tostring() 121 | return ("Lens(%s)"):format(self.name) 122 | end 123 | 124 | return Lens 125 | -------------------------------------------------------------------------------- /lib/Query/Selectors/LayerSelector.lua: -------------------------------------------------------------------------------- 1 | local BaseSelector = require(script.Parent.BaseSelector) 2 | 3 | local LayerSelector = setmetatable({}, BaseSelector) 4 | LayerSelector.__index = LayerSelector 5 | 6 | function LayerSelector.new(rocs, componentResolvable, properties, metaLayers) 7 | assert(componentResolvable) 8 | local self = setmetatable(BaseSelector.new(rocs), LayerSelector) 9 | 10 | self._componentResolvable = componentResolvable 11 | self._properties = properties or {} 12 | self._metaLayers = metaLayers or {} 13 | 14 | return self 15 | end 16 | 17 | function LayerSelector:_listen() 18 | 19 | self._rocs:registerLayerHook( 20 | self._componentResolvable, 21 | "onAdded", 22 | function(lens) 23 | local instance = lens.instance 24 | if not self._lookup[instance] and self:check(instance) then 25 | self._lookup[instance] = true 26 | self:_trigger("onAdded", lens) 27 | end 28 | end 29 | ) 30 | 31 | self._rocs:registerLayerHook( 32 | self._componentResolvable, 33 | "onRemoved", 34 | function(lens) 35 | local instance = lens.instance 36 | if self._lookup[instance] then 37 | self._lookup[instance] = nil 38 | self:_trigger("onRemoved", lens) 39 | end 40 | end 41 | ) 42 | 43 | self._rocs:registerLayerHook( 44 | self._componentResolvable, 45 | "onUpdated", 46 | function(lens) 47 | local instance = lens.instance 48 | if self._lookup[instance] then 49 | if self:check(instance) then 50 | self:_trigger("onUpdated", lens) 51 | else 52 | self._lookup[instance] = nil 53 | self:_trigger("onRemoved", lens) 54 | end 55 | else 56 | if self:check(instance) then 57 | self._lookup[instance] = true 58 | self:_trigger("onAdded", lens) 59 | else 60 | self:_trigger("onUpdated", lens) 61 | end 62 | end 63 | end 64 | ) 65 | 66 | -- TODO: is this right? 67 | self._rocs:registerLayerHook( 68 | self._componentResolvable, 69 | "onParentUpdated", 70 | function(lens) 71 | if self._lookup[lens.instance] then 72 | self:_trigger("onParentUpdated", lens) 73 | end 74 | end 75 | ) 76 | 77 | end 78 | 79 | function LayerSelector:instances() 80 | local instances = {} 81 | 82 | for _, component in pairs(self._rocs:getLayers(self._componentResolvable)) do 83 | instances[component.instance] = true 84 | end 85 | 86 | for instance in pairs(instances) do 87 | table.insert(instances, instance) 88 | instances[instance] = nil 89 | end 90 | 91 | return instances 92 | end 93 | 94 | function LayerSelector:check(instance) 95 | local component = self._rocs:getPipeline(instance):getLayer(self._componentResolvable) 96 | if not component then 97 | return false 98 | end 99 | 100 | for key, property in pairs(self._properties) do 101 | -- TODO handle properties 102 | end 103 | 104 | for name, _ in pairs(self._metaLayers) do 105 | -- TODO handle meta-components 106 | end 107 | 108 | return true 109 | end 110 | 111 | return LayerSelector 112 | -------------------------------------------------------------------------------- /lib/Shared/Util.lua: -------------------------------------------------------------------------------- 1 | local Util = {} 2 | 3 | do 4 | local metatables = {} 5 | function Util.easyIndex(levels) 6 | for i = 1, levels do 7 | if metatables[i] == nil then 8 | metatables[i] = { 9 | __index = function(self, k) 10 | self[k] = setmetatable({}, metatables[i - 1]) 11 | return self[k] 12 | end 13 | } 14 | end 15 | end 16 | 17 | return metatables[levels] 18 | end 19 | end 20 | 21 | function Util.assign(toObj, ...) 22 | for _, fromObj in ipairs({...}) do 23 | for key, value in pairs(fromObj) do 24 | toObj[key] = value 25 | end 26 | end 27 | 28 | return toObj 29 | end 30 | 31 | function Util.makeToString(staticName) 32 | return function(self) 33 | return ("%s(%s)"):format(staticName, getmetatable(self).name) 34 | end 35 | end 36 | 37 | function Util.concat(list, ...) 38 | local args = { ... } 39 | local result = {} 40 | for i = 1, #list do 41 | result[i] = list[i] 42 | end 43 | for i = 1, #args do 44 | local value = args[i] 45 | for j = 1, #value do 46 | result[#result + 1] = value[j] 47 | end 48 | end 49 | return result 50 | end 51 | 52 | function Util.callCounter() 53 | return setmetatable({ 54 | call = function(self, key) 55 | self[key] = self[key] + 1 56 | end 57 | }, { 58 | __index = function(self, key) 59 | self[key] = 0 60 | return 0 61 | end 62 | }) 63 | end 64 | 65 | function Util.deepCopy(t) 66 | if type(t) == "table" then 67 | local n = {} 68 | for i,v in pairs(t) do 69 | n[i] = Util.deepCopy(v) 70 | end 71 | return n 72 | else 73 | return t 74 | end 75 | end 76 | 77 | function Util.deepEquals(a, b) 78 | if type(a) ~= "table" or type(b) ~= "table" then 79 | return a == b 80 | end 81 | 82 | for k in pairs(a) do 83 | local av = a[k] 84 | local bv = b[k] 85 | if type(av) == "table" and type(bv) == "table" then 86 | local result = Util.deepEquals(av, bv) 87 | if not result then 88 | return false 89 | end 90 | elseif av ~= bv then 91 | return false 92 | end 93 | end 94 | 95 | -- extra keys in b 96 | for k in pairs(b) do 97 | if a[k] == nil then 98 | return false 99 | end 100 | end 101 | 102 | return true 103 | end 104 | 105 | function Util.requireAllInAnd(instance, callback, self) 106 | for _, object in ipairs(instance:GetChildren()) do 107 | if object:IsA("ModuleScript") then 108 | callback(self, require(object)) 109 | else 110 | Util.requireAllInAnd(object, callback, self) 111 | end 112 | end 113 | end 114 | 115 | --- Maps values of an array through a callback and returns an array of mapped values 116 | function Util.mapArray(array, callback) 117 | local results = {} 118 | 119 | for i, v in ipairs(array) do 120 | results[i] = callback(v, i) 121 | end 122 | 123 | return results 124 | end 125 | 126 | --- Maps arguments #2-n through callback and returns values as tuple 127 | function Util.mapTuple(callback, ...) 128 | local results = {} 129 | for i, value in ipairs({...}) do 130 | results[i] = callback(value) 131 | end 132 | return unpack(results) 133 | end 134 | 135 | return Util 136 | -------------------------------------------------------------------------------- /lib/Chaining/Chainer.lua: -------------------------------------------------------------------------------- 1 | local types = require(script.Parent.Types) 2 | local Constants = require(script.Parent.Constants) 3 | local CollectionService = game:GetService("CollectionService") 4 | local IS_SERVER = game:GetService("RunService"):IsServer() 5 | 6 | local Chainer = {} 7 | Chainer.__index = Chainer 8 | 9 | function Chainer.new(rocs) 10 | local self = setmetatable({ 11 | rocs = rocs 12 | }, Chainer) 13 | 14 | local tagName = self:getTagName() 15 | CollectionService:GetInstanceAddedSignal(tagName):Connect(function(...) 16 | self:_handleModule(...) 17 | end) 18 | 19 | for _, module in ipairs(CollectionService:GetTagged(tagName)) do 20 | self:_handleModule(module) 21 | end 22 | 23 | return self 24 | end 25 | 26 | function Chainer:getTagName() 27 | return Constants.TAG:format(self.rocs.name) 28 | end 29 | 30 | function Chainer:warn(text, ...) 31 | return self.rocs:warn("[Chaining] " .. text, ...) 32 | end 33 | 34 | function Chainer:_handleModule(module) 35 | if not module:IsA("ModuleScript") then 36 | return self:warn("%s is not a ModuleScript!", module) 37 | end 38 | 39 | local contents = require(module) 40 | 41 | local isValid, typeError = types.module(contents) 42 | if not isValid then 43 | self:warn("Invalid chaining configuration:\n\n%s", typeError) 44 | end 45 | 46 | local function chain(section) 47 | if contents[section] then 48 | self:chain(module.Parent, contents[section]) 49 | end 50 | end 51 | 52 | if IS_SERVER then 53 | chain("server") 54 | else 55 | chain("client") 56 | end 57 | 58 | chain("shared") 59 | end 60 | 61 | function Chainer:_connect(lens, entries) 62 | for _, entry in ipairs(entries) do 63 | lens:listen(entry.event, function() 64 | local targetLens = self.rocs:getPipeline(entry.target, "chaining"):getLayer(entry.component) 65 | 66 | if targetLens then 67 | targetLens[entry.call](targetLens) -- TODO: Arguments 68 | else 69 | self:warn( 70 | "Layer %s is missing from %s which is needed for a component chain!", 71 | entry.component, 72 | entry.target 73 | ) 74 | end 75 | end) 76 | end 77 | end 78 | 79 | function Chainer:_chainLens(sourceInstance, staticLens, entries) 80 | local pipeline = self.rocs:getPipeline(sourceInstance, "chaining") 81 | 82 | local currentLens = pipeline:getLayer(staticLens) 83 | if currentLens then 84 | self:_connect(currentLens, entries) 85 | end 86 | 87 | self.rocs:registerPipelineLayerHook( 88 | sourceInstance, 89 | staticLens, 90 | "initialize", 91 | function (lens) 92 | self:_connect(lens, entries) 93 | end 94 | ) 95 | 96 | if staticLens.chainingEvents == nil then 97 | self:warn("chainingEvents array is missing from component %s! Please list all chainable events upon registration.", staticLens) 98 | end 99 | end 100 | 101 | function Chainer:chain(sourceInstance, structure) 102 | for componentResolvable, entries in pairs(structure) do 103 | local staticLens = self.rocs:resolveLens(componentResolvable) 104 | 105 | if staticLens then 106 | self:_chainLens(sourceInstance, staticLens, entries) 107 | else 108 | self:warn("Could not resolve component %s in chaining configuration for %s", componentResolvable, sourceInstance) 109 | end 110 | end 111 | end 112 | 113 | return Chainer 114 | -------------------------------------------------------------------------------- /lib/Query/Selectors/AllSelector.lua: -------------------------------------------------------------------------------- 1 | local BaseSelector = require(script.Parent.BaseSelector) 2 | local Util = require(script.Parent.Util) 3 | 4 | local AllSelector = setmetatable({}, BaseSelector) 5 | AllSelector.__index = AllSelector 6 | 7 | function AllSelector.new(rocs, ...) 8 | local self = setmetatable(BaseSelector.new(rocs), AllSelector) 9 | 10 | self._checks = {} 11 | for _, property in pairs(...) do 12 | if type(property) == "function" then 13 | table.insert(self._checks, property) 14 | else 15 | table.insert(self._selectors, Util.resolve(rocs, property)) 16 | end 17 | end 18 | 19 | return self 20 | end 21 | 22 | function AllSelector:_listen() 23 | for _, selector in pairs(self._selectors) do 24 | selector:onAdded( 25 | function(lens) 26 | local instance = lens.instance 27 | if not self._lookup[instance] and self:check(instance, selector) then 28 | self._lookup[instance] = true 29 | self:_trigger("onAdded", lens) 30 | end 31 | end 32 | ) 33 | 34 | selector:onRemoved( 35 | function(lens) 36 | local instance = lens.instance 37 | if self._lookup[instance] then 38 | self._lookup[instance] = nil 39 | self:_trigger("onRemoved", lens) 40 | end 41 | end 42 | ) 43 | 44 | selector:onUpdated( 45 | function(lens) 46 | local instance = lens.instance 47 | if self._lookup[instance] then 48 | if self:check(instance, selector) then 49 | self:_trigger("onUpdated", lens) 50 | else 51 | self._lookup[instance] = nil 52 | self:_trigger("onRemoved", lens) 53 | end 54 | else 55 | if self:check(instance, selector) then 56 | self._lookup[instance] = true 57 | self:_trigger("onAdded", lens) 58 | else 59 | self:_trigger("onUpdated", lens) 60 | end 61 | end 62 | end 63 | ) 64 | 65 | -- TODO: is this right? 66 | selector:onParentUpdated( 67 | function(lens) 68 | if self._lookup[lens.instance] then 69 | self:_trigger("onParentUpdated", lens) 70 | end 71 | end 72 | ) 73 | end 74 | end 75 | 76 | function AllSelector:instances() 77 | local instances = {} 78 | 79 | if #self._selectors > 0 then 80 | -- get first selection of entities 81 | for _, instance in pairs(self._selectors[1]:fetch()) do 82 | instances[instance] = true 83 | end 84 | 85 | -- filter the ones out that don't match function checks 86 | for _, check in pairs(self._checks) do 87 | for instance in pairs(instances) do 88 | if not check(instance) then 89 | instances[instance] = nil 90 | end 91 | end 92 | end 93 | 94 | -- filter the ones out that don't match other selectors 95 | for i = 2, #self._selectors do 96 | local selector = self._selectors[i] 97 | for instance in pairs(instances) do 98 | if not selector:check(instance) then 99 | instances[instance] = nil 100 | end 101 | end 102 | end 103 | 104 | -- turn lookup into array 105 | for instance in pairs(instances) do 106 | table.insert(instances, instance) 107 | instances[instance] = nil 108 | end 109 | end 110 | 111 | return instances 112 | end 113 | 114 | function AllSelector:check(instance, exceptSelector) 115 | for _, check in pairs(self._checks) do 116 | if not check(instance) then 117 | return false 118 | end 119 | end 120 | 121 | for _, selector in pairs(self._selectors) do 122 | if exceptSelector ~= selector and not selector:check(instance) then 123 | return false 124 | end 125 | end 126 | 127 | return true 128 | end 129 | 130 | return AllSelector 131 | -------------------------------------------------------------------------------- /lib/Query/Selectors/ComponentSelector.lua: -------------------------------------------------------------------------------- 1 | local BaseSelector = require(script.Parent.BaseSelector) 2 | 3 | local ComponentSelector = setmetatable({}, BaseSelector) 4 | ComponentSelector.__index = ComponentSelector 5 | 6 | function ComponentSelector.new(rocs, componentResolvable, properties, metaComponents) 7 | assert(componentResolvable) 8 | local self = setmetatable(BaseSelector.new(rocs), ComponentSelector) 9 | 10 | self._componentResolvable = componentResolvable 11 | self._properties = properties or {} 12 | self._metaComponents = metaComponents or {} 13 | 14 | self._rocsHooks = {} 15 | 16 | return self 17 | end 18 | 19 | function ComponentSelector:_listen() 20 | 21 | self._rocsHooks.onAdded = self._rocs:registerComponentHook( 22 | self._componentResolvable, 23 | "onAdded", 24 | function(aggregate) 25 | local instance = aggregate.instance 26 | if not self._lookup[instance] and self:check(instance) then 27 | self._lookup[instance] = true 28 | self:_trigger("onAdded", aggregate) 29 | end 30 | end 31 | ) 32 | 33 | self._rocsHooks.onRemoved = self._rocs:registerComponentHook( 34 | self._componentResolvable, 35 | "onRemoved", 36 | function(aggregate) 37 | local instance = aggregate.instance 38 | if self._lookup[instance] then 39 | self._lookup[instance] = nil 40 | self:_trigger("onRemoved", aggregate) 41 | end 42 | end 43 | ) 44 | 45 | self._rocsHooks.onUpdated = self._rocs:registerComponentHook( 46 | self._componentResolvable, 47 | "onUpdated", 48 | function(aggregate) 49 | local instance = aggregate.instance 50 | if self._lookup[instance] then 51 | if self:check(instance) then 52 | self:_trigger("onUpdated", aggregate) 53 | else 54 | self._lookup[instance] = nil 55 | self:_trigger("onRemoved", aggregate) 56 | end 57 | else 58 | if self:check(instance) then 59 | self._lookup[instance] = true 60 | self:_trigger("onAdded", aggregate) 61 | else 62 | self:_trigger("onUpdated", aggregate) 63 | end 64 | end 65 | end 66 | ) 67 | 68 | -- TODO: is this right? 69 | self._rocsHooks.onParentUpdated = self._rocs:registerComponentHook( 70 | self._componentResolvable, 71 | "onParentUpdated", 72 | function(aggregate) 73 | if self._lookup[aggregate.instance] then 74 | self:_trigger("onParentUpdated", aggregate) 75 | end 76 | end 77 | ) 78 | 79 | end 80 | 81 | function BaseSelector:destroy() -- override 82 | if not self._ready then 83 | return 84 | end 85 | self._ready = nil 86 | 87 | for category, _ in pairs(self._hooks) do 88 | self._hooks[category] = {} 89 | end 90 | 91 | for lifecycle, hook in pairs(self._rocsHooks) do 92 | self._rocs:unregisterComponentHook(self._componentResolvable, lifecycle, hook) 93 | end 94 | 95 | self._lookup = {} 96 | 97 | return self 98 | end 99 | 100 | function ComponentSelector:instances() 101 | local instances = {} 102 | 103 | for _, component in pairs(self._rocs:getComponents(self._componentResolvable)) do 104 | instances[component.instance] = true 105 | end 106 | 107 | for instance in pairs(instances) do 108 | table.insert(instances, instance) 109 | instances[instance] = nil 110 | end 111 | 112 | return instances 113 | end 114 | 115 | function ComponentSelector:check(instance) 116 | local component = self._rocs:getEntity(instance):getComponent(self._componentResolvable) 117 | if not component then 118 | return false 119 | end 120 | 121 | for key, property in pairs(self._properties) do 122 | -- TODO handle properties 123 | end 124 | 125 | for name, _ in pairs(self._metaComponents) do 126 | -- TODO handle meta-components 127 | end 128 | 129 | return true 130 | end 131 | 132 | return ComponentSelector 133 | -------------------------------------------------------------------------------- /lib/Core/Operators/Reducers.lua: -------------------------------------------------------------------------------- 1 | local Util = require(script.Parent.Parent.Parent.Shared.Util) 2 | 3 | local Reducers = {} 4 | 5 | function Reducers.last(values) 6 | return values[#values] 7 | end 8 | 9 | function Reducers.first(values) 10 | return values[1] 11 | end 12 | 13 | function Reducers.truthy(values) 14 | for _, value in ipairs(values) do 15 | if value then 16 | return value 17 | end 18 | end 19 | end 20 | 21 | function Reducers.falsy(values) 22 | for _, value in ipairs(values) do 23 | if not value then 24 | return value 25 | end 26 | end 27 | end 28 | 29 | function Reducers.add(values) 30 | local reducedValue = 0 31 | 32 | for _, value in ipairs(values) do 33 | reducedValue = reducedValue + value 34 | end 35 | 36 | return reducedValue 37 | end 38 | 39 | function Reducers.multiply(values) 40 | local reducedValue = 1 41 | 42 | for _, value in ipairs(values) do 43 | reducedValue = reducedValue * value 44 | end 45 | 46 | return reducedValue 47 | end 48 | 49 | function Reducers.concatArray(values) 50 | return Util.concat(unpack(values)) 51 | end 52 | 53 | function Reducers.collect(values) 54 | return values 55 | end 56 | 57 | function Reducers.lowest(values) 58 | if #values == 0 then 59 | return 60 | end 61 | 62 | return math.min(unpack(values)) 63 | end 64 | 65 | function Reducers.highest(values) 66 | if #values == 0 then 67 | return 68 | end 69 | 70 | return math.max(unpack(values)) 71 | end 72 | 73 | function Reducers.mergeTable(values) 74 | return Util.assign({}, unpack(values)) 75 | end 76 | 77 | -- Utilities 78 | 79 | function Reducers.concatString(delim) 80 | return function (values) 81 | return table.concat(values, delim or "") 82 | end 83 | end 84 | 85 | function Reducers.priorityValue(reducer) 86 | reducer = reducer or Reducers.last 87 | 88 | return function (values) 89 | 90 | local highestPriority = -math.huge 91 | local highestPriorityValues = {} 92 | 93 | for _, struct in ipairs(values) do 94 | if struct.priority > highestPriority then 95 | highestPriorityValues = {struct.value} 96 | elseif struct.priorty == highestPriority then 97 | table.insert(highestPriorityValues, struct.value) 98 | end 99 | end 100 | 101 | return reducer(highestPriorityValues) 102 | end 103 | end 104 | 105 | function Reducers.structure(reducers, default) 106 | if default == nil then 107 | default = Reducers.last 108 | end 109 | 110 | return function(values) 111 | local properties = {} 112 | 113 | for _, value in ipairs(values) do 114 | for propName, propValue in pairs(value) do 115 | if properties[propName] == nil then 116 | properties[propName] = {} 117 | end 118 | 119 | table.insert(properties[propName], propValue) 120 | end 121 | end 122 | 123 | local reducedValue = {} 124 | 125 | for propName, propValues in pairs(properties) do 126 | reducedValue[propName] = 127 | (reducers[propName] or default)(propValues, properties) 128 | end 129 | 130 | return reducedValue 131 | end 132 | end 133 | 134 | -- TODO: structure with unknown fields using one 135 | function Reducers.map(reducer, ...) 136 | return Reducers.structure({}, reducer, ...) 137 | end 138 | 139 | function Reducers.exactly(value) 140 | return function () 141 | return value 142 | end 143 | end 144 | 145 | function Reducers.try(...) 146 | local reducers = {...} 147 | 148 | return function (values) 149 | for _, reducer in ipairs(reducers) do 150 | local result = reducer(values) 151 | 152 | if result ~= nil then 153 | return result 154 | end 155 | end 156 | 157 | return nil 158 | end 159 | end 160 | 161 | function Reducers.compose(...) 162 | local reducers = {...} 163 | 164 | return function (values) 165 | for _, reducer in ipairs(reducers) do 166 | values = reducer(values) 167 | end 168 | 169 | return values 170 | end 171 | end 172 | 173 | --? Should this be removed in favor of Reducers.try? 174 | function Reducers.thisOr(reducer, defaultValue) 175 | return function(values) 176 | local result = reducer(values) 177 | 178 | if result == nil then 179 | return defaultValue 180 | else 181 | return result 182 | end 183 | end 184 | end 185 | 186 | local function makeOr(func) 187 | return function (defaultValue) 188 | return Reducers.thisOr(func, defaultValue) 189 | end 190 | end 191 | 192 | Reducers.truthyOr = makeOr(Reducers.truthy) 193 | Reducers.falsyOr = makeOr(Reducers.falsy) 194 | Reducers.lastOr = makeOr(Reducers.last) 195 | Reducers.default = Reducers.structure({}) 196 | 197 | return Reducers 198 | -------------------------------------------------------------------------------- /lib/Core/init.lua: -------------------------------------------------------------------------------- 1 | local Pipeline = require(script.Pipeline) 2 | local Util = require(script.Parent.Shared.Util) 3 | local t = require(script.Parent.Shared.t) 4 | local Constants = require(script.Constants) 5 | local Reducers = require(script.Operators.Reducers) 6 | local Comparators = require(script.Operators.Comparators) 7 | local inspect = require(script.Parent.Shared.Inspect) 8 | 9 | local LensCollection = require(script.Lens.LensCollection) 10 | 11 | local Rocs = { 12 | debug = false; 13 | None = Constants.None; 14 | Internal = Constants.Internal; 15 | reducers = Reducers; 16 | comparators = Comparators; 17 | } 18 | Rocs.__index = Rocs 19 | 20 | function Rocs.new(name) 21 | local self = setmetatable({ 22 | name = name or "global"; 23 | _lifecycleHooks = { 24 | global = setmetatable({}, Util.easyIndex(1)); 25 | component = setmetatable({}, Util.easyIndex(2)); 26 | instance = setmetatable({}, Util.easyIndex(3)); 27 | registration = {}; 28 | } 29 | }, Rocs) 30 | 31 | self._lenses = LensCollection.new(self) 32 | 33 | return self 34 | end 35 | 36 | function Rocs:registerLifecycleHook(lifecycle, hook) 37 | table.insert(self._lifecycleHooks.global[lifecycle], hook) 38 | end 39 | 40 | function Rocs:registerLayerHook(componentResolvable, lifecycle, hook) 41 | local staticLens = self._lenses:getStatic(componentResolvable) 42 | 43 | table.insert(self._lifecycleHooks.component[lifecycle][staticLens], hook) 44 | 45 | return hook 46 | end 47 | 48 | function Rocs:unregisterLayerHook(component, lifecycle, hook) 49 | local staticLens = self._lenses:getStatic(componentResolvable) 50 | local hooks = self._lifecycleHooks.component[lifecycle][staticLens] 51 | for i, v in ipairs(hooks) do 52 | if v == hook then 53 | table.remove(hooks, i) 54 | 55 | if #hooks == 0 then 56 | self._lifecycleHooks.component[lifecycle][staticLens] = nil 57 | end 58 | 59 | break 60 | end 61 | end 62 | end 63 | 64 | function Rocs:registerPipelineLayerHook(instance, componentResolvable, lifecycle, hook) 65 | local staticLens = self._lenses:getStatic(componentResolvable) 66 | 67 | table.insert(self._lifecycleHooks.instance[instance][lifecycle][staticLens], hook) 68 | 69 | if typeof(instance) == "Instance" then 70 | instance.AncestryChanged:Connect(function() 71 | if not instance:IsDescendantOf(game) then 72 | self._lifecycleHooks.instance[instance] = nil 73 | end 74 | end) 75 | end 76 | end 77 | 78 | function Rocs:registerLayerRegistrationHook(hook) 79 | table.insert(self._lifecycleHooks.registration, hook) 80 | end 81 | 82 | function Rocs:registerLayer(...) 83 | local staticLens = self._lenses:register(...) 84 | 85 | for _, hook in ipairs(self._lifecycleHooks.registration) do 86 | hook(staticLens) 87 | end 88 | 89 | return staticLens 90 | end 91 | 92 | function Rocs:getLayers(componentResolvable) 93 | return self._lenses._lenses[self._lenses:getStatic(componentResolvable)] or {} 94 | end 95 | 96 | function Rocs:resolveLens(componentResolvable) 97 | return self._lenses:resolve(componentResolvable) 98 | end 99 | 100 | function Rocs:registerLayersIn(instance) 101 | return Util.requireAllInAnd(instance, self.registerLayer, self) 102 | end 103 | 104 | local getPipelineCheck = t.tuple(t.union(t.Instance, t.table), t.string) 105 | function Rocs:getPipeline(instance, scope, override) 106 | assert(getPipelineCheck(instance, scope)) 107 | assert(override == nil or override == Rocs.Internal) 108 | 109 | return Pipeline.new(self, instance, scope, override ~= nil) 110 | end 111 | 112 | 113 | function Rocs:_dispatchLifecycleHooks(lens, stagePool, stage) 114 | stage = stage or stagePool 115 | local staticLens = getmetatable(lens) 116 | 117 | for _, hook in ipairs(self._lifecycleHooks.global[stagePool]) do 118 | hook(lens, stage) 119 | end 120 | 121 | if rawget(self._lifecycleHooks.component[stagePool], staticLens) then 122 | local hooks = self._lifecycleHooks.component[stagePool][staticLens] 123 | 124 | for _, hook in ipairs(hooks) do 125 | hook(lens, stage) 126 | end 127 | end 128 | 129 | if 130 | rawget(self._lifecycleHooks.instance, lens.instance) 131 | and rawget(self._lifecycleHooks.instance[lens.instance], stagePool) 132 | and rawget(self._lifecycleHooks.instance[lens.instance][stagePool], staticLens) 133 | then 134 | local hooks = self._lifecycleHooks.instance[lens.instance][stagePool][staticLens] 135 | 136 | for _, hook in ipairs(hooks) do 137 | hook(lens, stage) 138 | end 139 | end 140 | end 141 | 142 | function Rocs:_dispatchLifecycle(lens, stage) 143 | self:_dispatchLifecycleHooks(lens, stage) 144 | self:_dispatchLifecycleHooks(lens, "global", stage) 145 | end 146 | 147 | function Rocs:warn(text, ...) 148 | return warn(("[Rocs %s]"):format(self.name), text:format(Util.mapTuple(function(obj) 149 | return typeof(obj) == "Instance" 150 | and obj:GetFullName() 151 | or tostring(obj) 152 | end, ...))) 153 | end 154 | 155 | return Rocs 156 | -------------------------------------------------------------------------------- /lib/Query/System.lua: -------------------------------------------------------------------------------- 1 | local AllSelector = require(script.Parent.Selectors.AllSelector) 2 | local LayerSelector = require(script.Parent.Selectors.LayerSelector) 3 | local Util = require(script.Parent.Selectors.Util) 4 | 5 | local intervalSignal = game:GetService("RunService").Stepped 6 | 7 | local System = setmetatable({}, AllSelector) 8 | System.__index = System 9 | 10 | -- TODO: look into logic of lens/component events 11 | 12 | function System.new(rocs, scope, ...) 13 | local args = {...} 14 | 15 | local base 16 | if #args == 1 then 17 | if Util.inheritsBase(args[1]) then 18 | base = args[1] 19 | elseif type(args[1]) == "string" then 20 | base = LayerSelector.new(rocs, args[1]) 21 | end 22 | end 23 | 24 | local self = setmetatable(base or AllSelector.new(rocs, ...), System) 25 | 26 | self._entities = {} -- keeping track of what entities are in system 27 | -- self._lookup: [instance] = pipeline 28 | 29 | self._events = {} -- {Event = RbxScriptSignal, Hook = function, Connection = nil/RbxScriptConnection} 30 | self._intervals = {} -- {Interval = num, Hook = function, LastInvoke = tick()} 31 | self._intervalConnection = nil 32 | 33 | self._hooks.onLayerAdded = {} 34 | self._hooks.onLayerRemoved = {} 35 | self._hooks.onLayerUpdated = {} 36 | self._hooks.onLayerParentUpdated = {} 37 | 38 | self._scope = scope 39 | 40 | return self 41 | end 42 | 43 | function System:_stop() 44 | for _, entry in pairs(self._events) do 45 | if entry.connection then 46 | entry.connection:Disconnect() 47 | entry.connection = nil 48 | end 49 | end 50 | 51 | if self._intervalConnection then 52 | self._intervalConnection:Disconnect() 53 | self._intervalConnection = nil 54 | end 55 | end 56 | 57 | function System:_start() 58 | for _, entry in pairs(self.events) do 59 | if not entry.connection then 60 | entry.connection = entry.event:Connect(entry.hook) 61 | end 62 | end 63 | 64 | if not self._intervalConnection then 65 | self._intervalConnection = intervalSignal:Connect( 66 | function() 67 | for _, interval in pairs(self._intervals) do 68 | local lastInvoke = interval.lastInvoke 69 | local timestamp = tick() 70 | if not lastInvoke then 71 | interval.lastInvoke = timestamp 72 | interval.hook(0) 73 | elseif timestamp - lastInvoke >= interval.interval then 74 | interval.lastInvoke = timestamp 75 | interval.hook(timestamp - lastInvoke) 76 | end 77 | end 78 | end 79 | ) 80 | end 81 | end 82 | 83 | function System:_listen() -- override 84 | for _, selector in pairs(self._selectors) do 85 | selector:onAdded( 86 | function(lens) 87 | local instance = lens.instance 88 | if self:check(instance, selector) then 89 | if not self._lookup[instance] then 90 | local pipeline = self._rocs:getPipeline(instance, self._scope) 91 | table.insert(self._entities, pipeline) 92 | self._lookup[instance] = pipeline 93 | if #self._entities == 1 then 94 | self:_start() 95 | end 96 | self:_trigger("onAdded", pipeline) 97 | end 98 | self:_trigger("onLayerAdded", lens) 99 | end 100 | end 101 | ) 102 | 103 | selector:onRemoved( 104 | function(lens) 105 | local instance = lens.instance 106 | if self._lookup[instance] then 107 | if not self:check(instance) then 108 | local pipeline = self._lookup[instance] 109 | self._lookup[instance] = nil 110 | for key, value in pairs(self._entities) do 111 | if value == pipeline then 112 | table.remove(self._entities, key) 113 | break 114 | end 115 | end 116 | if #self._entities == 0 then 117 | self:_stop() 118 | end 119 | self:_trigger("onRemoved", pipeline) 120 | end 121 | self:_trigger("onLayerRemoved", lens) 122 | end 123 | end 124 | ) 125 | 126 | -- TODO: is this right? 127 | selector:onUpdated( 128 | function(lens) 129 | local pipeline = self._lookup[lens.instance] 130 | if pipeline then 131 | self:_trigger("onUpdated", pipeline) 132 | self:_trigger("onLayerUpdated", lens) 133 | end 134 | end 135 | ) 136 | 137 | -- TODO: is this right? 138 | selector:onParentUpdated( 139 | function(lens) 140 | local pipeline = self._lookup[lens.instance] 141 | if pipeline then 142 | self:_trigger("onParentUpdated", pipeline) 143 | self:_trigger("onLayerParentUpdated", lens) 144 | end 145 | end 146 | ) 147 | end 148 | end 149 | 150 | function System:setup() -- override 151 | if self._ready then 152 | return 153 | end 154 | self._ready = true 155 | 156 | self:_listen() 157 | 158 | for _, selector in pairs(self._selectors) do 159 | selector:setup() 160 | end 161 | 162 | for _, instance in pairs(self:instances()) do 163 | local pipeline = self._rocs:getPipeline(instance, self._scope) 164 | table.insert(self._entities, pipeline) 165 | self._lookup[instance] = pipeline 166 | end 167 | 168 | if #self._entities > 0 then 169 | self:_start() 170 | end 171 | 172 | return self 173 | end 174 | 175 | function System:destroy() -- override 176 | if not self._ready then 177 | return 178 | end 179 | self._ready = nil 180 | 181 | for category, _ in pairs(self._hooks) do 182 | self._hooks[category] = {} 183 | end 184 | 185 | for _, selector in pairs(self._selectors) do 186 | selector:destroy() 187 | end 188 | 189 | self._entities = {} 190 | self._lookup = {} 191 | 192 | self:_stop() 193 | 194 | return self 195 | end 196 | 197 | function System:catchup() 198 | for _, pipeline in pairs(self._entities) do 199 | self:_trigger("onAdded", pipeline) 200 | end 201 | return self 202 | end 203 | 204 | function System:onInterval(interval, hook) 205 | table.insert(self._intervals, { 206 | interval = interval, 207 | hook = hook 208 | }) 209 | 210 | if #self._entities > 0 then 211 | self:_start() 212 | end 213 | 214 | return self 215 | end 216 | 217 | function System:onEvent(event, hook) 218 | table.insert(self._events, { 219 | event = event, 220 | hook = hook 221 | }) 222 | 223 | if #self._entities > 0 then 224 | self:_start() 225 | end 226 | 227 | return self 228 | end 229 | 230 | function System:get() 231 | return self._entities -- TODO: doc that user should not modify 232 | end 233 | 234 | function System:onLayerAdded(hook) 235 | table.insert(self._hooks.onLayerAdded, hook) 236 | return self 237 | end 238 | 239 | function System:onLayerRemoved(hook) 240 | table.insert(self._hooks.onLayerRemoved, hook) 241 | return self 242 | end 243 | 244 | function System:onLayerUpdated(hook) 245 | table.insert(self._hooks.onLayerUpdated, hook) 246 | return self 247 | end 248 | 249 | function System:onLayerParentUpdated(hook) 250 | table.insert(self._hooks.onLayerParentUpdated, hook) 251 | return self 252 | end 253 | 254 | return System 255 | -------------------------------------------------------------------------------- /lib/Replication/Replicator.lua: -------------------------------------------------------------------------------- 1 | local RunService = game:GetService("RunService") 2 | local Players = game:GetService("Players") 3 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 4 | local BuiltInSerializers = require(script.Parent.BuiltInSerializers) 5 | local Util = require(script.Parent.Util) 6 | 7 | local IS_SERVER = RunService:IsServer() 8 | local EVENT_NAME = "RocsEvent" 9 | local EVENT_NAME_INIT = "RocsInitial" 10 | local inspect = require(script.Parent.Parent.Shared.Inspect) 11 | 12 | local Replicator = {} 13 | Replicator.__index = Replicator 14 | 15 | local function idpipeline(...) 16 | return ... 17 | end 18 | 19 | local function getOrCreate(parent, name, class) 20 | local instance = parent:FindFirstChild(name) 21 | 22 | if not instance then 23 | instance = Instance.new(class) 24 | instance.Name = name 25 | instance.Parent = parent 26 | end 27 | 28 | return instance 29 | end 30 | 31 | local function getData(data, replicated, player) 32 | return 33 | replicated and ( 34 | replicated.playerMasks 35 | and replicated.playerMasks[player] 36 | and Util.clipMask(data, replicated.playerMasks[player]) 37 | or replicated.mask 38 | and Util.clipMask(data, replicated.mask) 39 | ) or data 40 | end 41 | 42 | function Replicator.new(rocs) 43 | local self = { 44 | rocs = rocs; 45 | _serializers = setmetatable({}, BuiltInSerializers.serializers); 46 | _deserializers = setmetatable({}, BuiltInSerializers.deserializers); 47 | } 48 | 49 | if IS_SERVER then 50 | self._event = getOrCreate( 51 | ReplicatedStorage, 52 | EVENT_NAME, 53 | "RemoteEvent" 54 | ) 55 | 56 | self._eventInit = getOrCreate( 57 | ReplicatedStorage, 58 | EVENT_NAME_INIT, 59 | "RemoteEvent" 60 | ) 61 | 62 | self._component = rocs:registerLayer({ 63 | name = "Replicated"; 64 | reducer = rocs.reducers.structure({ 65 | players = rocs.reducers.concatArray; 66 | mask = rocs.reducers.mergeTable; 67 | playerMasks = rocs.reducers.mergeTable; 68 | }); 69 | check = function(value) 70 | return type(value) == "table" 71 | end; 72 | onUpdated = function(replicated) 73 | replicated:dispatch("onParentUpdated", true) 74 | end; 75 | onParentUpdated = function(replicated, fromSelf) 76 | local lens = replicated.instance 77 | 78 | local serializedTarget = self:_serialize(lens.instance) 79 | 80 | local shouldBroadcast = 81 | not fromSelf 82 | or rocs.comparators.value( 83 | replicated.data and replicated.data.mask, 84 | replicated.lastData and replicated.lastData.mask 85 | ) 86 | or rocs.comparators.value( 87 | replicated.data and replicated.data.playerMasks, 88 | replicated.lastData and replicated.lastData.playerMasks 89 | ) 90 | 91 | local removedPlayers = {} 92 | local players 93 | if 94 | replicated.data 95 | and replicated.lastData 96 | and replicated.data.players 97 | and replicated.lastData.players 98 | then 99 | for _, player in ipairs(replicated.lastData.players) do 100 | if Util.find(replicated.data.players, player) == nil then 101 | removedPlayers = removedPlayers 102 | 103 | table.insert(removedPlayers, player) 104 | end 105 | end 106 | 107 | if not shouldBroadcast then 108 | for _, player in ipairs(replicated.data.players) do 109 | if Util.find(replicated.lastData.players) == nil then 110 | players = players or {} 111 | 112 | table.insert(players, player) 113 | end 114 | end 115 | end 116 | end 117 | 118 | players = players or (replicated.data and replicated.data.players) or Players:GetPlayers() 119 | 120 | for _, player in ipairs(players) do 121 | self:_replicate( 122 | player, 123 | lens.name, 124 | serializedTarget, 125 | getData(lens.data, replicated.data, player) 126 | ) 127 | end 128 | 129 | for _, player in ipairs(removedPlayers) do 130 | self:_replicate( 131 | player, 132 | lens.name, 133 | serializedTarget, 134 | nil 135 | ) 136 | end 137 | end; 138 | 139 | Players.PlayerAdded:Connect(function(player) 140 | local payload = {} 141 | 142 | for _, replicated in ipairs(rocs:getLayers(self._component)) do 143 | -- Only do this because if the player is added to an exclusive 144 | -- one, they'll be gotten in the onUpdated 145 | if replicated.data.players == nil then 146 | local lens = replicated.instance 147 | table.insert(payload, { 148 | target = self:_serialize(lens.instance); 149 | data = getData(lens.data, replicated.data, player); 150 | component = lens.name; 151 | }) 152 | end 153 | end 154 | 155 | self:_replicatePayload(player, payload, self._eventInit) 156 | end) 157 | }) 158 | else 159 | self._event = ReplicatedStorage:WaitForChild(EVENT_NAME) 160 | self._eventInit = ReplicatedStorage:WaitForChild(EVENT_NAME_INIT) 161 | 162 | local function handleEvent(rocsName, payload) 163 | if rocsName ~= self.rocs.name then 164 | return 165 | end 166 | 167 | self:_reifyPayload(payload) 168 | end 169 | 170 | -- Delay until later to allow registration to complete 171 | spawn(function() 172 | handleEvent(self._eventInit.OnClientEvent:Wait()) 173 | self._event.OnClientEvent:Connect(handleEvent) 174 | end) 175 | end 176 | 177 | return setmetatable(self, Replicator) 178 | end 179 | 180 | function Replicator:_reifyPayload(payload) 181 | for _, entry in ipairs(payload) do 182 | 183 | if entry.target then 184 | local instance = self:_deserialize(entry.target) 185 | 186 | local pipeline = self.rocs:getPipeline(instance, "_remote", self.rocs.Internal) 187 | 188 | pipeline:addLayer(entry.component, entry.data) 189 | else 190 | warn(("Missing target from payload, does the client have access to this instance? \n\n %s"):format(inspect(entry))) 191 | end 192 | end 193 | end 194 | 195 | function Replicator:_deserialize(serializedTarget) 196 | if typeof(serializedTarget) == "Instance" then 197 | return serializedTarget 198 | end 199 | 200 | local deserializer = self._deserializers[serializedTarget.type] 201 | if not deserializer then 202 | error("Unable to deserialize object") -- TODO: Dump inspect of object 203 | end 204 | 205 | local object = deserializer(serializedTarget, self.rocs) 206 | 207 | return object or error("Deserialization failed for object") 208 | end 209 | 210 | function Replicator:_serialize(object) 211 | local serializer = 212 | typeof(object) == "Instance" 213 | and idpipeline 214 | or self:findSerializer(object) 215 | 216 | return 217 | serializer and serializer(object, self.rocs) 218 | or error(("Unable to serialize replicated component %s"):format(object)) 219 | end 220 | 221 | function Replicator:_replicate(player, component, target, data) 222 | return self:_replicatePayload(player, {{ 223 | target = target; 224 | data = data; 225 | component = component; 226 | }}) 227 | end 228 | 229 | function Replicator:_replicatePayload(player, payload, eventOverride) 230 | (eventOverride or self._event):FireClient(player, self.rocs.name, payload) 231 | end 232 | 233 | function Replicator:registerSerializer(class, callback) 234 | self._serializers[class] = callback 235 | end 236 | 237 | function Replicator:registerDeserializer(name, callback) 238 | assert(type(name) == "string", "Deserializer type must be a string") 239 | self._deserializers[name] = callback 240 | end 241 | 242 | local function find(class, map) 243 | if map[class] then 244 | return map[class] 245 | end 246 | 247 | local metatable = getmetatable(class) 248 | 249 | if metatable then 250 | return find(metatable, map) 251 | end 252 | end 253 | 254 | function Replicator:findSerializer(class) 255 | return find(class, self._serializers) 256 | end 257 | 258 | function Replicator:findDeserializer(name) 259 | return self._deserializers[name] 260 | end 261 | 262 | return Replicator 263 | -------------------------------------------------------------------------------- /lib/Core/init.spec.lua: -------------------------------------------------------------------------------- 1 | local t = require(script.Parent.Parent.Shared.t) 2 | local inspect = require(script.Parent.Parent.Shared.Inspect) -- luacheck: ignore 211 3 | local Util = require(script.Parent.Parent.Shared.Util) 4 | local Rocs = require(script.Parent) 5 | local Constants = require(script.Parent.Constants) 6 | 7 | local function makeTestCmp(rocs, callCounts) 8 | callCounts = callCounts or Util.callCounter() 9 | 10 | local reducers = rocs.reducers 11 | 12 | -- TODO: Test onUpdated in components 13 | 14 | return { 15 | name = "Test"; 16 | initialize = function(self) 17 | expect(self).to.be.ok() 18 | callCounts:call("testCmpInit") 19 | end; 20 | destroy = function(self) 21 | expect(self).to.be.ok() 22 | callCounts:call("testCmpDestroy") 23 | end; 24 | defaults = { 25 | testDefault = 5; 26 | }; 27 | reducer = reducers.structure({ 28 | nested = reducers.structure({ 29 | value = reducers.last; 30 | }) 31 | }); 32 | shouldUpdate = rocs.comparators.structure({ 33 | shouldUpdateTest = function() return false end 34 | }); 35 | check = t.interface({}); 36 | pipelineCheck = t.union(t.instance("Workspace"), t.instance("DataModel")); 37 | tag = "Test"; 38 | onUpdated = function(self) 39 | callCounts:call("onUpdated") 40 | self:dispatch("customUpdated") 41 | end; 42 | } 43 | end 44 | 45 | -- TODO: Test life cycle hooks 46 | 47 | return function() 48 | describe("Layers", function() 49 | local rocs = Rocs.new() 50 | local callCounts = Util.callCounter() 51 | local testCmp = makeTestCmp(rocs, callCounts) 52 | rocs:registerLayer(testCmp) 53 | 54 | local reducers = rocs.reducers 55 | local mtTest = rocs:registerLayer({ 56 | name = "MtTest"; 57 | reducer = reducers.structure({ 58 | num = reducers.add; 59 | }); 60 | check = t.interface({ 61 | num = t.number; 62 | }); 63 | onParentUpdated = function(self) 64 | callCounts:call("onParentUpdated") 65 | end 66 | }) 67 | 68 | it("should apply components", function() 69 | local ent = rocs:getPipeline(workspace, "foo") 70 | 71 | expect(callCounts.testCmpInit).to.equal(0) 72 | ent:addLayer(testCmp, { 73 | one = 1; 74 | }, { 75 | MtTest = { 76 | num = 1; 77 | } 78 | }) 79 | expect(callCounts.testCmpInit).to.equal(1) 80 | expect(callCounts.onParentUpdated).to.equal(1) 81 | ent:addBaseLayer("Test", { 82 | two = 2; 83 | }, { 84 | MtTest = { 85 | num = 2; 86 | } 87 | }) 88 | expect(callCounts.onParentUpdated).to.equal(2) 89 | 90 | local cmpAg = rocs._lenses._entities[workspace] and rocs._lenses._entities[workspace][testCmp] 91 | 92 | expect(cmpAg).to.be.ok() 93 | 94 | expect(cmpAg.components[Constants.SCOPE_BASE]).to.be.ok() 95 | expect(cmpAg.components[Constants.SCOPE_BASE].two).to.equal(2) 96 | 97 | expect(cmpAg.components.foo).to.be.ok() 98 | expect(cmpAg.components.foo.one).to.equal(1) 99 | 100 | expect(cmpAg:get()).to.equal(cmpAg.data) 101 | 102 | expect(cmpAg:get("one")).to.equal(1) 103 | expect(cmpAg:get("two")).to.equal(2) 104 | expect(cmpAg:get("testDefault")).to.equal(5) 105 | 106 | local listener = cmpAg:listen("customUpdated", function() 107 | callCounts:call("customUpdated") 108 | 109 | print'asdfasfsaf' 110 | end) 111 | 112 | local cmpAgEnt = rocs._lenses._entities[cmpAg][mtTest] 113 | 114 | expect(cmpAgEnt).to.be.ok() 115 | 116 | expect(cmpAgEnt:get("num")).to.equal(3) 117 | 118 | expect(cmpAg:get("three")).to.never.be.ok() 119 | cmpAg:set("three", 3) 120 | expect(cmpAg:get("three")).to.equal(3) 121 | 122 | cmpAg:removeListener("customUpdated", listener) 123 | 124 | cmpAg:set("three", Rocs.None) 125 | expect(cmpAg:get("three")).to.never.be.ok() 126 | 127 | expect(tostring(cmpAg)).to.equal("Lens(Test)") 128 | 129 | expect(cmpAg:get("one")).to.equal(1) 130 | 131 | ent:removeLayer(testCmp) 132 | ent:removeBaseLayer(testCmp) 133 | expect(callCounts.testCmpDestroy).to.equal(1) 134 | expect(callCounts.onUpdated).to.equal(5) 135 | expect(callCounts.customUpdated).to.equal(1) 136 | expect(callCounts.onParentUpdated).to.equal(5) 137 | end) 138 | 139 | it("should allow looping over components", function() 140 | local entWorkspace = rocs:getPipeline(workspace, "foo") 141 | entWorkspace:addLayer(testCmp) 142 | 143 | local componentArray = rocs:getLayers(testCmp) 144 | 145 | expect(#componentArray).to.equal(1) 146 | 147 | entWorkspace:addBaseLayer(testCmp, {num = 1}) 148 | expect(#componentArray).to.equal(1) 149 | 150 | local entGame = rocs:getPipeline(game, "foo") 151 | entGame:addLayer(testCmp) 152 | 153 | expect(#componentArray).to.equal(2) 154 | end) 155 | end) 156 | 157 | -- describe("Systems", function() 158 | -- local rocs = Rocs.new() 159 | -- rocs:registerLayer(makeTestCmp(rocs)) 160 | 161 | -- it("should fire lifecycle methods", function() 162 | -- local dep = rocs.dependencies:any( 163 | -- rocs.dependencies:all("Test") 164 | -- ) 165 | 166 | -- local bindable = Instance.new("BindableEvent") 167 | -- local counter = Util.callCounter() 168 | -- local registeredSystem 169 | -- registeredSystem = rocs:registerSystem({ 170 | -- name = "test"; 171 | 172 | -- initialize = function(self) 173 | -- counter:call("initialize") 174 | -- expect(self).to.be.ok() 175 | -- end; 176 | 177 | -- destroy = function(self) 178 | -- counter:call("destroy") 179 | -- expect(self).to.be.ok() 180 | -- end; 181 | -- }, { 182 | -- dep:onAdded(function(self, e) 183 | -- expect(getmetatable(self).name).to.equal("test") 184 | -- expect(e.pipeline.scope).to.equal("system__test") 185 | -- expect(e.components.Test).to.be.ok() 186 | -- expect(e.components.Test:get("one")).to.equal(1) 187 | 188 | -- counter:call("added") 189 | -- end); 190 | 191 | -- dep:onUpdated(function(self, e) 192 | -- expect(getmetatable(self).name).to.equal("test") 193 | -- expect(e.pipeline.scope).to.equal("system__test") 194 | 195 | -- expect(e.components.Test).to.be.ok() 196 | -- if counter.updated == 0 then 197 | -- expect(e.components.Test:get("one")).to.equal(1) 198 | -- end 199 | 200 | -- counter:call("updated") 201 | -- end); 202 | 203 | -- dep:onRemoved(function(self, e) 204 | -- expect(getmetatable(self).name).to.equal("test") 205 | -- expect(e.pipeline.scope).to.equal("system__test") 206 | -- expect(e.components.Test).to.be.ok() 207 | -- expect(e.components.Test:get("one")).to.equal(nil) 208 | 209 | -- counter:call("removed") 210 | -- end); 211 | 212 | -- dep:onInterval(5, function(system, dt) 213 | -- counter:call("interval") 214 | -- end); 215 | 216 | -- dep:onEvent(bindable.Event, function(system, param) 217 | -- expect(getmetatable(system)).to.equal(registeredSystem) 218 | -- expect(param).to.equal("param") 219 | -- counter:call("event") 220 | -- end) 221 | -- }) 222 | -- local ent = rocs:getPipeline(workspace, "foo") 223 | 224 | -- bindable:Fire("foo") 225 | -- expect(counter.event).to.equal(0) 226 | 227 | -- expect(dep:entities()()).to.equal(nil) 228 | 229 | -- expect(counter.initialize).to.equal(0) 230 | -- ent:addBaseLayer("Test", { one = 1 }) 231 | -- expect(counter.initialize).to.equal(1) 232 | -- expect(counter.added).to.equal(1) 233 | -- expect(counter.updated).to.equal(1) 234 | 235 | -- ent:getLayer("Test"):set("shouldUpdateTest", 1) 236 | 237 | -- -- TODO: Write more tests for entities 238 | -- expect(dep:entities()()).to.equal(ent.instance) 239 | 240 | -- expect(counter.event).to.equal(0) 241 | -- bindable:Fire("param") 242 | -- expect(counter.event).to.equal(1) 243 | 244 | -- expect(counter.destroy).to.equal(0) 245 | -- ent:removeBaseLayer("Test") 246 | -- expect(counter.destroy).to.equal(1) 247 | 248 | -- expect(counter.updated).to.equal(2) 249 | -- expect(counter.removed).to.equal(1) 250 | 251 | -- bindable:Fire("bar") 252 | -- expect(counter.event).to.equal(1) 253 | 254 | -- expect(counter.interval).to.equal(0) 255 | -- end) 256 | -- end) 257 | 258 | -- describe("Layers", function() 259 | -- local rocs = Rocs.new() 260 | -- local callCounts = Util.callCounter() 261 | -- local testCmp = makeTestCmp(rocs, callCounts) 262 | -- rocs:registerLayer(testCmp) 263 | 264 | -- it("should add and remove components", function() 265 | -- local ent = rocs:getPipeline(workspace, "rawr") 266 | 267 | -- local layerId = ent:addLayer({ 268 | -- [workspace] = { 269 | -- Test = { 270 | -- one = 1 271 | -- } 272 | -- } 273 | -- }) 274 | 275 | -- local lens = ent:getLayer("Test") 276 | -- expect(lens:get("one")).to.equal(1) 277 | 278 | -- ent:removeLayer(layerId) 279 | 280 | -- expect(lens:get("one")).to.equal(nil) 281 | -- end) 282 | -- end) 283 | end 284 | -------------------------------------------------------------------------------- /lib/Core/Lens/LensCollection.lua: -------------------------------------------------------------------------------- 1 | local I = require(script.Parent.Parent.Types) 2 | local Util = require(script.Parent.Parent.Parent.Shared.Util) 3 | local Lens = require(script.Parent.Lens) 4 | local Constants = require(script.Parent.Parent.Constants) 5 | 6 | local LensCollection = {} 7 | LensCollection.__index = LensCollection 8 | 9 | function LensCollection.new(rocs) 10 | return setmetatable({ 11 | rocs = rocs; 12 | _components = {}; 13 | _entities = {}; 14 | _lenses = {}; 15 | }, LensCollection) 16 | end 17 | 18 | local function makeArrayPipelineCheck(array) 19 | return function(instance) 20 | for _, className in ipairs(array) do 21 | if instance:IsA(className) then 22 | return true 23 | end 24 | end 25 | 26 | return 27 | false, 28 | ("Instance type %q is not allowed to have this component!") 29 | :format(instance.ClassName) 30 | end 31 | end 32 | 33 | function LensCollection.runPipelineCheck(staticLens, instance) 34 | if staticLens.pipelineCheck == nil then 35 | return true 36 | end 37 | 38 | if type(staticLens.pipelineCheck) == "table" then 39 | staticLens.pipelineCheck = makeArrayPipelineCheck(staticLens.pipelineCheck) 40 | end 41 | 42 | return staticLens.pipelineCheck(instance) 43 | end 44 | 45 | function LensCollection:register(componentDefinition) 46 | assert(I.LayerDefinition(componentDefinition)) 47 | assert(self._components[componentDefinition.name] == nil, "A component with this name is already registered!") 48 | 49 | setmetatable(componentDefinition, Lens) 50 | 51 | componentDefinition.__index = componentDefinition 52 | componentDefinition.__tostring = Lens.__tostring 53 | componentDefinition.rocs = self.rocs 54 | 55 | componentDefinition.new = componentDefinition.new or function() 56 | return setmetatable({}, componentDefinition) 57 | end 58 | 59 | self._components[componentDefinition.name] = componentDefinition 60 | self._components[componentDefinition] = componentDefinition 61 | 62 | return componentDefinition 63 | end 64 | 65 | function LensCollection:construct(staticLens, instance) 66 | local lens = staticLens.new() 67 | 68 | assert( 69 | getmetatable(lens) == staticLens, 70 | "Metatable of constructed component lens must be static component lens" 71 | ) 72 | 73 | lens.components = {} 74 | lens.instance = instance 75 | 76 | self:_dispatchLifecycle(lens, "initialize") 77 | 78 | return lens 79 | end 80 | 81 | function LensCollection:deconstruct(lens) 82 | -- destroy is called in removeLayer for correct timing 83 | 84 | local staticLens = getmetatable(lens) 85 | self._entities[lens.instance][staticLens] = nil 86 | 87 | local array = self._lenses[staticLens] 88 | 89 | for i, v in ipairs(array) do 90 | if v == lens then 91 | table.remove(array, i) 92 | break 93 | end 94 | end 95 | 96 | if #array == 0 then 97 | self._lenses[staticLens] = nil 98 | end 99 | 100 | if next(self._entities[lens.instance]) == nil then 101 | self._entities[lens.instance] = nil 102 | end 103 | 104 | self:removeAllLayers(lens) 105 | end 106 | 107 | function LensCollection:addLayer(instance, staticLens, scope, data, metacomponents) 108 | if data == nil then 109 | return self:removeLayer(instance, staticLens, scope), false 110 | end 111 | 112 | assert(LensCollection.runPipelineCheck(staticLens, instance)) 113 | 114 | if self._entities[instance] == nil then 115 | self._entities[instance] = {} 116 | end 117 | 118 | if self._lenses[staticLens] == nil then 119 | self._lenses[staticLens] = {} 120 | end 121 | 122 | local lens = self._entities[instance][staticLens] 123 | local isNew = false 124 | 125 | if lens == nil then 126 | isNew = true 127 | lens = self:construct(staticLens, instance) 128 | self._entities[instance][staticLens] = lens 129 | 130 | table.insert(self._lenses[staticLens], lens) 131 | end 132 | 133 | lens.components[scope] = data 134 | 135 | self:_dispatchLayerChange(lens) 136 | 137 | local pendingParentUpdated = {} 138 | 139 | if isNew and staticLens.components then 140 | for componentResolvable, metacomponentData in pairs(staticLens.components) do 141 | local metacomponentstaticLens = self:getStatic(componentResolvable) 142 | 143 | local metacomponentLens = self:addLayer( 144 | lens, 145 | metacomponentstaticLens, 146 | Constants.SCOPE_BASE, 147 | metacomponentData 148 | ) 149 | 150 | pendingParentUpdated[metacomponentLens] = true 151 | end 152 | end 153 | 154 | if metacomponents then 155 | for componentResolvable, metacomponentData in pairs(metacomponents) do 156 | local metacomponentstaticLens = self:getStatic(componentResolvable) 157 | 158 | local metacomponentLens, wasNew = self:addLayer( 159 | lens, 160 | metacomponentstaticLens, 161 | scope, 162 | metacomponentData 163 | ) 164 | 165 | if wasNew then 166 | pendingParentUpdated[metacomponentLens] = true 167 | end 168 | end 169 | end 170 | 171 | -- De-duplicate onParentUpdated calls in case both tables have same 172 | for metacomponentLens in pairs(pendingParentUpdated) do 173 | self:_dispatchLifecycle( 174 | metacomponentLens, 175 | Constants.LIFECYCLE_PARENT_UPDATED 176 | ) 177 | end 178 | 179 | return lens, isNew 180 | end 181 | 182 | function LensCollection:removeAllLayers(instance) 183 | if self._entities[instance] == nil then 184 | return 185 | end 186 | 187 | for _, lens in ipairs(self:getAll(instance)) do 188 | lens.components = {} 189 | 190 | self:deconstruct(lens) 191 | 192 | self:_dispatchLayerChange(lens) 193 | 194 | self:_dispatchLifecycle(lens, "destroy") 195 | end 196 | end 197 | 198 | function LensCollection:removeLayer(instance, staticLens, scope) 199 | if 200 | self._entities[instance] == nil 201 | or self._entities[instance][staticLens] == nil 202 | then 203 | return 204 | end 205 | 206 | local lens = self._entities[instance][staticLens] 207 | 208 | lens.components[scope] = nil 209 | 210 | self:_dispatchLayerChange(lens) 211 | 212 | local shouldDestroy = next(lens.components) == nil 213 | if shouldDestroy then 214 | self:deconstruct(lens) 215 | end 216 | 217 | if shouldDestroy then 218 | self:_dispatchLifecycle(lens, "destroy") 219 | end 220 | 221 | -- TODO: Should destroy be deffered to end-of-frame? 222 | end 223 | 224 | function LensCollection:getLayer(instance, staticLens, scope) 225 | if 226 | self._entities[instance] == nil 227 | or self._entities[instance][staticLens] == nil 228 | then 229 | return 230 | end 231 | 232 | return self._entities[instance][staticLens].components[scope] 233 | end 234 | 235 | function LensCollection:getAll(instance) 236 | local lenses = {} 237 | 238 | if self._entities[instance] ~= nil then 239 | for _, lens in pairs(self._entities[instance]) do 240 | table.insert(lenses, lens) 241 | end 242 | end 243 | 244 | return lenses 245 | end 246 | 247 | function LensCollection:get(instance, staticLens) 248 | return 249 | self._entities[instance] 250 | and self._entities[instance][staticLens] 251 | end 252 | 253 | function LensCollection:getStatic(componentResolvable) 254 | return 255 | self:resolve(componentResolvable) 256 | or error(("Cannot resolve component %s"):format(componentResolvable)) 257 | end 258 | 259 | function LensCollection:resolve(componentResolvable) 260 | return self._components[componentResolvable] 261 | or ( 262 | type(componentResolvable) == "table" 263 | and getmetatable(componentResolvable) == Lens 264 | and componentResolvable 265 | ) 266 | end 267 | 268 | function LensCollection:reduce(lens) 269 | if next(lens.components) == nil then 270 | return 271 | end 272 | 273 | local values = { lens.components[Constants.SCOPE_REMOTE] } 274 | table.insert(values, lens.components[Constants.SCOPE_BASE]) 275 | 276 | for name, component in pairs(lens.components) do 277 | if Constants.RESERVED_SCOPES[name] == nil then 278 | table.insert(values, component) 279 | end 280 | end 281 | 282 | local staticLens = getmetatable(lens) 283 | 284 | local reducedValue = (staticLens.reducer or self.rocs.reducers.default)(values) 285 | 286 | local data = reducedValue 287 | if staticLens.defaults and type(reducedValue) == "table" then 288 | staticLens.defaults.__index = staticLens.defaults 289 | data = setmetatable( 290 | reducedValue, 291 | staticLens.defaults 292 | ) 293 | end 294 | 295 | if staticLens.check then 296 | assert(staticLens.check(data)) 297 | end 298 | 299 | return data 300 | end 301 | 302 | function LensCollection:_dispatchLifecycle(lens, stage) 303 | lens:dispatch(stage) 304 | 305 | self.rocs:_dispatchLifecycle(lens, stage) 306 | end 307 | 308 | function LensCollection:_dispatchLayerChange(lens) 309 | local lastData = lens.data 310 | local newData = self:reduce(lens) 311 | 312 | lens.data = newData 313 | lens.lastData = lastData 314 | 315 | if lastData == nil and newData ~= nil then 316 | self:_dispatchLifecycle(lens, Constants.LIFECYCLE_ADDED) 317 | end 318 | 319 | local staticLens = getmetatable(lens) 320 | 321 | if (staticLens.shouldUpdate or self.rocs.comparators.default)(newData, lastData) then 322 | self:_dispatchLifecycle(lens, Constants.LIFECYCLE_UPDATED) 323 | 324 | local childLenss = self:getAll(lens) 325 | for _, childLens in ipairs(childLenss) do 326 | self:_dispatchLifecycle( 327 | childLens, 328 | Constants.LIFECYCLE_PARENT_UPDATED 329 | ) 330 | end 331 | end 332 | 333 | if newData == nil then 334 | self:_dispatchLifecycle(lens, Constants.LIFECYCLE_REMOVED) 335 | end 336 | 337 | lens.lastData = nil 338 | end 339 | 340 | return LensCollection 341 | -------------------------------------------------------------------------------- /lib/Shared/Inspect.lua: -------------------------------------------------------------------------------- 1 | local inspect ={ 2 | _VERSION = 'inspect.lua 3.1.0', 3 | _URL = 'http://github.com/kikito/inspect.lua', 4 | _DESCRIPTION = 'human-readable representations of tables', 5 | _LICENSE = [[ 6 | MIT LICENSE 7 | 8 | Copyright (c) 2013 Enrique García Cota 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ]] 29 | } 30 | 31 | local tostring = tostring 32 | 33 | inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) 34 | inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) 35 | 36 | local function rawpairs(t) 37 | return next, t, nil 38 | end 39 | 40 | -- Apostrophizes the string if it has quotes, but not aphostrophes 41 | -- Otherwise, it returns a regular quoted string 42 | local function smartQuote(str) 43 | if str:match('"') and not str:match("'") then 44 | return "'" .. str .. "'" 45 | end 46 | return '"' .. str:gsub('"', '\\"') .. '"' 47 | end 48 | 49 | -- \a => '\\a', \0 => '\\0', 31 => '\31' 50 | local shortControlCharEscapes = { 51 | ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", 52 | ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" 53 | } 54 | local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 55 | for i=0, 31 do 56 | local ch = string.char(i) 57 | if not shortControlCharEscapes[ch] then 58 | shortControlCharEscapes[ch] = "\\"..i 59 | longControlCharEscapes[ch] = string.format("\\%03d", i) 60 | end 61 | end 62 | 63 | local function escape(str) 64 | return (str:gsub("\\", "\\\\") 65 | :gsub("(%c)%f[0-9]", longControlCharEscapes) 66 | :gsub("%c", shortControlCharEscapes)) 67 | end 68 | 69 | local function isIdentifier(str) 70 | return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) 71 | end 72 | 73 | local function isSequenceKey(k, sequenceLength) 74 | return type(k) == 'number' 75 | and 1 <= k 76 | and k <= sequenceLength 77 | and math.floor(k) == k 78 | end 79 | 80 | local defaultTypeOrders = { 81 | ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, 82 | ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 83 | } 84 | 85 | local function sortKeys(a, b) 86 | local ta, tb = type(a), type(b) 87 | 88 | -- strings and numbers are sorted numerically/alphabetically 89 | if ta == tb and (ta == 'string' or ta == 'number') then return a < b end 90 | 91 | local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] 92 | -- Two default types are compared according to the defaultTypeOrders table 93 | if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] 94 | elseif dta then return true -- default types before custom ones 95 | elseif dtb then return false -- custom types after default ones 96 | end 97 | 98 | -- custom types are sorted out alphabetically 99 | return ta < tb 100 | end 101 | 102 | -- For implementation reasons, the behavior of rawlen & # is "undefined" when 103 | -- tables aren't pure sequences. So we implement our own # operator. 104 | local function getSequenceLength(t) 105 | local len = 1 106 | local v = rawget(t,len) 107 | while v ~= nil do 108 | len = len + 1 109 | v = rawget(t,len) 110 | end 111 | return len - 1 112 | end 113 | 114 | local function getNonSequentialKeys(t) 115 | local keys, keysLength = {}, 0 116 | local sequenceLength = getSequenceLength(t) 117 | for k,_ in rawpairs(t) do 118 | if not isSequenceKey(k, sequenceLength) then 119 | keysLength = keysLength + 1 120 | keys[keysLength] = k 121 | end 122 | end 123 | table.sort(keys, sortKeys) 124 | return keys, keysLength, sequenceLength 125 | end 126 | 127 | local function countTableAppearances(t, tableAppearances) 128 | tableAppearances = tableAppearances or {} 129 | 130 | if type(t) == 'table' then 131 | if not tableAppearances[t] then 132 | tableAppearances[t] = 1 133 | for k,v in rawpairs(t) do 134 | countTableAppearances(k, tableAppearances) 135 | countTableAppearances(v, tableAppearances) 136 | end 137 | countTableAppearances(getmetatable(t), tableAppearances) 138 | else 139 | tableAppearances[t] = tableAppearances[t] + 1 140 | end 141 | end 142 | 143 | return tableAppearances 144 | end 145 | 146 | local copySequence = function(s) 147 | local copy, len = {}, #s 148 | for i=1, len do copy[i] = s[i] end 149 | return copy, len 150 | end 151 | 152 | local function makePath(path, ...) 153 | local keys = {...} 154 | local newPath, len = copySequence(path) 155 | for i=1, #keys do 156 | newPath[len + i] = keys[i] 157 | end 158 | return newPath 159 | end 160 | 161 | local function processRecursive(process, item, path, visited) 162 | if item == nil then return nil end 163 | if visited[item] then return visited[item] end 164 | 165 | local processed = process(item, path) 166 | if type(processed) == 'table' then 167 | local processedCopy = {} 168 | visited[item] = processedCopy 169 | local processedKey 170 | 171 | for k,v in rawpairs(processed) do 172 | processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) 173 | if processedKey ~= nil then 174 | processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) 175 | end 176 | end 177 | 178 | local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) 179 | if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field 180 | setmetatable(processedCopy, mt) 181 | processed = processedCopy 182 | end 183 | return processed 184 | end 185 | 186 | 187 | 188 | ------------------------------------------------------------------- 189 | 190 | local Inspector = {} 191 | local Inspector_mt = {__index = Inspector} 192 | 193 | function Inspector:puts(...) 194 | local args = {...} 195 | local buffer = self.buffer 196 | local len = #buffer 197 | for i=1, #args do 198 | len = len + 1 199 | buffer[len] = args[i] 200 | end 201 | end 202 | 203 | function Inspector:down(f) 204 | self.level = self.level + 1 205 | f() 206 | self.level = self.level - 1 207 | end 208 | 209 | function Inspector:tabify() 210 | self:puts(self.newline, string.rep(self.indent, self.level)) 211 | end 212 | 213 | function Inspector:alreadyVisited(v) 214 | return self.ids[v] ~= nil 215 | end 216 | 217 | function Inspector:getId(v) 218 | local id = self.ids[v] 219 | if not id then 220 | local tv = type(v) 221 | id = (self.maxIds[tv] or 0) + 1 222 | self.maxIds[tv] = id 223 | self.ids[v] = id 224 | end 225 | return tostring(id) 226 | end 227 | 228 | function Inspector:putKey(k) 229 | if isIdentifier(k) then return self:puts(k) end 230 | self:puts("[") 231 | self:putValue(k) 232 | self:puts("]") 233 | end 234 | 235 | function Inspector:putTable(t) 236 | if t == inspect.KEY or t == inspect.METATABLE then 237 | self:puts(tostring(t)) 238 | elseif self:alreadyVisited(t) then 239 | self:puts('') 240 | elseif self.level >= self.depth then 241 | self:puts('{...}') 242 | else 243 | if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end 244 | 245 | local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) 246 | local mt = getmetatable(t) 247 | 248 | self:puts('{') 249 | self:down(function() 250 | local count = 0 251 | for i=1, sequenceLength do 252 | if count > 0 then self:puts(',') end 253 | self:puts(' ') 254 | self:putValue(t[i]) 255 | count = count + 1 256 | end 257 | 258 | for i=1, nonSequentialKeysLength do 259 | local k = nonSequentialKeys[i] 260 | if count > 0 then self:puts(',') end 261 | self:tabify() 262 | self:putKey(k) 263 | self:puts(' = ') 264 | self:putValue(t[k]) 265 | count = count + 1 266 | end 267 | 268 | if type(mt) == 'table' then 269 | if count > 0 then self:puts(',') end 270 | self:tabify() 271 | self:puts(' = ') 272 | self:putValue(mt) 273 | end 274 | end) 275 | 276 | if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } 277 | self:tabify() 278 | elseif sequenceLength > 0 then -- array tables have one extra space before closing } 279 | self:puts(' ') 280 | end 281 | 282 | self:puts('}') 283 | end 284 | end 285 | 286 | function Inspector:putValue(v) 287 | local tv = type(v) 288 | 289 | if tv == 'string' then 290 | self:puts(smartQuote(escape(v))) 291 | elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or 292 | tv == 'cdata' or tv == 'ctype' then 293 | self:puts(tostring(v)) 294 | elseif tv == 'table' then 295 | self:putTable(v) 296 | else 297 | self:puts('<', tv, ' ', self:getId(v), '>') 298 | end 299 | end 300 | 301 | ------------------------------------------------------------------- 302 | 303 | function inspect.inspect(root, options) 304 | options = options or {} 305 | 306 | local depth = options.depth or math.huge 307 | local newline = options.newline or '\n' 308 | local indent = options.indent or ' ' 309 | local process = options.process 310 | 311 | if process then 312 | root = processRecursive(process, root, {}, {}) 313 | end 314 | 315 | local inspector = setmetatable({ 316 | depth = depth, 317 | level = 0, 318 | buffer = {}, 319 | ids = {}, 320 | maxIds = {}, 321 | newline = newline, 322 | indent = indent, 323 | tableAppearances = countTableAppearances(root) 324 | }, Inspector_mt) 325 | 326 | inspector:putValue(root) 327 | 328 | return table.concat(inspector.buffer) 329 | end 330 | 331 | setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) 332 | 333 | return inspect.inspect 334 | -------------------------------------------------------------------------------- /lib/Shared/t.lua: -------------------------------------------------------------------------------- 1 | 2 | -- t: a runtime typechecker for Roblox 3 | 4 | -- regular lua compatibility 5 | local typeof = typeof or type 6 | 7 | local function primitive(typeName) 8 | return function(value) 9 | local valueType = typeof(value) 10 | if valueType == typeName then 11 | return true 12 | else 13 | return false, string.format("%s expected, got %s", typeName, valueType) 14 | end 15 | end 16 | end 17 | 18 | local t = {} 19 | 20 | --[[** 21 | matches any type except nil 22 | 23 | @param value The value to check against 24 | 25 | @returns True iff the condition is satisfied, false otherwise 26 | **--]] 27 | function t.any(value) 28 | if value ~= nil then 29 | return true 30 | else 31 | return false, "any expected, got nil" 32 | end 33 | end 34 | 35 | --Lua primitives 36 | 37 | --[[** 38 | ensures Lua primitive boolean type 39 | 40 | @param value The value to check against 41 | 42 | @returns True iff the condition is satisfied, false otherwise 43 | **--]] 44 | t.boolean = primitive("boolean") 45 | 46 | --[[** 47 | ensures Lua primitive coroutine type 48 | 49 | @param value The value to check against 50 | 51 | @returns True iff the condition is satisfied, false otherwise 52 | **--]] 53 | t.coroutine = primitive("thread") 54 | 55 | --[[** 56 | ensures Lua primitive callback type 57 | 58 | @param value The value to check against 59 | 60 | @returns True iff the condition is satisfied, false otherwise 61 | **--]] 62 | t.callback = primitive("function") 63 | 64 | --[[** 65 | ensures Lua primitive none type 66 | 67 | @param value The value to check against 68 | 69 | @returns True iff the condition is satisfied, false otherwise 70 | **--]] 71 | t.none = primitive("nil") 72 | 73 | --[[** 74 | ensures Lua primitive string type 75 | 76 | @param value The value to check against 77 | 78 | @returns True iff the condition is satisfied, false otherwise 79 | **--]] 80 | t.string = primitive("string") 81 | 82 | --[[** 83 | ensures Lua primitive table type 84 | 85 | @param value The value to check against 86 | 87 | @returns True iff the condition is satisfied, false otherwise 88 | **--]] 89 | t.table = primitive("table") 90 | 91 | --[[** 92 | ensures Lua primitive userdata type 93 | 94 | @param value The value to check against 95 | 96 | @returns True iff the condition is satisfied, false otherwise 97 | **--]] 98 | t.userdata = primitive("userdata") 99 | 100 | --[[** 101 | ensures value is a number and non-NaN 102 | 103 | @param value The value to check against 104 | 105 | @returns True iff the condition is satisfied, false otherwise 106 | **--]] 107 | function t.number(value) 108 | local valueType = typeof(value) 109 | if valueType == "number" then 110 | if value == value then 111 | return true 112 | else 113 | return false, "unexpected NaN value" 114 | end 115 | else 116 | return false, string.format("number expected, got %s", valueType) 117 | end 118 | end 119 | 120 | --[[** 121 | ensures value is NaN 122 | 123 | @param value The value to check against 124 | 125 | @returns True iff the condition is satisfied, false otherwise 126 | **--]] 127 | function t.nan(value) 128 | if value ~= value then 129 | return true 130 | else 131 | return false, "unexpected non-NaN value" 132 | end 133 | end 134 | 135 | -- roblox types 136 | 137 | --[[** 138 | ensures Roblox Axes type 139 | 140 | @param value The value to check against 141 | 142 | @returns True iff the condition is satisfied, false otherwise 143 | **--]] 144 | t.Axes = primitive("Axes") 145 | 146 | --[[** 147 | ensures Roblox BrickColor type 148 | 149 | @param value The value to check against 150 | 151 | @returns True iff the condition is satisfied, false otherwise 152 | **--]] 153 | t.BrickColor = primitive("BrickColor") 154 | 155 | --[[** 156 | ensures Roblox CFrame type 157 | 158 | @param value The value to check against 159 | 160 | @returns True iff the condition is satisfied, false otherwise 161 | **--]] 162 | t.CFrame = primitive("CFrame") 163 | 164 | --[[** 165 | ensures Roblox Color3 type 166 | 167 | @param value The value to check against 168 | 169 | @returns True iff the condition is satisfied, false otherwise 170 | **--]] 171 | t.Color3 = primitive("Color3") 172 | 173 | --[[** 174 | ensures Roblox ColorSequence type 175 | 176 | @param value The value to check against 177 | 178 | @returns True iff the condition is satisfied, false otherwise 179 | **--]] 180 | t.ColorSequence = primitive("ColorSequence") 181 | 182 | --[[** 183 | ensures Roblox ColorSequenceKeypoint type 184 | 185 | @param value The value to check against 186 | 187 | @returns True iff the condition is satisfied, false otherwise 188 | **--]] 189 | t.ColorSequenceKeypoint = primitive("ColorSequenceKeypoint") 190 | 191 | --[[** 192 | ensures Roblox DockWidgetPluginGuiInfo type 193 | 194 | @param value The value to check against 195 | 196 | @returns True iff the condition is satisfied, false otherwise 197 | **--]] 198 | t.DockWidgetPluginGuiInfo = primitive("DockWidgetPluginGuiInfo") 199 | 200 | --[[** 201 | ensures Roblox Faces type 202 | 203 | @param value The value to check against 204 | 205 | @returns True iff the condition is satisfied, false otherwise 206 | **--]] 207 | t.Faces = primitive("Faces") 208 | 209 | --[[** 210 | ensures Roblox Instance type 211 | 212 | @param value The value to check against 213 | 214 | @returns True iff the condition is satisfied, false otherwise 215 | **--]] 216 | t.Instance = primitive("Instance") 217 | 218 | --[[** 219 | ensures Roblox NumberRange type 220 | 221 | @param value The value to check against 222 | 223 | @returns True iff the condition is satisfied, false otherwise 224 | **--]] 225 | t.NumberRange = primitive("NumberRange") 226 | 227 | --[[** 228 | ensures Roblox NumberSequence type 229 | 230 | @param value The value to check against 231 | 232 | @returns True iff the condition is satisfied, false otherwise 233 | **--]] 234 | t.NumberSequence = primitive("NumberSequence") 235 | 236 | --[[** 237 | ensures Roblox NumberSequenceKeypoint type 238 | 239 | @param value The value to check against 240 | 241 | @returns True iff the condition is satisfied, false otherwise 242 | **--]] 243 | t.NumberSequenceKeypoint = primitive("NumberSequenceKeypoint") 244 | 245 | --[[** 246 | ensures Roblox PathWaypoint type 247 | 248 | @param value The value to check against 249 | 250 | @returns True iff the condition is satisfied, false otherwise 251 | **--]] 252 | t.PathWaypoint = primitive("PathWaypoint") 253 | 254 | --[[** 255 | ensures Roblox PhysicalProperties type 256 | 257 | @param value The value to check against 258 | 259 | @returns True iff the condition is satisfied, false otherwise 260 | **--]] 261 | t.PhysicalProperties = primitive("PhysicalProperties") 262 | 263 | --[[** 264 | ensures Roblox Random type 265 | 266 | @param value The value to check against 267 | 268 | @returns True iff the condition is satisfied, false otherwise 269 | **--]] 270 | t.Random = primitive("Random") 271 | 272 | --[[** 273 | ensures Roblox Ray type 274 | 275 | @param value The value to check against 276 | 277 | @returns True iff the condition is satisfied, false otherwise 278 | **--]] 279 | t.Ray = primitive("Ray") 280 | 281 | --[[** 282 | ensures Roblox Rect type 283 | 284 | @param value The value to check against 285 | 286 | @returns True iff the condition is satisfied, false otherwise 287 | **--]] 288 | t.Rect = primitive("Rect") 289 | 290 | --[[** 291 | ensures Roblox Region3 type 292 | 293 | @param value The value to check against 294 | 295 | @returns True iff the condition is satisfied, false otherwise 296 | **--]] 297 | t.Region3 = primitive("Region3") 298 | 299 | --[[** 300 | ensures Roblox Region3int16 type 301 | 302 | @param value The value to check against 303 | 304 | @returns True iff the condition is satisfied, false otherwise 305 | **--]] 306 | t.Region3int16 = primitive("Region3int16") 307 | 308 | --[[** 309 | ensures Roblox TweenInfo type 310 | 311 | @param value The value to check against 312 | 313 | @returns True iff the condition is satisfied, false otherwise 314 | **--]] 315 | t.TweenInfo = primitive("TweenInfo") 316 | 317 | --[[** 318 | ensures Roblox UDim type 319 | 320 | @param value The value to check against 321 | 322 | @returns True iff the condition is satisfied, false otherwise 323 | **--]] 324 | t.UDim = primitive("UDim") 325 | 326 | --[[** 327 | ensures Roblox UDim2 type 328 | 329 | @param value The value to check against 330 | 331 | @returns True iff the condition is satisfied, false otherwise 332 | **--]] 333 | t.UDim2 = primitive("UDim2") 334 | 335 | --[[** 336 | ensures Roblox Vector2 type 337 | 338 | @param value The value to check against 339 | 340 | @returns True iff the condition is satisfied, false otherwise 341 | **--]] 342 | t.Vector2 = primitive("Vector2") 343 | 344 | --[[** 345 | ensures Roblox Vector3 type 346 | 347 | @param value The value to check against 348 | 349 | @returns True iff the condition is satisfied, false otherwise 350 | **--]] 351 | t.Vector3 = primitive("Vector3") 352 | 353 | --[[** 354 | ensures Roblox Vector3int16 type 355 | 356 | @param value The value to check against 357 | 358 | @returns True iff the condition is satisfied, false otherwise 359 | **--]] 360 | t.Vector3int16 = primitive("Vector3int16") 361 | 362 | -- roblox enum types 363 | 364 | --[[** 365 | ensures Roblox Enum type 366 | 367 | @param value The value to check against 368 | 369 | @returns True iff the condition is satisfied, false otherwise 370 | **--]] 371 | t.Enum = primitive("Enum") 372 | 373 | --[[** 374 | ensures Roblox EnumItem type 375 | 376 | @param value The value to check against 377 | 378 | @returns True iff the condition is satisfied, false otherwise 379 | **--]] 380 | t.EnumItem = primitive("EnumItem") 381 | 382 | --[[** 383 | ensures value is a given literal value 384 | 385 | @param literal The literal to use 386 | 387 | @returns A function that will return true iff the condition is passed 388 | **--]] 389 | function t.literal(literal) 390 | return function(value) 391 | if value ~= literal then 392 | return false, string.format("expected %s, got %s", tostring(literal), tostring(value)) 393 | end 394 | return true 395 | end 396 | end 397 | 398 | --[[** 399 | DEPRECATED 400 | Please use t.literal 401 | **--]] 402 | t.exactly = t.literal 403 | 404 | --[[** 405 | ensures value is an integer 406 | 407 | @param value The value to check against 408 | 409 | @returns True iff the condition is satisfied, false otherwise 410 | **--]] 411 | function t.integer(value) 412 | local success, errMsg = t.number(value) 413 | if not success then 414 | return false, errMsg or "" 415 | end 416 | if value%1 == 0 then 417 | return true 418 | else 419 | return false, string.format("integer expected, got %d", value) 420 | end 421 | end 422 | 423 | --[[** 424 | ensures value is a number where min <= value 425 | 426 | @param min The minimum to use 427 | 428 | @returns A function that will return true iff the condition is passed 429 | **--]] 430 | function t.numberMin(min) 431 | return function(value) 432 | local success, errMsg = t.number(value) 433 | if not success then 434 | return false, errMsg or "" 435 | end 436 | if value >= min then 437 | return true 438 | else 439 | return false, string.format("number >= %d expected, got %d", min, value) 440 | end 441 | end 442 | end 443 | 444 | --[[** 445 | ensures value is a number where value <= max 446 | 447 | @param max The maximum to use 448 | 449 | @returns A function that will return true iff the condition is passed 450 | **--]] 451 | function t.numberMax(max) 452 | return function(value) 453 | local success, errMsg = t.number(value) 454 | if not success then 455 | return false, errMsg 456 | end 457 | if value <= max then 458 | return true 459 | else 460 | return false, string.format("number <= %d expected, got %d", max, value) 461 | end 462 | end 463 | end 464 | 465 | --[[** 466 | ensures value is a number where min < value 467 | 468 | @param min The minimum to use 469 | 470 | @returns A function that will return true iff the condition is passed 471 | **--]] 472 | function t.numberMinExclusive(min) 473 | return function(value) 474 | local success, errMsg = t.number(value) 475 | if not success then 476 | return false, errMsg or "" 477 | end 478 | if min < value then 479 | return true 480 | else 481 | return false, string.format("number > %d expected, got %d", min, value) 482 | end 483 | end 484 | end 485 | 486 | --[[** 487 | ensures value is a number where value < max 488 | 489 | @param max The maximum to use 490 | 491 | @returns A function that will return true iff the condition is passed 492 | **--]] 493 | function t.numberMaxExclusive(max) 494 | return function(value) 495 | local success, errMsg = t.number(value) 496 | if not success then 497 | return false, errMsg or "" 498 | end 499 | if value < max then 500 | return true 501 | else 502 | return false, string.format("number < %d expected, got %d", max, value) 503 | end 504 | end 505 | end 506 | 507 | --[[** 508 | ensures value is a number where value > 0 509 | 510 | @returns A function that will return true iff the condition is passed 511 | **--]] 512 | t.numberPositive = t.numberMinExclusive(0) 513 | 514 | --[[** 515 | ensures value is a number where value < 0 516 | 517 | @returns A function that will return true iff the condition is passed 518 | **--]] 519 | t.numberNegative = t.numberMaxExclusive(0) 520 | 521 | --[[** 522 | ensures value is a number where min <= value <= max 523 | 524 | @param min The minimum to use 525 | @param max The maximum to use 526 | 527 | @returns A function that will return true iff the condition is passed 528 | **--]] 529 | function t.numberConstrained(min, max) 530 | assert(t.number(min) and t.number(max)) 531 | local minCheck = t.numberMin(min) 532 | local maxCheck = t.numberMax(max) 533 | return function(value) 534 | local minSuccess, minErrMsg = minCheck(value) 535 | if not minSuccess then 536 | return false, minErrMsg or "" 537 | end 538 | 539 | local maxSuccess, maxErrMsg = maxCheck(value) 540 | if not maxSuccess then 541 | return false, maxErrMsg or "" 542 | end 543 | 544 | return true 545 | end 546 | end 547 | 548 | --[[** 549 | ensures value is a number where min < value < max 550 | 551 | @param min The minimum to use 552 | @param max The maximum to use 553 | 554 | @returns A function that will return true iff the condition is passed 555 | **--]] 556 | function t.numberConstrainedExclusive(min, max) 557 | assert(t.number(min) and t.number(max)) 558 | local minCheck = t.numberMinExclusive(min) 559 | local maxCheck = t.numberMaxExclusive(max) 560 | return function(value) 561 | local minSuccess, minErrMsg = minCheck(value) 562 | if not minSuccess then 563 | return false, minErrMsg or "" 564 | end 565 | 566 | local maxSuccess, maxErrMsg = maxCheck(value) 567 | if not maxSuccess then 568 | return false, maxErrMsg or "" 569 | end 570 | 571 | return true 572 | end 573 | end 574 | 575 | --[[** 576 | ensures value is either nil or passes check 577 | 578 | @param check The check to use 579 | 580 | @returns A function that will return true iff the condition is passed 581 | **--]] 582 | function t.optional(check) 583 | assert(t.callback(check)) 584 | return function(value) 585 | if value == nil then 586 | return true 587 | end 588 | local success, errMsg = check(value) 589 | if success then 590 | return true 591 | else 592 | return false, string.format("(optional) %s", errMsg or "") 593 | end 594 | end 595 | end 596 | 597 | --[[** 598 | matches given tuple against tuple type definition 599 | 600 | @param ... The type definition for the tuples 601 | 602 | @returns A function that will return true iff the condition is passed 603 | **--]] 604 | function t.tuple(...) 605 | local checks = {...} 606 | return function(...) 607 | local args = {...} 608 | for i = 1, #checks do 609 | local success, errMsg = checks[i](args[i]) 610 | if success == false then 611 | return false, string.format("Bad tuple index #%d:\n\t%s", i, errMsg or "") 612 | end 613 | end 614 | return true 615 | end 616 | end 617 | 618 | --[[** 619 | ensures all keys in given table pass check 620 | 621 | @param check The function to use to check the keys 622 | 623 | @returns A function that will return true iff the condition is passed 624 | **--]] 625 | function t.keys(check) 626 | assert(t.callback(check)) 627 | return function(value) 628 | local tableSuccess, tableErrMsg = t.table(value) 629 | if tableSuccess == false then 630 | return false, tableErrMsg or "" 631 | end 632 | 633 | for key in pairs(value) do 634 | local success, errMsg = check(key) 635 | if success == false then 636 | return false, string.format("bad key %s:\n\t%s", tostring(key), errMsg or "") 637 | end 638 | end 639 | 640 | return true 641 | end 642 | end 643 | 644 | --[[** 645 | ensures all values in given table pass check 646 | 647 | @param check The function to use to check the values 648 | 649 | @returns A function that will return true iff the condition is passed 650 | **--]] 651 | function t.values(check) 652 | assert(t.callback(check)) 653 | return function(value) 654 | local tableSuccess, tableErrMsg = t.table(value) 655 | if tableSuccess == false then 656 | return false, tableErrMsg or "" 657 | end 658 | 659 | for key, val in pairs(value) do 660 | local success, errMsg = check(val) 661 | if success == false then 662 | return false, string.format("bad value for key %s:\n\t%s", tostring(key), errMsg or "") 663 | end 664 | end 665 | 666 | return true 667 | end 668 | end 669 | 670 | --[[** 671 | ensures value is a table and all keys pass keyCheck and all values pass valueCheck 672 | 673 | @param keyCheck The function to use to check the keys 674 | @param valueCheck The function to use to check the values 675 | 676 | @returns A function that will return true iff the condition is passed 677 | **--]] 678 | function t.map(keyCheck, valueCheck) 679 | assert(t.callback(keyCheck), t.callback(valueCheck)) 680 | local keyChecker = t.keys(keyCheck) 681 | local valueChecker = t.values(valueCheck) 682 | return function(value) 683 | local keySuccess, keyErr = keyChecker(value) 684 | if not keySuccess then 685 | return false, keyErr or "" 686 | end 687 | 688 | local valueSuccess, valueErr = valueChecker(value) 689 | if not valueSuccess then 690 | return false, valueErr or "" 691 | end 692 | 693 | return true 694 | end 695 | end 696 | 697 | do 698 | local arrayKeysCheck = t.keys(t.integer) 699 | --[[** 700 | ensures value is an array and all values of the array match check 701 | 702 | @param check The check to compare all values with 703 | 704 | @returns A function that will return true iff the condition is passed 705 | **--]] 706 | function t.array(check) 707 | assert(t.callback(check)) 708 | local valuesCheck = t.values(check) 709 | return function(value) 710 | local keySuccess, keyErrMsg = arrayKeysCheck(value) 711 | if keySuccess == false then 712 | return false, string.format("[array] %s", keyErrMsg or "") 713 | end 714 | 715 | -- all keys are sequential 716 | local arraySize = #value 717 | for key in pairs(value) do 718 | if key < 1 or key > arraySize then 719 | return false, string.format("[array] key %s must be sequential", tostring(key)) 720 | end 721 | end 722 | 723 | local valueSuccess, valueErrMsg = valuesCheck(value) 724 | if not valueSuccess then 725 | return false, string.format("[array] %s", valueErrMsg or "") 726 | end 727 | 728 | return true 729 | end 730 | end 731 | end 732 | 733 | do 734 | local callbackArray = t.array(t.callback) 735 | --[[** 736 | creates a union type 737 | 738 | @param ... The checks to union 739 | 740 | @returns A function that will return true iff the condition is passed 741 | **--]] 742 | function t.union(...) 743 | local checks = {...} 744 | assert(callbackArray(checks)) 745 | return function(value) 746 | for _, check in pairs(checks) do 747 | if check(value) then 748 | return true 749 | end 750 | end 751 | return false, "bad type for union" 752 | end 753 | end 754 | 755 | --[[** 756 | creates an intersection type 757 | 758 | @param ... The checks to intersect 759 | 760 | @returns A function that will return true iff the condition is passed 761 | **--]] 762 | function t.intersection(...) 763 | local checks = {...} 764 | assert(callbackArray(checks)) 765 | return function(value) 766 | for _, check in pairs(checks) do 767 | local success, errMsg = check(value) 768 | if not success then 769 | return false, errMsg or "" 770 | end 771 | end 772 | return true 773 | end 774 | end 775 | end 776 | 777 | do 778 | local checkInterface = t.map(t.string, t.callback) 779 | --[[** 780 | ensures value matches given interface definition 781 | 782 | @param checkTable The interface definition 783 | 784 | @returns A function that will return true iff the condition is passed 785 | **--]] 786 | function t.interface(checkTable) 787 | assert(checkInterface(checkTable)) 788 | return function(value) 789 | local tableSuccess, tableErrMsg = t.table(value) 790 | if tableSuccess == false then 791 | return false, tableErrMsg or "" 792 | end 793 | 794 | for key, check in pairs(checkTable) do 795 | local success, errMsg = check(value[key]) 796 | if success == false then 797 | return false, string.format("[interface] bad value for %s:\n\t%s", key, errMsg or "") 798 | end 799 | end 800 | return true 801 | end 802 | end 803 | 804 | --[[** 805 | ensures value matches given interface definition strictly 806 | 807 | @param checkTable The interface definition 808 | 809 | @returns A function that will return true iff the condition is passed 810 | **--]] 811 | function t.strictInterface(checkTable) 812 | assert(checkInterface(checkTable)) 813 | return function(value) 814 | local tableSuccess, tableErrMsg = t.table(value) 815 | if tableSuccess == false then 816 | return false, tableErrMsg or "" 817 | end 818 | 819 | for key, check in pairs(checkTable) do 820 | local success, errMsg = check(value[key]) 821 | if success == false then 822 | return false, string.format("[interface] bad value for %s:\n\t%s", key, errMsg or "") 823 | end 824 | end 825 | 826 | for key in pairs(value) do 827 | if not checkTable[key] then 828 | return false, string.format("[interface] unexpected field '%s'", key) 829 | end 830 | end 831 | 832 | return true 833 | end 834 | end 835 | end 836 | 837 | --[[** 838 | ensure value is an Instance and it's ClassName matches the given ClassName 839 | 840 | @param className The class name to check for 841 | 842 | @returns A function that will return true iff the condition is passed 843 | **--]] 844 | function t.instance(className) 845 | assert(t.string(className)) 846 | return function(value) 847 | local instanceSuccess, instanceErrMsg = t.Instance(value) 848 | if not instanceSuccess then 849 | return false, instanceErrMsg or "" 850 | end 851 | 852 | if value.ClassName ~= className then 853 | return false, string.format("%s expected, got %s", className, value.ClassName) 854 | end 855 | 856 | return true 857 | end 858 | end 859 | 860 | --[[** 861 | ensure value is an Instance and it's ClassName matches the given ClassName by an IsA comparison 862 | 863 | @param className The class name to check for 864 | 865 | @returns A function that will return true iff the condition is passed 866 | **--]] 867 | function t.instanceIsA(className) 868 | assert(t.string(className)) 869 | return function(value) 870 | local instanceSuccess, instanceErrMsg = t.Instance(value) 871 | if not instanceSuccess then 872 | return false, instanceErrMsg or "" 873 | end 874 | 875 | if not value:IsA(className) then 876 | return false, string.format("%s expected, got %s", className, value.ClassName) 877 | end 878 | 879 | return true 880 | end 881 | end 882 | 883 | --[[** 884 | ensures value is an enum of the correct type 885 | 886 | @param enum The enum to check 887 | 888 | @returns A function that will return true iff the condition is passed 889 | **--]] 890 | function t.enum(enum) 891 | assert(t.Enum(enum)) 892 | return function(value) 893 | local enumItemSuccess, enumItemErrMsg = t.EnumItem(value) 894 | if not enumItemSuccess then 895 | return false, enumItemErrMsg 896 | end 897 | 898 | if value.EnumType == enum then 899 | return true 900 | else 901 | return false, string.format("enum of %s expected, got enum of %s", tostring(enum), tostring(value.EnumType)) 902 | end 903 | end 904 | end 905 | 906 | do 907 | local checkWrap = t.tuple(t.callback, t.callback) 908 | 909 | --[[** 910 | wraps a callback in an assert with checkArgs 911 | 912 | @param callback The function to wrap 913 | @param checkArgs The functon to use to check arguments in the assert 914 | 915 | @returns A function that first asserts using checkArgs and then calls callback 916 | **--]] 917 | function t.wrap(callback, checkArgs) 918 | assert(checkWrap(callback, checkArgs)) 919 | return function(...) 920 | assert(checkArgs(...)) 921 | return callback(...) 922 | end 923 | end 924 | end 925 | 926 | --[[** 927 | asserts a given check 928 | 929 | @param check The function to wrap with an assert 930 | 931 | @returns A function that simply wraps the given check in an assert 932 | **--]] 933 | function t.strict(check) 934 | return function(...) 935 | assert(check(...)) 936 | end 937 | end 938 | 939 | return t 940 | -------------------------------------------------------------------------------- /docs/index.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Rocs 3 | 4 | language_tabs: 5 | - lua 6 | - typescript 7 | 8 | toc_footers: 9 | - GitHub Repository 10 | - Examples 11 | 12 | includes: 13 | 14 | search: true 15 | --- 16 | 17 | # Rocs 18 | 19 | **Rocs** is a *progressive* entity-component-system (ECS) library developed for use in the [Roblox](http://developer.roblox.com) game engine. 20 | 21 | Rocs also performs the role of a general game state management library. Specifically, Rocs facilitates managing resources from multiple, unrelated places in your code base in a generic way. 22 | 23 | To that end, Rocs allows more than one of the same component to exist at the same time on a single entity. After every mutation, Rocs uses functions that you define to determine the single source of truth for that component. This allows disparate locations in your code base to influence a shared state. 24 | 25 | The Rocs workflow encourages compositional patterns, decoupled separation of concerns, and generic components which are reusable across many games. Because of this, patterns such as higher order components emerge naturally and allow you to keep grouped state changes atomic and concise. 26 | 27 | Additionally, Rocs supports middleware by means of life cycle hooks and augmenting core functionality. By default, two optional middleware are included: 28 | 29 | - Selectors, which offer a way to query against your current set of components within your game and create systems with behaviors which are only active when those queries are met. 30 | - Replication, which offers a batteries-included way to replicate components (or parts of components) you choose to clients. 31 | 32 | To get started with Rocs, sync in with [Rojo](https://rojo.space) or [download the latest release](https://github.com/rocs-rbx/Rocs/releases). 33 | 34 | 35 | 36 | ## Use cases 37 | 38 | > Register the component: 39 | 40 | ```lua 41 | rocs:registerComponent({ 42 | name = "WalkSpeed"; 43 | reducer = function (values) 44 | return math.min(unpack(values)) 45 | end; 46 | check = t.number; 47 | entityCheck = t.instance("Humanoid"); 48 | onUpdated = function (self) 49 | self.instance.WalkSpeed = self:getOr(16) 50 | end; 51 | }) 52 | ``` 53 | 54 | > Inside your weapon code: 55 | 56 | ```lua 57 | local entity = rocs:getEntity(humanoid, "weapon") 58 | 59 | -- On Equipped: 60 | entity:addComponent("WalkSpeed", 10) 61 | 62 | -- On Unequipped: 63 | entity:removeComponent("WalkSpeed") 64 | ``` 65 | 66 | > Inside your menu code: 67 | 68 | ```lua 69 | local entity = rocs:getEntity(humanoid, "menu") 70 | 71 | -- On Opened: 72 | entity:addComponent("WalkSpeed", 0) 73 | 74 | -- On Closed: 75 | entity:removeComponent("WalkSpeed") 76 | ``` 77 | 78 | ### Influencing shared state from disparate code locations 79 | 80 | A classic example of the necessity to share resources is changing the player's walk speed. 81 | 82 | Let's say you have a heavy weapon that you want to have slow down the player when he equips it. That's easy enough, you just set the walk speed when the weapon is equipped, and set it back to default when the weapon is unequipped. 83 | 84 | But what if you want something else to change the player's walk speed as well, potentially at the same time? For example, let's say you want opening a menu to set the character's WalkSpeed to `0`. 85 | 86 | If we follow the same flow as when we implemented the logic for the heavy weapon above, we now have a problem: The player can equip the heavy weapon and then open and close the menu. Now, the player can walk around at full speed with a heavy weapon equipped when they should still be slowed! 87 | 88 | Rocs solves this problem correctly by allowing you to apply a movement speed component from each location in the code base that needs to modify the walk speed. Each component can provide its own intensity level for which to affect the movement speed. Then, every time a component is added, modified, or removed, Rocs will group all components of the same type and determine a single value to set the WalkSpeed to, based on your defined [reducer function](#component-aggregates). 89 | 90 | In this case, the function will find the lowest value from all of the components, and then the player's WalkSpeed will be set to that number. Now, there is only one source of truth for the player's WalkSpeed, which solves all of our problems. When there are no longer any components of this type, you can clean up by setting the player's walk speed back to default in a destructor. 91 | 92 | 93 | 110 | 111 | # Concepts 112 | 113 | This section will give an overview of the fundamental concepts used in Rocs so that you can understand the details in the following sections. Don't focus too much on the code right now, that will come later. 114 | 115 | ## Rocs instance 116 | 117 | > Instantiating an instance of Rocs: 118 | 119 | ```lua 120 | local rocs = Rocs.new() 121 | ``` 122 | ```typescript 123 | const rocs = new Rocs() 124 | ``` 125 | 126 | Multiple versions of Rocs can exist at the same time in the same place and do not affect each other. **Rocs instance** refers to the instance of Rocs that you instantiated. Typically, you should only have one instance of Rocs per game. However, this allows Rocs to be used by libraries independently of the containing game. 127 | 128 | ## Entities 129 | 130 | > Getting an entity wrapper for Workspace, with the scope `"my scope"`: 131 | 132 | ```lua 133 | local entity = rocs:getEntity(workspace, "my scope") 134 | ``` 135 | ```typescript 136 | const entity = rocs.getEntity(workspace, 'my scope') 137 | ``` 138 | 139 | **Entities** are the objects that we put components on. These can be Roblox Instances or objects that you create yourself. "Instance" will generally be used to refer to these objects for purposes of conciseness, but remember that it does not need to be an actual Roblox Instance. 140 | 141 | For the sake of ergonomic API, Rocs provides an entity wrapper class which has many of the methods you will use to modify components. However, state is associated internally with the instance for which the wrapper belongs, and not within the wrapper itself. Multiple entity wrappers can exist for one instance. 142 | 143 | Entity wrappers must be created with a **scope**, which is some value associated with the source of the state change. For example, if you wanted to stop a player from moving when they open a menu, then the entity wrapper you create to enact that change could be created with the scope `"menu"` (a string). If instead some state change on a player was coming from within a tool, you could use the `Tool` object as the scope. 144 | 145 | The rationale behind this requirement is explained in the next section. 146 | 147 | ## Components 148 | 149 | > Adding a component to an entity: 150 | 151 | ```lua 152 | entity:addComponent("MyComponent", { 153 | foo = "bar"; 154 | }) 155 | ``` 156 | ```typescript 157 | import { MyComponent } from 'path/to/component' 158 | 159 | entity.addComponent(MyComponent, { 160 | foo: 'bar' 161 | }) 162 | ``` 163 | 164 | **Components** are, in essence, named groups of related data which can be associated with an entity. Every type of component you want to use must be explicitly registered on the Rocs instance with a unique name along with various other options. 165 | 166 | In Rocs, components are a little different from a typical ECS library. In order to allow disparate locations of your code to influence and mutate the same state in a safe way, multiple of the same component can exist on one entity. 167 | 168 | As discussed in the previous section, entity wrappers must be created with a scope. Multiple of the same component on one entity are distinguished by the scopes that you choose when adding the component. When you add or remove a component from an entity, you only affect components which are also branded with your scope. 169 | 170 | ### Reduced values 171 | 172 | Every time you add, modify, or remove a component, all components of the same type are grouped and fed into a **reducer function**. The resultant value is now referred to as a **reduced value**. 173 | 174 | ### Component Aggregates 175 | 176 | When you register a component type with Rocs, the table you register becomes the metatable of that component's **Aggregate**. Aggregates are essentially class instances which are used to represent all components of the same type on a single entity. They provide methods and have properties where you can access data from this component externally. 177 | 178 | Aggregates may have their own constructors and destructors, life cycle hooks, and custom methods if you wish. 179 | 180 | Only one instance of an Aggregate will exist per entity per component type. So if you have an entity with two components of type `"MyComponent"`, there will only be one `MyComponent` Aggregate for this entity. 181 | 182 | ### Tags and Base Scope Components 183 | 184 | Component types may also be optionally registered with a [CollectionService] tag. Rocs will then automatically add and remove this component from the Instance when the tag is added or removed. 185 | 186 | For situations like this where there is a component that exists at a more fundamental level, the **base scope** is used. The base scope is just like any other component scope, except it has special significance in performing the role as the "bottom-most" component (or in other words, the component that holds data for a component type without any additional modifiers from other places in the code base). 187 | 188 | > Changing the `name` field of the *base component*. 189 | 190 | ```lua 191 | entity:getBaseComponent("Character"):set("name", "New name") 192 | ``` 193 | 194 | The base scope is used for CollectionService tags, but is also useful in your own code. For example, if you had a component that represents a character, situations may arise where you want to modify data destructively, such as in changing a character's name. We aren't *influencing* the existing name, but instead we are completely changing it. In this case, you should change the `"name"` field on the base component directly. (This assumes that you initialized the base component earlier in your code, which should be the case if you have *basic* data like this). 195 | 196 | Base components also have special precedence when passed to reducer functions: the base component is always the first value. Values from other scopes subsequently follow in the array in any order. This is useful for situations when you want non-base scopes to partially override fields from the base scope. 197 | 198 | ## Patterns 199 | 200 | ### Higher-Order Components 201 | 202 | ```lua 203 | rocs:registerComponent({ 204 | name = "Empowered"; 205 | reducer = rocs.reducers.structure({ 206 | intensity = rocs.reducers.highest; 207 | }); 208 | check = t.interface({ 209 | intensity = t.number; 210 | }); 211 | onUpdated = function (self) 212 | local entity = rocs:getEntity(self.instance, self) -- This component is the scope. 213 | 214 | entity:addComponent("Health", self:getAnd("intensity", function(intensity) 215 | return { 216 | MaxHealthModifier = intensity; 217 | } 218 | end)) 219 | 220 | entity:addComponent("WalkSpeed", self:getAnd("intensity", function(intensity) 221 | return intensity * 16 222 | end)) 223 | end; 224 | }) 225 | ``` 226 | 227 | When creating games, it is often useful to have multiple levels of abstraction when dealing with state changes. 228 | 229 | For example, you might have a `WalkSpeed` component which is only focused on dealing with the player's movement speed and nothing more. You might also have a `Health` component which only deals with the player's health. It's a good idea to create small components like this with each of their concerns wholly separated and decoupled. 230 | 231 | However, it can become tiresome to modify these components individually if you often find yourself changing them in tandem. For example, if your game had a mechanic where players regularly received a buff that makes them walk faster *and* have more health, it's a good idea to group these state changes together so that they stay in sync and are applied atomically. 232 | 233 | Higher-order components allow you to do just this. A higher-order component is simply just a component that creates other components within their life cycle methods. In the code sample, we use the `onUpdated` life cycle method to add two components to the instance that this component is already attached to. 234 | 235 | `getAnd` is a helper function on Aggregates which gets a field from the current component's data and then calls the callback only if that value is non-nil. If the value is nil, `getAnd` just returns `nil` immediately. Adding a component with the value of `nil` is the same as removing it. 236 | 237 | 238 | 239 | ### Meta-components 240 | 241 | ```lua 242 | entity:addComponent("ComponentName", data, { 243 | Replicated = true 244 | }) 245 | ``` 246 | > The above code is equivalent to: 247 | 248 | ```lua 249 | local component = entity:addComponent("ComponentName", data) 250 | local componentEntity = rocs:getEntity(component) 251 | componentEntity:addComponent("Replicated", true) 252 | ``` 253 | 254 | Not only can components create other components, but components can actually be *on* other components. We refer to these components which are on other components as **meta-components**. Meta-components exist on the Aggregate instance of another component. 255 | 256 | Meta-components are useful to store state about components themselves rather than whatever the component manages. For example, the optional built-in `Replication` component, when present on another component, will cause that parent component to automatically be replicated over the network to clients. 257 | 258 | A short hand exists in the `addComponent` method on entity wrappers to add meta-components quickly immediately after adding your main component. You can also define implicit meta-components upon component registration which will always be added to those specific components via the `components` field. 259 | 260 | # Components API 261 | 262 | ## Registering a component 263 | ```lua 264 | rocs:registerComponent({ 265 | name = "MyComponent"; 266 | reducer = rocs.reducers.structure({ 267 | field = rocs.reducers.add; 268 | }); 269 | check = t.interface({ 270 | field = t.number; 271 | }); 272 | entityCheck = t.instanceIsA("BasePart"); 273 | tag = "MyComponentTag"; 274 | }) 275 | ``` 276 | 277 | `rocs:registerComponent` is used to register a component. 278 | 279 | ### Component registration fields 280 | 281 | Field | Type | Description | Required 282 | ----- | ---- | ----------- | -------- 283 | name | string | The name of the component. Must be unique across all registered components. | ✓ 284 | reducer | function `(values: Array) -> any` | A function that reduces component data into a reduced value. | 285 | check | function `(value: any) -> boolean` | A function which is invoked to type check the reduced value after reduction. | 286 | entityCheck | function | A function which is invoked to ensure this component is allowed to be on this entity. | 287 | defaults | dict | Default values for fields within this component. Becomes the metatable of the `data` field if it is a table. Does nothing if `data` is not a table. | 288 | components | dict | Default meta-components for this component. | 289 | initialize | method | Called when the Aggregate is instantiated 290 | destroy | method | Called when the Aggregate is destroyed 291 | onAdded | method | Called when the component is added for the first time 292 | onUpdated | method | Called when the component's reduced value is updated 293 | onParentUpdated | method | called when the component this meta-component is attached to is updated. (Only applies to meta-components). 294 | onRemoved | method | Called when the component is removed 295 | shouldUpdate | function `(a: any, b: any) -> boolean` | Called before onUpdated to decide if onUpdated should be called. 296 | tag | string | A CollectionService tag. When added to an Instance, Rocs will automatically create this component on the Instance when using the Tags middleware. | 297 | 298 | All *method*s above are called with `self` as their only parameter. 299 | 300 | ## Aggregate methods and fields 301 | 302 | The following fields are inherited from the base Aggregate class and must not be present in registered components. 303 | 304 | ### get 305 | `component:get(...fields) -> any` 306 | 307 | ```lua 308 | local component = entity:getComponent("MyComponent") 309 | 310 | local allData = component:get() 311 | 312 | local field = component:get("field") 313 | 314 | local nested = component:get("very", "nested", "field") 315 | ``` 316 | 317 | Retrieves a field from the current reduced value, or the entire thing if no parameters are given. 318 | 319 | Short circuits and returns `nil` if any value in the path to the last field is `nil`. 320 | 321 | ```lua 322 | local nestedField = component:get("one", "two", "three") 323 | ``` 324 | 325 | You can also get nested values from sub-tables in the component. 326 | 327 | ### getOr 328 | `component:getOr(...fields, default)` 329 | 330 | ```lua 331 | local value = component:getOr("field", "default value if nil") 332 | 333 | local value = component:getOr("field", function(field) 334 | return "default value if nil" 335 | end) 336 | ``` 337 | 338 | Similar to `get`, except returns the last parameter if the given field happens to be `nil`. 339 | 340 | If the last parameter is a function, the function will be called and its return value will be returned. 341 | 342 | ### getAnd 343 | `component:getAnd(...fields, callback)` 344 | 345 | ```lua 346 | local value = component:getAnd("field", function(field) 347 | return field or "default value" 348 | end) 349 | ``` 350 | 351 | Similar to `get`, except the retrieved field is fed through the given callback and its return value is returned from `getAnd` if the field is non-nil. 352 | 353 | If the field *is* `nil`, then `getAnd` always returns `nil` and the callback is never invoked. This function is useful for transforming a value before using it. 354 | 355 | ### set 356 | `component:set(...fields, value) -> void` 357 | 358 | ```lua 359 | component:set("field", 1) 360 | 361 | component:set("one", "two", "three", Rocs.None) 362 | ``` 363 | 364 | Sets a field on the base scope within component. If you want to set a field to `nil`, you must use `Rocs.None` instead of `nil`. 365 | 366 | ### dispatch 367 | `component:dispatch(eventName: string, ...params) -> void` 368 | 369 | Dispatch an event on this component. Invokes the callback of any listeners which are registered for this event name. 370 | 371 | If there is a method on this component sharing the same name as `eventName`, it is also invoked. 372 | 373 | ### listen 374 | `component:listen(eventName: string, listener: callback) -> listener` 375 | 376 | Adds a listener for this specific event name. Works for custom events which are fired with `dispatch`, and built-ins such as `onAdded` and `onUpdated`. 377 | 378 | Returns the passed listener. 379 | 380 | ### removeListener 381 | `component:removeListener(eventName: string, listener: callback) -> void` 382 | 383 | Removes a previously registered listener from this component. Send the same function that you registered previously as `listener` to unregister it. 384 | 385 | ### data 386 | `data: any` 387 | 388 | The current reduced value from this component. 389 | 390 | ### lastData 391 | `lastData: any` 392 | 393 | The previous reduced value from this component. This is only available during life cycle methods such as `onUpdated`. 394 | 395 | # Built-in Operators 396 | 397 | Rocs provides a number of composable reducer and reducer utilities, so you only have to spend time writing a function for when you need something very specific. 398 | 399 | 400 | 401 | ## Reducers 402 | 403 | Reducer | Description 404 | - | - 405 | `last` | Returns the *last* value of the set. base scope components are always first, so `last` will be any non-base scope (unless the base component is the only value) 406 | `first` | Returns the *first* value of the set. Opposite of `last`. 407 | `truthy` | Returns the first truthy value in the set (or nil if there is none) 408 | `falsy` | Returns the first falsy value in the set (or nil if none) 409 | `add` | Adds the values from the set together (for numbers) 410 | `multiply` | Multiplies the values from the set together (for numbers) 411 | `lowest` | Lowest value (for numbers) 412 | `highest` | Highest value (for numbers) 413 | `concatArray` | Concatenates arrays 414 | `mergeTable` | Merges tables together, with keys from later values overriding earlier. 415 | `collect` | Returns all values this property is set to in each component as an array 416 | 417 | ## Reducer Utilities 418 | 419 | ### structure 420 | 421 | ```lua 422 | reducer = rocs.reducers.structure({ 423 | field = rocs.reducers.add; 424 | }); 425 | ``` 426 | 427 | Reduces a dictionary with a separate reducer for each field. 428 | 429 | Accepts the default reducer to use for omitted properties as a secondary parameter. By default, `Reducers.last` is used for omitted properties. 430 | 431 | ### map 432 | 433 | ```lua 434 | reducer = rocs.reducers.map( 435 | rocs.reducers.structure({ 436 | one = rocs.reducers.multiply; 437 | two = table.concat; 438 | three = function (values) 439 | return values[3] 440 | end 441 | }) 442 | ) 443 | ``` 444 | 445 | Reduces a table, using the same reducer for each key. 446 | 447 | ### concatString 448 | 449 | ```lua 450 | reducer = rocs.reducers.concatString(" - ") 451 | ``` 452 | 453 | Concatenates strings with a given delimiter. 454 | 455 | ### priorityValue 456 | 457 | ```lua 458 | reducer = rocs.reducers.priorityValue(rocs.reducers.concatString(" - ")) 459 | ``` 460 | 461 | Takes values in the form `{ priority: number, value: any }` and produces the `value` with the highest `priority`, reducing any values with equivalent priorities through the given reducer. If the reducer is omitted, `Reducers.last` is implicit. 462 | 463 | ### exactly 464 | 465 | ```lua 466 | reducer = rocs.reducers.exactly("some value") 467 | ``` 468 | 469 | Creates a reducer which always results in the same value. 470 | 471 | ### try 472 | 473 | ```lua 474 | reducer = rocs.reducers.try( 475 | rocs.reducers.truthy, 476 | rocs.reducers.exactly("Default") 477 | ) 478 | ``` 479 | 480 | Tries a set of reducer functions until one of them returns a non-nil value. 481 | 482 | ### compose 483 | 484 | ```lua 485 | reducer = rocs.reducers.compose( 486 | rocs.reducers.structure({ 487 | base = rocs.reducers.last; 488 | add = rocs.reducers.add; 489 | mult = rocs.reducers.multiply; 490 | }), 491 | function (value) 492 | return value.base + value.add * value.mult; 493 | end 494 | ) 495 | ``` 496 | 497 | Composes a set of reducers together such that the return value from each is passed into the next. Uses the return value of the last reducer. 498 | 499 | ### thisOr 500 | 501 | ```lua 502 | reducer = rocs.reducers.thisOr( 503 | rocs.reducers.truthy, 504 | 1 505 | ) 506 | ``` 507 | 508 | Runs the given reducer, and provide a default value in case that reducer returns nil. 509 | 510 | ### lastOr 511 | 512 | ```lua 513 | reducer = rocs.reducers.lastOr(1) 514 | ``` 515 | 516 | Returns the last non-nil value or a default value if there is none. 517 | 518 | ### truthyOr 519 | 520 | ```lua 521 | reducer = rocs.reducers.truthyOr(1) 522 | ``` 523 | 524 | Same as `thisOr`, except the `truthy` reducer is always used. 525 | 526 | ### falsyOr 527 | 528 | Same as `truthyOr`, except for falsy values. 529 | 530 | ## Comparators 531 | 532 | Comparator | Description 533 | ---------- | ----------- 534 | `reference` | Compares two values by reference. 535 | `value` | Compares two objects by value with a deep comparison. 536 | `near` | Compares two numbers and only allows an update if the difference is greater than 0.001. 537 | 538 | ## Comparator Utilities 539 | 540 | ### structure 541 | 542 | ```lua 543 | shouldUpdate = rocs.comparators.structure({ 544 | propertyOne = rocs.comparators.reference; 545 | propertyTwo = rocs.comparators.near; 546 | }) 547 | ``` 548 | 549 | Compares tables with a different function for each field within the table. If any of the properties should update, the entire structure will update. Omitted properties are compared by reference. 550 | 551 | ### within 552 | 553 | ```lua 554 | shouldUpdate = rocs.comparators.within(1) 555 | ``` 556 | 557 | Allows an update only when the change is not within the given epsilon. 558 | 559 | # Rocs API 560 | 561 | ## Types 562 | 563 | ### componentResolvable 564 | 565 | "componentResolvable" refers to any value which can resolve into a component. Specifically, this means either the component name as a string, or the component definition itself as a value. Either will work, but the string form is usually more ergonomic. 566 | 567 | ## `new` 568 | `Rocs.new(name: string = "global"): rocs` 569 | 570 | Creates a new Rocs instance. Use `name` if you are using Rocs within a library; for games the default of `"global"` is fine. 571 | 572 | ## `getEntity` 573 | `rocs:getEntity(instance: any, scope: any): Entity` 574 | 575 | Creates an Entity wrapper for this instance with the given scope. The scope can be any value that discriminates what is enacting this change (such as a menu or a weapon). 576 | 577 | Multiple Entities for the same instance can exist at once, so do not rely on receiving the same object when given the same parameters. 578 | 579 | ## `registerComponent` 580 | `rocs:registerComponent(definition: dictionary): definition` 581 | 582 | See "Registering a component" 583 | 584 | ## `registerComponentsIn` 585 | `rocs:registerComponentsIn(instance: Instance): void` 586 | 587 | Calls registerComponent on the return value from all ModuleScripts inside the given instance. 588 | 589 | ## `registerLifecycleHook` 590 | `rocs:registerLifecycleHook(lifecycle: string, hook: callback): void` 591 | 592 | Registers a callback which is called whenever the given life cycle method is invoked on any component. Callback is called with `(componentAggregate, stageName)`. 593 | 594 | - `initialize` 595 | - `onAdded` 596 | - `onUpdated` 597 | - `onParentUpdated` 598 | - `onRemoved` 599 | - `destroy` 600 | - `global` 601 | 602 | ## `registerComponentHook` 603 | `rocs:registerComponentHook(component: componentResolvable, lifecycle: string, hook: callback): hook` 604 | 605 | Same as `registerLifecycleHook`, except only for a single component type. 606 | 607 | ## `unregisterComponentHook` 608 | `rocs:registerComponentHook(component: componentResolvable, lifecycle: string, hook: callback): void` 609 | 610 | Unregisters this component hook. 611 | 612 | ## `registerEntityComponentHook` 613 | `rocs:registerEntityComponentHook(entity: any, component: componentResolvable, lifecycle: string, hook: callback): hook` 614 | 615 | Registers a component hook for specific components on a specific entity. If the entity is a Roblox Instance, then this will be disconnected when the Instance is no longer a child of the DataModel. 616 | 617 | ## `getComponents` 618 | `rocs:getComponents(component: componentResolvable): array` 619 | 620 | Returns an array of all Aggregates of the given type in the entire Rocs instance. 621 | 622 | 623 | 624 | # Entities API 625 | 626 | ```lua 627 | local entity = rocs:getEntity(workspace) 628 | ``` 629 | 630 | ## `addComponent` 631 | `entity:addComponent(component: componentResolvable, data: any, metaComponents: dict?): Aggregate, boolean` 632 | 633 | Adds a new component to this entity under the entity's scope. 634 | 635 | If `data` is nil then this is equivalent to `removeComponent`. 636 | 637 | Returns the associated Aggregate and a boolean indicating whether or not this component was new on this entity. 638 | 639 | ## `removeComponent` 640 | `entity:removeComponent(component: componentResolvable): void` 641 | 642 | Removes the given component from this entity. 643 | 644 | ## `addBaseComponent` 645 | `entity:addBaseComponent(component: componentResolvable, data: any, metaComponents: dict?): Aggregate, boolean` 646 | 647 | Similar to `addComponent`, but with the special base scope. 648 | 649 | ## `removeBaseComponent` 650 | `entity:removeBaseComponent(component: componentResolvable): void` 651 | 652 | Similar to `removeBaseComponent`, but with the special base scope. 653 | 654 | ## `getComponent` 655 | `entity:getComponent(component: componentResolvable): Aggregate?` 656 | 657 | Returns the Aggregate for the given component from this entity if it exists. 658 | 659 | ## `getAllComponents` 660 | `entity:getAllComponents(): array` 661 | 662 | Returns all Aggregates on this entity. 663 | 664 | ## `removeAllComponents` 665 | `entity:removeAllComponents(): void` 666 | 667 | Removes all Aggregates on this entity. 668 | 669 | ## `getScope` 670 | `entity:getScope(scope: any): Entity` 671 | 672 | Returns a new Entity linked to the same instance as this entity but with a new scope. 673 | 674 | # Built-in Middleware 675 | 676 | - Mention how to use middleware 677 | 678 | ## Replication 679 | 680 | - Todo 681 | 682 | ## Chaining 683 | 684 | ## Tags 685 | 686 | ## Selectors 687 | 688 | - Todo 689 | 690 | # Authors 691 | 692 | Rocs was designed and created by [evaera](https://eryn.io) and [buildthomas](https://github.com/buildthomas/). 693 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | -- luacheck: ignore 2 | 3 | local empty = {} 4 | local read_write = { read_only = false } 5 | local read_write_class = { read_only = false, other_fields = true } 6 | local read_only = { read_only = true } 7 | 8 | local function def_fields(field_list) 9 | local fields = {} 10 | 11 | for _, field in ipairs(field_list) do 12 | fields[field] = empty 13 | end 14 | 15 | return { fields = fields } 16 | end 17 | 18 | local enum = def_fields({"Value", "Name"}) 19 | 20 | local function def_enum(field_list) 21 | local fields = {} 22 | 23 | for _, field in ipairs(field_list) do 24 | fields[field] = enum 25 | end 26 | 27 | fields["GetEnumItems"] = read_only 28 | 29 | return { fields = fields } 30 | end 31 | 32 | stds.roblox = { 33 | globals = { 34 | script = { 35 | other_fields = true, 36 | fields = { 37 | Source = read_write; 38 | GetHash = read_write; 39 | Disabled = read_write; 40 | LinkedSource = read_write; 41 | CurrentEditor = read_write_class; 42 | Archivable = read_write; 43 | ClassName = read_only; 44 | DataCost = read_only; 45 | Name = read_write; 46 | Parent = read_write_class; 47 | RobloxLocked = read_write; 48 | ClearAllChildren = read_write; 49 | Clone = read_write; 50 | Destroy = read_write; 51 | FindFirstAncestor = read_write; 52 | FindFirstAncestorOfClass = read_write; 53 | FindFirstAncestorWhichIsA = read_write; 54 | FindFirstChild = read_write; 55 | FindFirstChildOfClass = read_write; 56 | FindFirstChildWhichIsA = read_write; 57 | GetChildren = read_write; 58 | GetDebugId = read_write; 59 | GetDescendants = read_write; 60 | GetFullName = read_write; 61 | GetPropertyChangedSignal = read_write; 62 | IsA = read_write; 63 | IsAncestorOf = read_write; 64 | IsDescendantOf = read_write; 65 | WaitForChild = read_write; 66 | AncestryChanged = read_write; 67 | Changed = read_write; 68 | ChildAdded = read_write; 69 | ChildRemoved = read_write; 70 | DescendantAdded = read_write; 71 | DescendantRemoving = read_write; 72 | } 73 | }, 74 | game = { 75 | other_fields = true, 76 | fields = { 77 | CreatorId = read_only; 78 | CreatorType = read_only; 79 | GameId = read_only; 80 | GearGenreSetting = read_only; 81 | Genre = read_only; 82 | IsSFFlagsLoaded = read_only; 83 | JobId = read_only; 84 | PlaceId = read_only; 85 | PlaceVersion = read_only; 86 | PrivateServerId = read_only; 87 | PrivateServerOwnerId = read_only; 88 | Workspace = read_only; 89 | BindToClose = read_write; 90 | GetJobIntervalPeakFraction = read_write; 91 | GetJobTimePeakFraction = read_write; 92 | GetJobsExtendedStats = read_write; 93 | GetJobsInfo = read_write; 94 | GetObjects = read_write; 95 | IsGearTypeAllowed = read_write; 96 | IsLoaded = read_write; 97 | Load = read_write; 98 | OpenScreenshotsFolder = read_write; 99 | OpenVideosFolder = read_write; 100 | ReportInGoogleAnalytics = read_write; 101 | SetPlaceId = read_write; 102 | SetUniverseId = read_write; 103 | Shutdown = read_write; 104 | HttpGetAsync = read_write; 105 | HttpPostAsync = read_write; 106 | GraphicsQualityChangeRequest = read_write; 107 | Loaded = read_write; 108 | ScreenshotReady = read_write; 109 | FindService = read_write; 110 | GetService = read_write; 111 | Close = read_write; 112 | CloseLate = read_write; 113 | ServiceAdded = read_write; 114 | ServiceRemoving = read_write; 115 | Archivable = read_write; 116 | ClassName = read_only; 117 | DataCost = read_only; 118 | Name = read_write; 119 | Parent = read_write_class; 120 | RobloxLocked = read_write; 121 | ClearAllChildren = read_write; 122 | Clone = read_write; 123 | Destroy = read_write; 124 | FindFirstAncestor = read_write; 125 | FindFirstAncestorOfClass = read_write; 126 | FindFirstAncestorWhichIsA = read_write; 127 | FindFirstChild = read_write; 128 | FindFirstChildOfClass = read_write; 129 | FindFirstChildWhichIsA = read_write; 130 | GetChildren = read_write; 131 | GetDebugId = read_write; 132 | GetDescendants = read_write; 133 | GetFullName = read_write; 134 | GetPropertyChangedSignal = read_write; 135 | IsA = read_write; 136 | IsAncestorOf = read_write; 137 | IsDescendantOf = read_write; 138 | WaitForChild = read_write; 139 | AncestryChanged = read_write; 140 | Changed = read_write; 141 | ChildAdded = read_write; 142 | ChildRemoved = read_write; 143 | DescendantAdded = read_write; 144 | DescendantRemoving = read_write; 145 | } 146 | }, 147 | workspace = { 148 | other_fields = true, 149 | fields = { 150 | AllowThirdPartySales = read_write; 151 | AutoJointsMode = read_write; 152 | CurrentCamera = read_write_class; 153 | DistributedGameTime = read_write; 154 | FallenPartsDestroyHeight = read_write; 155 | FilteringEnabled = read_write; 156 | Gravity = read_write; 157 | PGSPhysicsSolverEnabled = read_write; 158 | StreamingEnabled = read_write; 159 | StreamingMinRadius = read_write; 160 | StreamingTargetRadius = read_write; 161 | TemporaryLegacyPhysicsSolverOverride = read_write; 162 | Terrain = read_only; 163 | BreakJoints = read_write; 164 | ExperimentalSolverIsEnabled = read_write; 165 | FindPartOnRay = read_write; 166 | FindPartOnRayWithIgnoreList = read_write; 167 | FindPartOnRayWithWhitelist = read_write; 168 | FindPartsInRegion3 = read_write; 169 | FindPartsInRegion3WithIgnoreList = read_write; 170 | FindPartsInRegion3WithWhiteList = read_write; 171 | GetNumAwakeParts = read_write; 172 | GetPhysicsAnalyzerBreakOnIssue = read_write; 173 | GetPhysicsAnalyzerIssue = read_write; 174 | GetPhysicsThrottling = read_write; 175 | GetRealPhysicsFPS = read_write; 176 | IsRegion3Empty = read_write; 177 | IsRegion3EmptyWithIgnoreList = read_write; 178 | JoinToOutsiders = read_write; 179 | MakeJoints = read_write; 180 | PGSIsEnabled = read_write; 181 | SetPhysicsAnalyzerBreakOnIssue = read_write; 182 | SetPhysicsThrottleEnabled = read_write; 183 | UnjoinFromOutsiders = read_write; 184 | ZoomToExtents = read_write; 185 | PhysicsAnalyzerIssuesFound = read_write; 186 | PrimaryPart = read_write_class; 187 | BreakJoints = read_write; 188 | GetBoundingBox = read_write; 189 | GetExtentsSize = read_write; 190 | GetPrimaryPartCFrame = read_write; 191 | MakeJoints = read_write; 192 | MoveTo = read_write; 193 | SetPrimaryPartCFrame = read_write; 194 | TranslateBy = read_write; 195 | Archivable = read_write; 196 | ClassName = read_only; 197 | DataCost = read_only; 198 | Name = read_write; 199 | Parent = read_write_class; 200 | RobloxLocked = read_write; 201 | ClearAllChildren = read_write; 202 | Clone = read_write; 203 | Destroy = read_write; 204 | FindFirstAncestor = read_write; 205 | FindFirstAncestorOfClass = read_write; 206 | FindFirstAncestorWhichIsA = read_write; 207 | FindFirstChild = read_write; 208 | FindFirstChildOfClass = read_write; 209 | FindFirstChildWhichIsA = read_write; 210 | GetChildren = read_write; 211 | GetDebugId = read_write; 212 | GetDescendants = read_write; 213 | GetFullName = read_write; 214 | GetPropertyChangedSignal = read_write; 215 | IsA = read_write; 216 | IsAncestorOf = read_write; 217 | IsDescendantOf = read_write; 218 | WaitForChild = read_write; 219 | AncestryChanged = read_write; 220 | Changed = read_write; 221 | ChildAdded = read_write; 222 | ChildRemoved = read_write; 223 | DescendantAdded = read_write; 224 | DescendantRemoving = read_write; 225 | } 226 | }, 227 | }, 228 | read_globals = { 229 | -- Methods 230 | delay = empty; 231 | settings = empty; 232 | spawn = empty; 233 | tick = empty; 234 | time = empty; 235 | typeof = empty; 236 | version = empty; 237 | wait = empty; 238 | warn = empty; 239 | 240 | -- Libraries 241 | math = def_fields({"abs", "acos", "asin", "atan", "atan2", "ceil", "clamp", "cos", "cosh", 242 | "deg", "exp", "floor", "fmod", "frexp", "ldexp", "log", "log10", "max", "min", "modf", 243 | "noise", "pow", "rad", "random", "randomseed", "sign", "sin", "sinh", "sqrt", "tan", 244 | "tanh", "huge", "pi"}), 245 | 246 | debug = def_fields({"traceback", "profilebegin", "profileend"}), 247 | 248 | utf8 = def_fields({"char", "codes", "codepoint", "len", "offset", "graphemes", 249 | "nfcnormalize", "nfdnormalize", "charpattern"}), 250 | 251 | -- Types 252 | Axes = def_fields({"new"}), 253 | 254 | BrickColor = def_fields({"new", "palette", "random", "White", "Gray", "DarkGray", "Black", 255 | "Red", "Yellow", "Green", "Blue"}), 256 | 257 | CFrame = def_fields({"new", "fromEulerAnglesXYZ", "Angles", "fromOrientation", 258 | "fromAxisAngle", "fromMatrix"}), 259 | 260 | Color3 = def_fields({"new", "fromRGB", "fromHSV"}), 261 | 262 | ColorSequence = def_fields({"new"}), 263 | 264 | ColorSequenceKeypoint = def_fields({"new"}), 265 | 266 | DockWidgetPluginGuiInfo = def_fields({"new"}), 267 | 268 | Enums = def_fields({"GetEnums"}), 269 | 270 | Faces = def_fields({"new"}), 271 | 272 | Instance = def_fields({"new"}), 273 | 274 | NumberRange = def_fields({"new"}), 275 | 276 | NumberSequence = def_fields({"new"}), 277 | 278 | NumberSequenceKeypoint = def_fields({"new"}), 279 | 280 | PhysicalProperties = def_fields({"new"}), 281 | 282 | Random = def_fields({"new"}), 283 | 284 | Ray = def_fields({"new"}), 285 | 286 | Rect = def_fields({"new"}), 287 | 288 | Region3 = def_fields({"new"}), 289 | 290 | Region3int16 = def_fields({"new"}), 291 | 292 | TweenInfo = def_fields({"new"}), 293 | 294 | UDim = def_fields({"new"}), 295 | 296 | UDim2 = def_fields({"new"}), 297 | 298 | Vector2 = def_fields({"new"}), 299 | 300 | Vector2int16 = def_fields({"new"}), 301 | 302 | Vector3 = def_fields({"new", "FromNormalId", "FromAxis"}), 303 | 304 | Vector3int16 = def_fields({"new"}), 305 | 306 | -- Enums 307 | Enum = { 308 | readonly = true, 309 | fields = { 310 | ActionType = def_enum({"Nothing", "Pause", "Lose", "Draw", "Win"}), 311 | ActuatorRelativeTo = def_enum({"Attachment0", "Attachment1", "World"}), 312 | ActuatorType = def_enum({"None", "Motor", "Servo"}), 313 | AnimationPriority = def_enum({"Idle", "Movement", "Action", "Core"}), 314 | AppShellActionType = def_enum({"None", "OpenApp", "TapChatTab", 315 | "TapConversationEntry", "TapAvatarTab", "ReadConversation", "TapGamePageTab", 316 | "TapHomePageTab", "GamePageLoaded", "HomePageLoaded", "AvatarEditorPageLoaded"}), 317 | AspectType = def_enum({"FitWithinMaxSize", "ScaleWithParentSize"}), 318 | AssetType = def_enum({"Image", "TeeShirt", "Audio", "Mesh", "Lua", "Hat", "Place", 319 | "Model", "Shirt", "Pants", "Decal", "Head", "Face", "Gear", "Badge", 320 | "Animation", "Torso", "RightArm", "LeftArm", "LeftLeg", "RightLeg", "Package", 321 | "GamePass", "Plugin", "MeshPart", "HairAccessory", "FaceAccessory", 322 | "NeckAccessory", "ShoulderAccessory", "FrontAccessory", "BackAccessory", 323 | "WaistAccessory", "ClimbAnimation", "DeathAnimation", "FallAnimation", 324 | "IdleAnimation", "JumpAnimation", "RunAnimation", "SwimAnimation", 325 | "WalkAnimation", "PoseAnimation", "EarAccessory", "EyeAccessory"}), 326 | AutoJointsMode = def_enum({"Default", "Explicit", "LegacyImplicit"}), 327 | AvatarContextMenuOption = def_enum({"Friend", "Chat", "Emote"}), 328 | AvatarJointPositionType = def_enum({"Fixed", "ArtistIntent"}), 329 | Axis = def_enum({"X", "Y", "Z"}), 330 | BinType = def_enum({"Script", "GameTool", "Grab", "Clone", "Hammer"}), 331 | BodyPart = def_enum({"Head", "Torso", "LeftArm", "RightArm", "LeftLeg", "RightLeg"}), 332 | BodyPartR15 = def_enum({"Head", "UpperTorso", "LowerTorso", "LeftFoot", 333 | "LeftLowerLeg", "LeftUpperLeg", "RightFoot", "RightLowerLeg", "RightUpperLeg", 334 | "LeftHand", "LeftLowerArm", "LeftUpperArm", "RightHand", "RightLowerArm", 335 | "RightUpperArm", "RootPart", "Unknown"}), 336 | Button = def_enum({"Jump", "Dismount"}), 337 | ButtonStyle = def_enum({"Custom", "RobloxButtonDefault", "RobloxButton", 338 | "RobloxRoundButton", "RobloxRoundDefaultButton", "RobloxRoundDropdownButton"}), 339 | CameraMode = def_enum({"Classic", "LockFirstPerson"}), 340 | CameraPanMode = def_enum({"Classic", "EdgeBump"}), 341 | CameraType = def_enum({"Fixed", "Watch", "Attach", "Track", "Follow", "Custom", 342 | "Scriptable", "Orbital"}), 343 | CellBlock = def_enum({"Solid", "VerticalWedge", "CornerWedge", 344 | "InverseCornerWedge", "HorizontalWedge"}), 345 | CellMaterial = def_enum({"Empty", "Grass", "Sand", "Brick", "Granite", "Asphalt", 346 | "Iron", "Aluminum", "Gold", "WoodPlank", "WoodLog", "Gravel", "CinderBlock", 347 | "MossyStone", "Cement", "RedPlastic", "BluePlastic", "Water"}), 348 | CellOrientation = def_enum({"NegZ", "X", "Z", "NegX"}), 349 | CenterDialogType = def_enum({"UnsolicitedDialog", "PlayerInitiatedDialog", 350 | "ModalDialog", "QuitDialog"}), 351 | ChatCallbackType = def_enum({"OnCreatingChatWindow", "OnClientSendingMessage", 352 | "OnClientFormattingMessage", "OnServerReceivingMessage"}), 353 | ChatColor = def_enum({"Blue", "Green", "Red", "White"}), 354 | ChatMode = def_enum({"Menu", "TextAndMenu"}), 355 | ChatPrivacyMode = def_enum({"AllUsers", "NoOne", "Friends"}), 356 | ChatStyle = def_enum({"Classic", "Bubble", "ClassicAndBubble"}), 357 | CollisionFidelity = def_enum({"Default", "Hull", "Box"}), 358 | ComputerCameraMovementMode = def_enum({"Default", "Follow", "Classic", "Orbital"}), 359 | ComputerMovementMode = def_enum({"Default", "KeyboardMouse", "ClickToMove"}), 360 | ConnectionError = def_enum({"OK", "DisconnectErrors", "DisconnectBadhash", 361 | "DisconnectSecurityKeyMismatch", "DisconnectNewSecurityKeyMismatch", 362 | "DisconnectProtocolMismatch", "DisconnectReceivePacketError", 363 | "DisconnectReceivePacketStreamError", "DisconnectSendPacketError", 364 | "DisconnectIllegalTeleport", "DisconnectDuplicatePlayer", 365 | "DisconnectDuplicateTicket", "DisconnectTimeout", "DisconnectLuaKick", 366 | "DisconnectOnRemoteSysStats", "DisconnectHashTimeout", 367 | "DisconnectCloudEditKick", "DisconnectPlayerless", "DisconnectEvicted", 368 | "DisconnectDevMaintenance", "DisconnectRobloxMaintenance", "DisconnectRejoin", 369 | "DisconnectConnectionLost", "DisconnectIdle", "DisconnectRaknetErrors", 370 | "DisconnectWrongVersion", "PlacelaunchErrors", "PlacelaunchDisabled", 371 | "PlacelaunchError", "PlacelaunchGameEnded", "PlacelaunchGameFull", 372 | "PlacelaunchUserLeft", "PlacelaunchRestricted", "PlacelaunchUnauthorized", 373 | "PlacelaunchFlooded", "PlacelaunchHashExpired", "PlacelaunchHashException", 374 | "PlacelaunchPartyCannotFit", "PlacelaunchHttpError", 375 | "PlacelaunchCustomMessage", "PlacelaunchOtherError", "TeleportErrors", 376 | "TeleportFailure", "TeleportGameNotFound", "TeleportGameEnded", 377 | "TeleportGameFull", "TeleportUnauthorized", "TeleportFlooded", 378 | "TeleportIsTeleporting"}), 379 | ConnectionState = def_enum({"Connected", "Disconnected"}), 380 | ContextActionPriority = def_enum({"Low", "Medium", "Default", "High"}), 381 | ContextActionResult = def_enum({"Pass", "Sink"}), 382 | ControlMode = def_enum({"MouseLockSwitch", "Classic"}), 383 | CoreGuiType = def_enum({"PlayerList", "Health", "Backpack", "Chat", "All"}), 384 | CreatorType = def_enum({"User", "Group"}), 385 | CurrencyType = def_enum({"Default", "Robux", "Tix"}), 386 | CustomCameraMode = def_enum({"Default", "Follow", "Classic"}), 387 | DEPRECATED_DebuggerDataModelPreference = def_enum({"Server", "Client"}), 388 | DataStoreRequestType = def_enum({"GetAsync", "SetIncrementAsync", "UpdateAsync", 389 | "GetSortedAsync", "SetIncrementSortedAsync", "OnUpdate"}), 390 | DevCameraOcclusionMode = def_enum({"Zoom", "Invisicam"}), 391 | DevComputerCameraMovementMode = def_enum({"UserChoice", "Classic", "Follow", 392 | "Orbital"}), 393 | DevComputerMovementMode = def_enum({"UserChoice", "KeyboardMouse", "ClickToMove", 394 | "Scriptable"}), 395 | DevTouchCameraMovementMode = def_enum({"UserChoice", "Classic", "Follow", 396 | "Orbital"}), 397 | DevTouchMovementMode = def_enum({"UserChoice", "Thumbstick", "DPad", "Thumbpad", 398 | "ClickToMove", "Scriptable", "DynamicThumbstick"}), 399 | DeveloperMemoryTag = def_enum({"Internal", "HttpCache", "Instances", "Signals", 400 | "LuaHeap", "Script", "PhysicsCollision", "PhysicsParts", "GraphicsSolidModels", 401 | "GraphicsMeshParts", "GraphicsParticles", "GraphicsParts", 402 | "GraphicsSpatialHash", "GraphicsTerrain", "GraphicsTexture", 403 | "GraphicsTextureCharacter", "Sounds", "StreamingSounds", "TerrainVoxels", 404 | "Gui", "Animation", "Navigation"}), 405 | DialogBehaviorType = def_enum({"SinglePlayer", "MultiplePlayers"}), 406 | DialogPurpose = def_enum({"Quest", "Help", "Shop"}), 407 | DialogTone = def_enum({"Neutral", "Friendly", "Enemy"}), 408 | DominantAxis = def_enum({"Width", "Height"}), 409 | EasingDirection = def_enum({"In", "Out", "InOut"}), 410 | EasingStyle = def_enum({"Linear", "Sine", "Back", "Quad", "Quart", "Quint", 411 | "Bounce", "Elastic"}), 412 | ElasticBehavior = def_enum({"WhenScrollable", "Always", "Never"}), 413 | EnviromentalPhysicsThrottle = def_enum({"DefaultAuto", "Disabled", "Always", 414 | "Skip2", "Skip4", "Skip8", "Skip16"}), 415 | ErrorReporting = def_enum({"DontReport", "Prompt", "Report"}), 416 | ExplosionType = def_enum({"NoCraters", "Craters", "CratersAndDebris"}), 417 | FillDirection = def_enum({"Horizontal", "Vertical"}), 418 | FilterResult = def_enum({"Rejected", "Accepted"}), 419 | Font = def_enum({"Legacy", "Arial", "ArialBold", "SourceSans", "SourceSansBold", 420 | "SourceSansSemibold", "SourceSansLight", "SourceSansItalic", "Bodoni", 421 | "Garamond", "Cartoon", "Code", "Highway", "SciFi", "Arcade", "Fantasy", 422 | "Antique", "Gotham", "GothamSemibold", "GothamBold", "GothamBlack"}), 423 | FontSize = def_enum({"Size8", "Size9", "Size10", "Size11", "Size12", "Size14", 424 | "Size18", "Size24", "Size36", "Size48", "Size28", "Size32", "Size42", "Size60", 425 | "Size96"}), 426 | FormFactor = def_enum({"Symmetric", "Brick", "Plate", "Custom"}), 427 | FrameStyle = def_enum({"Custom", "ChatBlue", "RobloxSquare", "RobloxRound", 428 | "ChatGreen", "ChatRed", "DropShadow"}), 429 | FramerateManagerMode = def_enum({"Automatic", "On", "Off"}), 430 | FriendRequestEvent = def_enum({"Issue", "Revoke", "Accept", "Deny"}), 431 | FriendStatus = def_enum({"Unknown", "NotFriend", "Friend", "FriendRequestSent", 432 | "FriendRequestReceived"}), 433 | FunctionalTestResult = def_enum({"Passed", "Warning", "Error"}), 434 | GameAvatarType = def_enum({"R6", "R15", "PlayerChoice"}), 435 | GearGenreSetting = def_enum({"AllGenres", "MatchingGenreOnly"}), 436 | GearType = def_enum({"MeleeWeapons", "RangedWeapons", "Explosives", "PowerUps", 437 | "NavigationEnhancers", "MusicalInstruments", "SocialItems", "BuildingTools", 438 | "Transport"}), 439 | Genre = def_enum({"All", "TownAndCity", "Fantasy", "SciFi", "Ninja", "Scary", 440 | "Pirate", "Adventure", "Sports", "Funny", "WildWest", "War", "SkatePark", 441 | "Tutorial"}), 442 | GraphicsMode = def_enum({"Automatic", "Direct3D9", "Direct3D11", "OpenGL", "Metal", 443 | "Vulkan", "NoGraphics"}), 444 | HandlesStyle = def_enum({"Resize", "Movement"}), 445 | HorizontalAlignment = def_enum({"Center", "Left", "Right"}), 446 | HoverAnimateSpeed = def_enum({"VerySlow", "Slow", "Medium", "Fast", "VeryFast"}), 447 | HttpCachePolicy = def_enum({"None", "Full", "DataOnly", "Default", 448 | "InternalRedirectRefresh"}), 449 | HttpContentType = def_enum({"ApplicationJson", "ApplicationXml", 450 | "ApplicationUrlEncoded", "TextPlain", "TextXml"}), 451 | HttpError = def_enum({"OK", "InvalidUrl", "DnsResolve", "ConnectFail", 452 | "OutOfMemory", "TimedOut", "TooManyRedirects", "InvalidRedirect", "NetFail", 453 | "Aborted", "SslConnectFail", "Unknown"}), 454 | HttpRequestType = def_enum({"Default", "MarketplaceService", "Players", "Chat", 455 | "Avatar", "Analytics"}), 456 | HumanoidDisplayDistanceType = def_enum({"Viewer", "Subject", "None"}), 457 | HumanoidHealthDisplayType = def_enum({"DisplayWhenDamaged", "AlwaysOn", 458 | "AlwaysOff"}), 459 | HumanoidRigType = def_enum({"R6", "R15"}), 460 | HumanoidStateType = def_enum({"FallingDown", "Running", "RunningNoPhysics", 461 | "Climbing", "StrafingNoPhysics", "Ragdoll", "GettingUp", "Jumping", "Landed", 462 | "Flying", "Freefall", "Seated", "PlatformStanding", "Dead", "Swimming", 463 | "Physics", "None"}), 464 | InOut = def_enum({"Edge", "Inset", "Center"}), 465 | InfoType = def_enum({"Asset", "Product", "GamePass"}), 466 | InitialDockState = def_enum({"Top", "Bottom", "Left", "Right", "Float"}), 467 | InputType = def_enum({"NoInput", "Constant", "Sin"}), 468 | JointCreationMode = def_enum({"All", "Surface", "None"}), 469 | JointType = def_enum({"None", "Rotate", "RotateP", "RotateV", "Glue", "Weld", 470 | "Snap"}), 471 | KeyCode = def_enum({"Unknown", "Backspace", "Tab", "Clear", "Return", "Pause", 472 | "Escape", "Space", "QuotedDouble", "Hash", "Dollar", "Percent", "Ampersand", 473 | "Quote", "LeftParenthesis", "RightParenthesis", "Asterisk", "Plus", "Comma", 474 | "Minus", "Period", "Slash", "Zero", "One", "Two", "Three", "Four", "Five", 475 | "Six", "Seven", "Eight", "Nine", "Colon", "Semicolon", "LessThan", "Equals", 476 | "GreaterThan", "Question", "At", "LeftBracket", "BackSlash", "RightBracket", 477 | "Caret", "Underscore", "Backquote", "A", "B", "C", "D", "E", "F", "G", "H", 478 | "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", 479 | "Y", "Z", "LeftCurly", "Pipe", "RightCurly", "Tilde", "Delete", "KeypadZero", 480 | "KeypadOne", "KeypadTwo", "KeypadThree", "KeypadFour", "KeypadFive", 481 | "KeypadSix", "KeypadSeven", "KeypadEight", "KeypadNine", "KeypadPeriod", 482 | "KeypadDivide", "KeypadMultiply", "KeypadMinus", "KeypadPlus", "KeypadEnter", 483 | "KeypadEquals", "Up", "Down", "Right", "Left", "Insert", "Home", "End", 484 | "PageUp", "PageDown", "LeftShift", "RightShift", "LeftMeta", "RightMeta", 485 | "LeftAlt", "RightAlt", "LeftControl", "RightControl", "CapsLock", "NumLock", 486 | "ScrollLock", "LeftSuper", "RightSuper", "Mode", "Compose", "Help", "Print", 487 | "SysReq", "Break", "Menu", "Power", "Euro", "Undo", "F1", "F2", "F3", "F4", 488 | "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14", "F15", 489 | "World0", "World1", "World2", "World3", "World4", "World5", "World6", "World7", 490 | "World8", "World9", "World10", "World11", "World12", "World13", "World14", 491 | "World15", "World16", "World17", "World18", "World19", "World20", "World21", 492 | "World22", "World23", "World24", "World25", "World26", "World27", "World28", 493 | "World29", "World30", "World31", "World32", "World33", "World34", "World35", 494 | "World36", "World37", "World38", "World39", "World40", "World41", "World42", 495 | "World43", "World44", "World45", "World46", "World47", "World48", "World49", 496 | "World50", "World51", "World52", "World53", "World54", "World55", "World56", 497 | "World57", "World58", "World59", "World60", "World61", "World62", "World63", 498 | "World64", "World65", "World66", "World67", "World68", "World69", "World70", 499 | "World71", "World72", "World73", "World74", "World75", "World76", "World77", 500 | "World78", "World79", "World80", "World81", "World82", "World83", "World84", 501 | "World85", "World86", "World87", "World88", "World89", "World90", "World91", 502 | "World92", "World93", "World94", "World95", "ButtonX", "ButtonY", "ButtonA", 503 | "ButtonB", "ButtonR1", "ButtonL1", "ButtonR2", "ButtonL2", "ButtonR3", 504 | "ButtonL3", "ButtonStart", "ButtonSelect", "DPadLeft", "DPadRight", "DPadUp", 505 | "DPadDown", "Thumbstick1", "Thumbstick2"}), 506 | KeywordFilterType = def_enum({"Include", "Exclude"}), 507 | Language = def_enum({"Default"}), 508 | LeftRight = def_enum({"Left", "Center", "Right"}), 509 | LevelOfDetailSetting = def_enum({"High", "Medium", "Low"}), 510 | Limb = def_enum({"Head", "Torso", "LeftArm", "RightArm", "LeftLeg", "RightLeg", 511 | "Unknown"}), 512 | ListDisplayMode = def_enum({"Horizontal", "Vertical"}), 513 | ListenerType = def_enum({"Camera", "CFrame", "ObjectPosition", "ObjectCFrame"}), 514 | Material = def_enum({"Plastic", "Wood", "Slate", "Concrete", "CorrodedMetal", 515 | "DiamondPlate", "Foil", "Grass", "Ice", "Marble", "Granite", "Brick", "Pebble", 516 | "Sand", "Fabric", "SmoothPlastic", "Metal", "WoodPlanks", "Cobblestone", "Air", 517 | "Water", "Rock", "Glacier", "Snow", "Sandstone", "Mud", "Basalt", "Ground", 518 | "CrackedLava", "Neon", "Glass", "Asphalt", "LeafyGrass", "Salt", "Limestone", 519 | "Pavement"}), 520 | MembershipType = def_enum({"None", "BuildersClub", "TurboBuildersClub", 521 | "OutrageousBuildersClub"}), 522 | MeshType = def_enum({"Head", "Torso", "Wedge", "Prism", "Pyramid", "ParallelRamp", 523 | "RightAngleRamp", "CornerWedge", "Brick", "Sphere", "Cylinder", "FileMesh"}), 524 | MessageType = def_enum({"MessageOutput", "MessageInfo", "MessageWarning", 525 | "MessageError"}), 526 | MouseBehavior = def_enum({"Default", "LockCenter", "LockCurrentPosition"}), 527 | MoveState = def_enum({"Stopped", "Coasting", "Pushing", "Stopping", "AirFree"}), 528 | NameOcclusion = def_enum({"OccludeAll", "EnemyOcclusion", "NoOcclusion"}), 529 | NetworkOwnership = def_enum({"Automatic", "Manual", "OnContact"}), 530 | NormalId = def_enum({"Top", "Bottom", "Back", "Front", "Right", "Left"}), 531 | OutputLayoutMode = def_enum({"Horizontal", "Vertical"}), 532 | OverrideMouseIconBehavior = def_enum({"None", "ForceShow", "ForceHide"}), 533 | PacketPriority = def_enum({"IMMEDIATE_PRIORITY", "HIGH_PRIORITY", 534 | "MEDIUM_PRIORITY", "LOW_PRIORITY"}), 535 | PartType = def_enum({"Ball", "Block", "Cylinder"}), 536 | PathStatus = def_enum({"Success", "ClosestNoPath", "ClosestOutOfRange", 537 | "FailStartNotEmpty", "FailFinishNotEmpty", "NoPath"}), 538 | PathWaypointAction = def_enum({"Walk", "Jump"}), 539 | PermissionLevelShown = def_enum({"Game", "RobloxGame", "RobloxScript", "Studio", 540 | "Roblox"}), 541 | Platform = def_enum({"Windows", "OSX", "IOS", "Android", "XBoxOne", "PS4", "PS3", 542 | "XBox360", "WiiU", "NX", "Ouya", "AndroidTV", "Chromecast", "Linux", "SteamOS", 543 | "WebOS", "DOS", "BeOS", "UWP", "None"}), 544 | PlaybackState = def_enum({"Begin", "Delayed", "Playing", "Paused", "Completed", 545 | "Cancelled"}), 546 | PlayerActions = def_enum({"CharacterForward", "CharacterBackward", "CharacterLeft", 547 | "CharacterRight", "CharacterJump"}), 548 | PlayerChatType = def_enum({"All", "Team", "Whisper"}), 549 | PoseEasingDirection = def_enum({"Out", "InOut", "In"}), 550 | PoseEasingStyle = def_enum({"Linear", "Constant", "Elastic", "Cubic", "Bounce"}), 551 | PrivilegeType = def_enum({"Owner", "Admin", "Member", "Visitor", "Banned"}), 552 | ProductPurchaseDecision = def_enum({"NotProcessedYet", "PurchaseGranted"}), 553 | QualityLevel = def_enum({"Automatic", "Level01", "Level02", "Level03", "Level04", 554 | "Level05", "Level06", "Level07", "Level08", "Level09", "Level10", "Level11", 555 | "Level12", "Level13", "Level14", "Level15", "Level16", "Level17", "Level18", 556 | "Level19", "Level20", "Level21"}), 557 | R15CollisionType = def_enum({"OuterBox", "InnerBox"}), 558 | RenderFidelity = def_enum({"Automatic", "Precise"}), 559 | RenderPriority = def_enum({"First", "Input", "Camera", "Character", "Last"}), 560 | RenderingTestComparisonMethod = def_enum({"psnr", "diff"}), 561 | ReverbType = def_enum({"NoReverb", "GenericReverb", "PaddedCell", "Room", 562 | "Bathroom", "LivingRoom", "StoneRoom", "Auditorium", "ConcertHall", "Cave", 563 | "Arena", "Hangar", "CarpettedHallway", "Hallway", "StoneCorridor", "Alley", 564 | "Forest", "City", "Mountains", "Quarry", "Plain", "ParkingLot", "SewerPipe", 565 | "UnderWater"}), 566 | RibbonTool = def_enum({"Select", "Scale", "Rotate", "Move", "Transform", 567 | "ColorPicker", "MaterialPicker", "Group", "Ungroup", "None"}), 568 | RollOffMode = def_enum({"Inverse", "Linear", "InverseTapered", "LinearSquare"}), 569 | RotationType = def_enum({"MovementRelative", "CameraRelative"}), 570 | RuntimeUndoBehavior = def_enum({"Aggregate", "Snapshot", "Hybrid"}), 571 | SaveFilter = def_enum({"SaveAll", "SaveWorld", "SaveGame"}), 572 | SavedQualitySetting = def_enum({"Automatic", "QualityLevel1", "QualityLevel2", 573 | "QualityLevel3", "QualityLevel4", "QualityLevel5", "QualityLevel6", 574 | "QualityLevel7", "QualityLevel8", "QualityLevel9", "QualityLevel10"}), 575 | ScaleType = def_enum({"Stretch", "Slice", "Tile", "Fit", "Crop"}), 576 | ScreenOrientation = def_enum({"LandscapeLeft", "LandscapeRight", "LandscapeSensor", 577 | "Portrait", "Sensor"}), 578 | ScrollBarInset = def_enum({"None", "ScrollBar", "Always"}), 579 | ScrollingDirection = def_enum({"X", "Y", "XY"}), 580 | ServerAudioBehavior = def_enum({"Enabled", "Muted", "OnlineGame"}), 581 | SizeConstraint = def_enum({"RelativeXY", "RelativeXX", "RelativeYY"}), 582 | SortOrder = def_enum({"LayoutOrder", "Name", "Custom"}), 583 | SoundType = def_enum({"NoSound", "Boing", "Bomb", "Break", "Click", "Clock", 584 | "Slingshot", "Page", "Ping", "Snap", "Splat", "Step", "StepOn", "Swoosh", 585 | "Victory"}), 586 | SpecialKey = def_enum({"Insert", "Home", "End", "PageUp", "PageDown", "ChatHotkey"}), 587 | StartCorner = def_enum({"TopLeft", "TopRight", "BottomLeft", "BottomRight"}), 588 | Status = def_enum({"Poison", "Confusion"}), 589 | StudioStyleGuideColor = def_enum({"MainBackground", "Titlebar", "Dropdown", 590 | "Tooltip", "Notification", "ScrollBar", "ScrollBarBackground", "TabBar", "Tab", 591 | "RibbonTab", "RibbonTabTopBar", "Button", "MainButton", "RibbonButton", 592 | "ViewPortBackground", "InputFieldBackground", "Item", "TableItem", 593 | "CategoryItem", "GameSettingsTableItem", "GameSettingsTooltip", "EmulatorBar", 594 | "EmulatorDropDown", "ColorPickerFrame", "CurrentMarker", "Border", "Shadow", 595 | "Light", "Dark", "Mid", "MainText", "SubText", "TitlebarText", "BrightText", 596 | "DimmedText", "LinkText", "WarningText", "ErrorText", "InfoText", 597 | "SensitiveText", "ScriptSideWidget", "ScriptBackground", "ScriptText", 598 | "ScriptSelectionText", "ScriptSelectionBackground", 599 | "ScriptFindSelectionBackground", "ScriptMatchingWordSelectionBackground", 600 | "ScriptOperator", "ScriptNumber", "ScriptString", "ScriptComment", 601 | "ScriptPreprocessor", "ScriptKeyword", "ScriptBuiltInFunction", 602 | "ScriptWarning", "ScriptError", "DebuggerCurrentLine", "DebuggerErrorLine", 603 | "DiffFilePathText", "DiffTextHunkInfo", "DiffTextNoChange", "DiffTextAddition", 604 | "DiffTextDeletion", "DiffTextSeparatorBackground", 605 | "DiffTextNoChangeBackground", "DiffTextAdditionBackground", 606 | "DiffTextDeletionBackground", "DiffLineNum", "DiffLineNumSeparatorBackground", 607 | "DiffLineNumNoChangeBackground", "DiffLineNumAdditionBackground", 608 | "DiffLineNumDeletionBackground", "DiffFilePathBackground", 609 | "DiffFilePathBorder", "Separator", "ButtonBorder", "ButtonText", 610 | "InputFieldBorder", "CheckedFieldBackground", "CheckedFieldBorder", 611 | "CheckedFieldIndicator", "HeaderSection", "Midlight", "StatusBar"}), 612 | StudioStyleGuideModifier = def_enum({"Default", "Selected", "Pressed", "Disabled", 613 | "Hover"}), 614 | Style = def_enum({"AlternatingSupports", "BridgeStyleSupports", "NoSupports"}), 615 | SurfaceConstraint = def_enum({"None", "Hinge", "SteppingMotor", "Motor"}), 616 | SurfaceType = def_enum({"Smooth", "Glue", "Weld", "Studs", "Inlet", "Universal", 617 | "Hinge", "Motor", "SteppingMotor", "SmoothNoOutlines"}), 618 | SwipeDirection = def_enum({"Right", "Left", "Up", "Down", "None"}), 619 | TableMajorAxis = def_enum({"RowMajor", "ColumnMajor"}), 620 | Technology = def_enum({"Legacy", "Voxel"}), 621 | TeleportResult = def_enum({"Success", "Failure", "GameNotFound", "GameEnded", 622 | "GameFull", "Unauthorized", "Flooded", "IsTeleporting"}), 623 | TeleportState = def_enum({"RequestedFromServer", "Started", "WaitingForServer", 624 | "Failed", "InProgress"}), 625 | TeleportType = def_enum({"ToPlace", "ToInstance", "ToReservedServer"}), 626 | TextFilterContext = def_enum({"PublicChat", "PrivateChat"}), 627 | TextTruncate = def_enum({"None", "AtEnd"}), 628 | TextXAlignment = def_enum({"Left", "Center", "Right"}), 629 | TextYAlignment = def_enum({"Top", "Center", "Bottom"}), 630 | TextureMode = def_enum({"Stretch", "Wrap", "Static"}), 631 | TextureQueryType = def_enum({"NonHumanoid", "NonHumanoidOrphaned", "Humanoid", 632 | "HumanoidOrphaned"}), 633 | ThreadPoolConfig = def_enum({"Auto", "PerCore1", "PerCore2", "PerCore3", 634 | "PerCore4", "Threads1", "Threads2", "Threads3", "Threads4", "Threads8", 635 | "Threads16"}), 636 | ThrottlingPriority = def_enum({"Extreme", "ElevatedOnServer", "Default"}), 637 | ThumbnailSize = def_enum({"Size48x48", "Size180x180", "Size420x420", "Size60x60", 638 | "Size100x100", "Size150x150", "Size352x352"}), 639 | ThumbnailType = def_enum({"HeadShot", "AvatarBust", "AvatarThumbnail"}), 640 | TickCountSampleMethod = def_enum({"Fast", "Benchmark", "Precise"}), 641 | TopBottom = def_enum({"Top", "Center", "Bottom"}), 642 | TouchCameraMovementMode = def_enum({"Default", "Follow", "Classic", "Orbital"}), 643 | TouchMovementMode = def_enum({"Default", "Thumbstick", "DPad", "Thumbpad", 644 | "ClickToMove", "DynamicThumbstick"}), 645 | TweenStatus = def_enum({"Canceled", "Completed"}), 646 | UITheme = def_enum({"Light", "Dark"}), 647 | UiMessageType = def_enum({"UiMessageError", "UiMessageInfo"}), 648 | UploadSetting = def_enum({"Never", "Ask", "Always"}), 649 | UserCFrame = def_enum({"Head", "LeftHand", "RightHand"}), 650 | UserInputState = def_enum({"Begin", "Change", "End", "Cancel", "None"}), 651 | UserInputType = def_enum({"MouseButton1", "MouseButton2", "MouseButton3", 652 | "MouseWheel", "MouseMovement", "Touch", "Keyboard", "Focus", "Accelerometer", 653 | "Gyro", "Gamepad1", "Gamepad2", "Gamepad3", "Gamepad4", "Gamepad5", "Gamepad6", 654 | "Gamepad7", "Gamepad8", "TextInput", "None"}), 655 | VRTouchpad = def_enum({"Left", "Right"}), 656 | VRTouchpadMode = def_enum({"Touch", "VirtualThumbstick", "ABXY"}), 657 | VerticalAlignment = def_enum({"Center", "Top", "Bottom"}), 658 | VerticalScrollBarPosition = def_enum({"Left", "Right"}), 659 | VibrationMotor = def_enum({"Large", "Small", "LeftTrigger", "RightTrigger", 660 | "LeftHand", "RightHand"}), 661 | VideoQualitySettings = def_enum({"LowResolution", "MediumResolution", 662 | "HighResolution"}), 663 | VirtualInputMode = def_enum({"Recording", "Playing", "None"}), 664 | WaterDirection = def_enum({"NegX", "X", "NegY", "Y", "NegZ", "Z"}), 665 | WaterForce = def_enum({"None", "Small", "Medium", "Strong", "Max"}), 666 | ZIndexBehavior = def_enum({"Global", "Sibling"}), 667 | } 668 | } 669 | }, 670 | } 671 | 672 | stds.testez = { 673 | read_globals = { 674 | "describe", 675 | "it", "itFOCUS", "itSKIP", 676 | "FOCUS", "SKIP", "HACK_NO_XPCALL", 677 | "expect", 678 | } 679 | } 680 | 681 | stds.plugin = { 682 | read_globals = { 683 | "plugin", 684 | } 685 | } 686 | 687 | ignore = { 688 | "212", -- unused arguments 689 | } 690 | 691 | std = "lua51+roblox" 692 | 693 | files["**/*.spec.lua"] = { 694 | std = "+testez", 695 | } --------------------------------------------------------------------------------