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