From cb5c81c01f7d20beed6daa68a06ac72b0fc7335d Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 16 Nov 2024 11:35:28 -0500 Subject: [PATCH 01/14] Unwrap non-table components in query --- lib/{component.luau => Component.luau} | 58 ++++++++++++++++---------- lib/World.luau | 14 ++++--- lib/World.spec.luau | 12 ++++++ lib/component.spec.luau | 4 +- lib/init.luau | 2 +- 5 files changed, 58 insertions(+), 32 deletions(-) rename lib/{component.luau => Component.luau} (82%) diff --git a/lib/component.luau b/lib/Component.luau similarity index 82% rename from lib/component.luau rename to lib/Component.luau index c257ab12..0ab984a6 100644 --- a/lib/component.luau +++ b/lib/Component.luau @@ -43,28 +43,34 @@ local merge = require(script.Parent.immutable).merge -- This is a special value we set inside the component's metatable that will allow us to detect when -- a Component is accidentally inserted as a Component Instance. -- It should not be accessible through indexing into a component instance directly. -local DIAGNOSTIC_COMPONENT_MARKER = {} +local DIAGNOSTIC_COMPONENT_MARKER = table.freeze({}) + +local PRIMITIVE_MARKER = table.freeze({}) local lastId = 0 -local function newComponent(name, defaultData) +local function new(name: string, defaultData) name = name or debug.info(2, "s") .. "@" .. debug.info(2, "l") - assert( - defaultData == nil or type(defaultData) == "table", - "if component default data is specified, it must be a table" - ) + local ComponentInstance = {} + ComponentInstance.__index = ComponentInstance - local component = {} - component.__index = component + function ComponentInstance.new(data) + local mt = getmetatable(ComponentInstance :: any) + if typeof(data) == "table" then + data = data or {} - function component.new(data) - data = data or {} + if defaultData then + data = merge(defaultData, data) + end - if defaultData then - data = merge(defaultData, data) - end + mt[PRIMITIVE_MARKER] = false + return setmetatable(table.freeze(data), ComponentInstance) + else + data = data or defaultData - return table.freeze(setmetatable(data, component)) + mt[PRIMITIVE_MARKER] = true + return setmetatable({ data }, ComponentInstance) + end end --[=[ @@ -96,29 +102,29 @@ local function newComponent(name, defaultData) @param partialNewData {} -- The table to be merged with the existing component data. @return ComponentInstance -- A copy of the component instance with values from `partialNewData` overriding existing values. ]=] - function component:patch(partialNewData) - debug.profilebegin("patch") - local patch = getmetatable(self).new(merge(self, partialNewData)) - debug.profileend() - return patch + function ComponentInstance:patch(partialNewData) + return getmetatable(self).new(merge(self, partialNewData)) end lastId += 1 local id = lastId - setmetatable(component, { + setmetatable(ComponentInstance, { __call = function(_, ...) - return component.new(...) + return ComponentInstance.new(...) end, + __tostring = function() return name end, + __len = function() return id end, + [DIAGNOSTIC_COMPONENT_MARKER] = true, }) - return component + return ComponentInstance end local function assertValidType(value, position) @@ -177,10 +183,16 @@ local function assertComponentArgsProvided(...) end end +local function isPrimitive(componentInstance) + return getmetatable(componentInstance)[PRIMITIVE_MARKER] == true +end + return { - newComponent = newComponent, + new = new, assertValidComponentInstance = assertValidComponentInstance, assertValidComponentInstances = assertValidComponentInstances, assertComponentArgsProvided = assertComponentArgsProvided, assertValidComponent = assertValidComponent, + + isPrimitive = isPrimitive, } diff --git a/lib/World.luau b/lib/World.luau index 197b34f0..f9449e89 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -2,7 +2,7 @@ --!optimize 2 local Archetype = require(script.Parent.Archetype) -local Component = require(script.Parent.component) +local Component = require(script.Parent.Component) local topoRuntime = require(script.Parent.topoRuntime) local assertValidComponentInstances = Component.assertValidComponentInstances @@ -271,6 +271,7 @@ local function executeInsert(world: World, insertCommand: InsertCommand) local component = getmetatable(componentInstance) local componentId = #component local componentIds = table.clone(oldArchetype.componentIds) + local isPrimitive = Component.isPrimitive(component) local archetype: Archetype local entityIndex: number @@ -281,8 +282,7 @@ local function executeInsert(world: World, insertCommand: InsertCommand) entityIndex = transitionArchetype(world, entityId, entityRecord, archetype) oldComponentInstance = archetype.fields[archetype.idToIndex[componentId]][entityIndex] - -- FIXME: - -- This shouldn't be in a hotpath, probably better in createArchetype + -- FIXME: This shouldn't be in a hotpath, probably better in createArchetype world.componentIdToComponent[componentId] = component else archetype = oldArchetype @@ -290,7 +290,9 @@ local function executeInsert(world: World, insertCommand: InsertCommand) oldComponentInstance = oldArchetype.fields[oldArchetype.idToIndex[componentId]][entityIndex] end - archetype.fields[archetype.idToIndex[componentId]][entityIndex] = componentInstance + archetype.fields[archetype.idToIndex[componentId]][entityIndex] = if isPrimitive + then componentInstance[1] + else componentInstance world:_trackChanged(component, entityId, oldComponentInstance, componentInstance) oldArchetype = archetype @@ -1247,10 +1249,10 @@ end @param ... ComponentInstance -- The component values to insert ]=] function World:insert(id, ...) - assertWorldOperationIsValid(self, id, ...) + --assertWorldOperationIsValid(self, id, ...) local componentInstances = { ... } - assertValidComponentInstances(componentInstances) + --assertValidComponentInstances(componentInstances) bufferCommand(self, { type = "insert", entityId = id, componentInstances = componentInstances }) end diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 0986a654..bc0388d9 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -143,6 +143,18 @@ return function() end) describe("immediate", function() + itFOCUS("should allow non-table components", function() + local world = World.new() + local A = component() + local B = component() + + world:spawn(A("hello")) + + for id, a in world:query(A) do + print(id, a) + end + end) + it("should be iterable", function() local world = World.new() local A = component() diff --git a/lib/component.spec.luau b/lib/component.spec.luau index 17144a98..5fce5eee 100644 --- a/lib/component.spec.luau +++ b/lib/component.spec.luau @@ -1,6 +1,6 @@ -local Component = require(script.Parent.component) +local Component = require(script.Parent.Component) local None = require(script.Parent.immutable).None -local component = Component.newComponent +local component = Component.new local assertValidComponentInstance = Component.assertValidComponentInstance local assertValidComponent = Component.assertValidComponent diff --git a/lib/init.luau b/lib/init.luau index 644e9859..8a5dc2d3 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -55,7 +55,7 @@ local immutable = require(script.immutable) local World = require(script.World) local Loop = require(script.Loop) -local newComponent = require(script.component).newComponent +local newComponent = require(script.Component).new local topoRuntime = require(script.topoRuntime) export type World = World.World From a777bd75e53f1d649bb0994c4ebe53626f2658b0 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 16 Nov 2024 17:50:28 -0500 Subject: [PATCH 02/14] Allow different datatypes in the same component --- lib/Component.luau | 17 +++++++---------- lib/World.luau | 5 +++-- lib/World.spec.luau | 4 ++++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/Component.luau b/lib/Component.luau index 0ab984a6..dc941c20 100644 --- a/lib/Component.luau +++ b/lib/Component.luau @@ -55,21 +55,18 @@ local function new(name: string, defaultData) ComponentInstance.__index = ComponentInstance function ComponentInstance.new(data) - local mt = getmetatable(ComponentInstance :: any) - if typeof(data) == "table" then - data = data or {} + data = if data == nil then defaultData or {} else data + local component = getmetatable(ComponentInstance :: any) + if typeof(data) == "table" then if defaultData then data = merge(defaultData, data) end - mt[PRIMITIVE_MARKER] = false - return setmetatable(table.freeze(data), ComponentInstance) + return table.freeze(setmetatable(data, ComponentInstance)) else - data = data or defaultData - - mt[PRIMITIVE_MARKER] = true - return setmetatable({ data }, ComponentInstance) + component[PRIMITIVE_MARKER] = true + return table.freeze(setmetatable({ data = data, [PRIMITIVE_MARKER] = true }, ComponentInstance)) end end @@ -184,7 +181,7 @@ local function assertComponentArgsProvided(...) end local function isPrimitive(componentInstance) - return getmetatable(componentInstance)[PRIMITIVE_MARKER] == true + return componentInstance[PRIMITIVE_MARKER] ~= nil end return { diff --git a/lib/World.luau b/lib/World.luau index f9449e89..33688067 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -271,7 +271,7 @@ local function executeInsert(world: World, insertCommand: InsertCommand) local component = getmetatable(componentInstance) local componentId = #component local componentIds = table.clone(oldArchetype.componentIds) - local isPrimitive = Component.isPrimitive(component) + local isPrimitive = Component.isPrimitive(componentInstance) local archetype: Archetype local entityIndex: number @@ -291,8 +291,9 @@ local function executeInsert(world: World, insertCommand: InsertCommand) end archetype.fields[archetype.idToIndex[componentId]][entityIndex] = if isPrimitive - then componentInstance[1] + then componentInstance.data else componentInstance + world:_trackChanged(component, entityId, oldComponentInstance, componentInstance) oldArchetype = archetype diff --git a/lib/World.spec.luau b/lib/World.spec.luau index bc0388d9..21ebc522 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -149,6 +149,10 @@ return function() local B = component() world:spawn(A("hello")) + world:spawn(A(1)) + world:spawn(A({ whats_good = true })) + world:spawn(A()) + world:spawn(A("test")) for id, a in world:query(A) do print(id, a) From 5dce139e8cd927d9484d1da02c08286dd4fec8c8 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 16 Nov 2024 17:59:15 -0500 Subject: [PATCH 03/14] Add tests --- lib/World.spec.luau | 53 ++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 21ebc522..4d9ed758 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -143,22 +143,6 @@ return function() end) describe("immediate", function() - itFOCUS("should allow non-table components", function() - local world = World.new() - local A = component() - local B = component() - - world:spawn(A("hello")) - world:spawn(A(1)) - world:spawn(A({ whats_good = true })) - world:spawn(A()) - world:spawn(A("test")) - - for id, a in world:query(A) do - print(id, a) - end - end) - it("should be iterable", function() local world = World.new() local A = component() @@ -232,7 +216,7 @@ return function() local world = World.new() local Player = component() - local Health = component() + local Health = component(100) local Poison = component() local id = world:spawn(Player(), Poison()) @@ -264,6 +248,41 @@ return function() expect(world:remove(entityId, A)).to.equal(nil) end) + it("should allow inserting the same component with multiple datatypes", function() + local world = World.new() + local A = component() + + local one = world:spawn(A("hello")) + local two = world:spawn(A(1)) + local three = world:spawn(A({ whats_good = true })) + local four = world:spawn(A()) + local five = world:spawn(A("test")) + + expect(world:get(one, A)).to.equal("hello") + + expect(world:get(two, A)).to.equal(1) + + expect(typeof(world:get(three, A))).to.equal("table") + expect(world:get(three, A).whats_good).to.be.ok() + + expect(typeof(world:get(four, A))).to.equal("table") + expect(next(world:get(four, A))).to.never.be.ok() + + expect(world:get(five, A)).to.equal("test") + end) + + it("should unwrap non-tables in queries", function() + local world = World.new() + local A = component() + + local entity = world:spawn(A("hello")) + expect(world:query(A):next()).to.equal(entity) + + for _, a in world:query(A) do + expect(a).to.equal("hello") + end + end) + it("should not find any entities", function() local world = World.new() From c77d59a500b00ad4b4f4c727c4307acf60b30301 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 16 Nov 2024 18:01:38 -0500 Subject: [PATCH 04/14] Add back sanity checks --- lib/Component.luau | 1 - lib/World.luau | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/Component.luau b/lib/Component.luau index dc941c20..717e2119 100644 --- a/lib/Component.luau +++ b/lib/Component.luau @@ -140,7 +140,6 @@ local function assertValidComponent(value, position) assertValidType(value, position) local metatable = getmetatable(value) - if getmetatable(metatable) ~= nil and getmetatable(metatable)[DIAGNOSTIC_COMPONENT_MARKER] then error( string.format( diff --git a/lib/World.luau b/lib/World.luau index 33688067..d7ce5c36 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -1250,10 +1250,10 @@ end @param ... ComponentInstance -- The component values to insert ]=] function World:insert(id, ...) - --assertWorldOperationIsValid(self, id, ...) + assertWorldOperationIsValid(self, id, ...) local componentInstances = { ... } - --assertValidComponentInstances(componentInstances) + assertValidComponentInstances(componentInstances) bufferCommand(self, { type = "insert", entityId = id, componentInstances = componentInstances }) end From d906668a693ab89ab3dc90dfd9588245dbcabed8 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 16 Nov 2024 18:03:25 -0500 Subject: [PATCH 05/14] Add comments --- lib/Component.luau | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Component.luau b/lib/Component.luau index 717e2119..171aee9d 100644 --- a/lib/Component.luau +++ b/lib/Component.luau @@ -45,6 +45,7 @@ local merge = require(script.Parent.immutable).merge -- It should not be accessible through indexing into a component instance directly. local DIAGNOSTIC_COMPONENT_MARKER = table.freeze({}) +-- This tells the World whether the component should be unwrapped on insertion. local PRIMITIVE_MARKER = table.freeze({}) local lastId = 0 @@ -55,6 +56,8 @@ local function new(name: string, defaultData) ComponentInstance.__index = ComponentInstance function ComponentInstance.new(data) + -- If we aren't passed data, then we use the default data. + -- If no default data is provided, we want to default a table. data = if data == nil then defaultData or {} else data local component = getmetatable(ComponentInstance :: any) From a9060cf7774930ff6f506d0065b514d24f86059f Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 16 Nov 2024 19:45:15 -0500 Subject: [PATCH 06/14] Fix despawn and change tracking --- lib/World.luau | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index d7ce5c36..e50192f1 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -244,10 +244,12 @@ local function executeDespawn(world: World, despawnCommand: DespawnCommand) local archetype = entityRecord.archetype -- Track changes - for _, componentStorage in archetype.fields do - local componentInstance = componentStorage[entityRecord.indexInArchetype] - local component = getmetatable(componentInstance :: any) - world:_trackChanged(component, entityId, componentInstance, nil) + for index, componentStorage in archetype.fields do + local data = componentStorage[entityRecord.indexInArchetype] + local componentId = archetype.indexToId[index] + local component = world.componentIdToComponent[componentId] + + world:_trackChanged(component, entityId, data, nil) end -- TODO: @@ -268,10 +270,12 @@ local function executeInsert(world: World, insertCommand: InsertCommand) local oldArchetype = entityRecord.archetype for _, componentInstance in componentInstances do + local isPrimitive = Component.isPrimitive(componentInstance) + local data = if isPrimitive then componentInstance.data else componentInstance + local component = getmetatable(componentInstance) local componentId = #component local componentIds = table.clone(oldArchetype.componentIds) - local isPrimitive = Component.isPrimitive(componentInstance) local archetype: Archetype local entityIndex: number @@ -290,11 +294,8 @@ local function executeInsert(world: World, insertCommand: InsertCommand) oldComponentInstance = oldArchetype.fields[oldArchetype.idToIndex[componentId]][entityIndex] end - archetype.fields[archetype.idToIndex[componentId]][entityIndex] = if isPrimitive - then componentInstance.data - else componentInstance - - world:_trackChanged(component, entityId, oldComponentInstance, componentInstance) + archetype.fields[archetype.idToIndex[componentId]][entityIndex] = data + world:_trackChanged(component, entityId, oldComponentInstance, data) oldArchetype = archetype end @@ -1212,6 +1213,7 @@ function World._trackChanged(self: World, metatable, id, old, new) return end + --print("Track changed called", tostring(metatable), id, old, new, debug.traceback("", 2)) local record = table.freeze({ old = old, new = new, From ff4eed52a4a098714a196877de4b64ca5509ac6b Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 16 Nov 2024 19:45:30 -0500 Subject: [PATCH 07/14] Update tests --- lib/World.spec.luau | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 4d9ed758..86ef37df 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -216,7 +216,7 @@ return function() local world = World.new() local Player = component() - local Health = component(100) + local Health = component("Health", 100) local Poison = component() local id = world:spawn(Player(), Poison()) @@ -225,6 +225,7 @@ return function() expect(world:query(Health):next()).to.never.be.ok() world:insert(id, Health()) + print(world:get(id, Health)) expect(world:query(Player):next()).to.be.ok() expect(world:query(Health):next()).to.be.ok() @@ -238,6 +239,8 @@ return function() expect(world:query(Player):next()).to.never.be.ok() expect(world:query(Health):next()).to.be.ok() expect(world:size()).to.equal(1) + + expect(world:query(Health):next()).to.never.be.ok() end) it("should allow removing missing components", function() From 6edfec442ac2ef0394847f95b0160389805b7169 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 16 Nov 2024 19:45:37 -0500 Subject: [PATCH 08/14] Change example game model usage --- example/src/client/systems/roombasHurt.luau | 2 +- example/src/client/systems/spinSpinners.luau | 6 +++--- .../src/server/systems/mothershipsSpawnRoombas.luau | 2 +- example/src/server/systems/playersAreTargets.luau | 7 +------ example/src/server/systems/removeMissingModels.luau | 10 +++++----- example/src/server/systems/roombasMove.luau | 6 +++--- example/src/server/systems/spawnMotherships.luau | 9 ++------- example/src/server/systems/spawnRoombas.luau | 7 +------ example/src/server/systems/updateTransforms.luau | 8 ++++---- example/src/shared/setupTags.luau | 4 +--- example/src/shared/systems/updateModelAttribute.luau | 2 +- 11 files changed, 23 insertions(+), 40 deletions(-) diff --git a/example/src/client/systems/roombasHurt.luau b/example/src/client/systems/roombasHurt.luau index f8647ae4..1184e3d5 100644 --- a/example/src/client/systems/roombasHurt.luau +++ b/example/src/client/systems/roombasHurt.luau @@ -5,7 +5,7 @@ local Matter = require(ReplicatedStorage.Lib.Matter) local function roombasHurt(world) for _, _, model in world:query(Components.Roomba, Components.Model) do - for _, part in Matter.useEvent(model.model.PrimaryPart, "Touched") do + for _, part in Matter.useEvent(model.PrimaryPart, "Touched") do local touchedModel = part:FindFirstAncestorWhichIsA("Model") if not touchedModel then continue diff --git a/example/src/client/systems/spinSpinners.luau b/example/src/client/systems/spinSpinners.luau index 7170f4e7..03b23c21 100644 --- a/example/src/client/systems/spinSpinners.luau +++ b/example/src/client/systems/spinSpinners.luau @@ -11,11 +11,11 @@ local function spinSpinners(world, _, ui) local randomize = ui.button("Randomize colors!"):clicked() for _, model in world:query(Components.Model, Components.Spinner) do - model.model.PrimaryPart.CFrame = model.model.PrimaryPart.CFrame * CFrame.Angles(0, math.rad(5), 0) - model.model.PrimaryPart.Transparency = transparency + model.PrimaryPart.CFrame = model.PrimaryPart.CFrame * CFrame.Angles(0, math.rad(5), 0) + model.PrimaryPart.Transparency = transparency if randomize then - model.model.PrimaryPart.BrickColor = BrickColor.random() + model.PrimaryPart.BrickColor = BrickColor.random() end end end diff --git a/example/src/server/systems/mothershipsSpawnRoombas.luau b/example/src/server/systems/mothershipsSpawnRoombas.luau index 79890837..f2f0dabd 100644 --- a/example/src/server/systems/mothershipsSpawnRoombas.luau +++ b/example/src/server/systems/mothershipsSpawnRoombas.luau @@ -6,7 +6,7 @@ local function mothershipsSpawnRoombas(world) for id, model, lasering, transform in world:query(Components.Model, Components.Lasering, Components.Transform, Components.Mothership) do - model.model.Beam.Transparency = 1 - lasering.remainingTime + model.Beam.Transparency = 1 - lasering.remainingTime lasering = lasering:patch({ remainingTime = lasering.remainingTime - Matter.useDeltaTime(), diff --git a/example/src/server/systems/playersAreTargets.luau b/example/src/server/systems/playersAreTargets.luau index cf80b8cc..b8411f4d 100644 --- a/example/src/server/systems/playersAreTargets.luau +++ b/example/src/server/systems/playersAreTargets.luau @@ -6,12 +6,7 @@ local Matter = require(ReplicatedStorage.Lib.Matter) local function playersAreTargets(world) for _, player in ipairs(Players:GetPlayers()) do for _, character in Matter.useEvent(player, "CharacterAdded") do - world:spawn( - Components.Target(), - Components.Model({ - model = character, - }) - ) + world:spawn(Components.Target(), Components.Model(character)) end end diff --git a/example/src/server/systems/removeMissingModels.luau b/example/src/server/systems/removeMissingModels.luau index 349f9dd7..6ec5b73c 100644 --- a/example/src/server/systems/removeMissingModels.luau +++ b/example/src/server/systems/removeMissingModels.luau @@ -4,21 +4,21 @@ local Matter = require(ReplicatedStorage.Lib.Matter) local function removeMissingModels(world) for id, model in world:query(Components.Model) do - for _ in Matter.useEvent(model.model, "AncestryChanged") do - if model.model:IsDescendantOf(game) == false then + for _ in Matter.useEvent(model, "AncestryChanged") do + if model:IsDescendantOf(game) == false then world:remove(id, Components.Model) break end end - if not model.model.PrimaryPart then + if not model.PrimaryPart then world:remove(id, Components.Model) end end for _id, modelRecord in world:queryChanged(Components.Model) do if modelRecord.new == nil then - if modelRecord.old and modelRecord.old.model then - modelRecord.old.model:Destroy() + if modelRecord.old then + modelRecord.old:Destroy() end end end diff --git a/example/src/server/systems/roombasMove.luau b/example/src/server/systems/roombasMove.luau index e5498856..a9763433 100644 --- a/example/src/server/systems/roombasMove.luau +++ b/example/src/server/systems/roombasMove.luau @@ -4,7 +4,7 @@ local Components = require(ReplicatedStorage.Shared.components) local function roombasMove(world) local targets = {} for _, model in world:query(Components.Model, Components.Target) do - table.insert(targets, model.model.PrimaryPart.CFrame.p) + table.insert(targets, model.PrimaryPart.CFrame.p) end for _, _, charge, model in world:query(Components.Roomba, Components.Charge, Components.Model) do @@ -13,7 +13,7 @@ local function roombasMove(world) end local closestPosition, closestDistance - local currentPosition = model.model.PrimaryPart.CFrame.p + local currentPosition = model.PrimaryPart.CFrame.p for _, target in ipairs(targets) do local distance = (currentPosition - target).magnitude @@ -24,7 +24,7 @@ local function roombasMove(world) end if closestPosition then - local body = model.model.Roomba + local body = model.Roomba local force = body:GetMass() * 20 if closestDistance < 4 then diff --git a/example/src/server/systems/spawnMotherships.luau b/example/src/server/systems/spawnMotherships.luau index e6d464fe..c14805ad 100644 --- a/example/src/server/systems/spawnMotherships.luau +++ b/example/src/server/systems/spawnMotherships.luau @@ -28,12 +28,7 @@ local function spawnMotherships(world) model.Parent = workspace model.PrimaryPart:SetNetworkOwner(nil) - world:insert( - id, - Components.Model({ - model = model, - }) - ) + world:insert(id, Components.Model(model)) end for id, mothership, transform in @@ -58,7 +53,7 @@ local function spawnMotherships(world) end for _, mothership, model in world:query(Components.Mothership, Components.Model):without(Components.Lasering) do - model.model.Roomba.AlignPosition.Position = mothership.goal + model.Roomba.AlignPosition.Position = mothership.goal end end diff --git a/example/src/server/systems/spawnRoombas.luau b/example/src/server/systems/spawnRoombas.luau index 95abe9f4..c286c35a 100644 --- a/example/src/server/systems/spawnRoombas.luau +++ b/example/src/server/systems/spawnRoombas.luau @@ -7,12 +7,7 @@ local function spawnRoombas(world) model.Parent = workspace model.PrimaryPart:SetNetworkOwner(nil) - world:insert( - id, - Components.Model({ - model = model, - }) - ) + world:insert(id, Components.Model(model)) end end diff --git a/example/src/server/systems/updateTransforms.luau b/example/src/server/systems/updateTransforms.luau index 3ce49315..c55b9ac3 100644 --- a/example/src/server/systems/updateTransforms.luau +++ b/example/src/server/systems/updateTransforms.luau @@ -16,7 +16,7 @@ local function updateTransforms(world) end if transformRecord.new and not transformRecord.new.doNotReconcile then - model.model:SetPrimaryPartCFrame(transformRecord.new.cframe) + model:SetPrimaryPartCFrame(transformRecord.new.cframe) end end @@ -33,18 +33,18 @@ local function updateTransforms(world) end if modelRecord.new then - modelRecord.new.model:SetPrimaryPartCFrame(transform.cframe) + modelRecord.new:SetPrimaryPartCFrame(transform.cframe) end end -- Update Transform on unanchored Models for id, model, transform in world:query(Components.Model, Components.Transform) do - if model.model.PrimaryPart.Anchored then + if model.PrimaryPart.Anchored then continue end local existingCFrame = transform.cframe - local currentCFrame = model.model.PrimaryPart.CFrame + local currentCFrame = model.PrimaryPart.CFrame -- Despawn models that fall into the void if currentCFrame.Y < -400 then diff --git a/example/src/shared/setupTags.luau b/example/src/shared/setupTags.luau index 91fefab8..d4c65f73 100644 --- a/example/src/shared/setupTags.luau +++ b/example/src/shared/setupTags.luau @@ -10,9 +10,7 @@ local function setupTags(world) local function spawnBound(instance, component) local id = world:spawn( component(), - Components.Model({ - model = instance, - }), + Components.Model(instance), Components.Transform({ cframe = instance.PrimaryPart.CFrame, }) diff --git a/example/src/shared/systems/updateModelAttribute.luau b/example/src/shared/systems/updateModelAttribute.luau index fbad30fc..18201db6 100644 --- a/example/src/shared/systems/updateModelAttribute.luau +++ b/example/src/shared/systems/updateModelAttribute.luau @@ -7,7 +7,7 @@ local name = RunService:IsServer() and "serverEntityId" or "clientEntityId" local function updateModelAttribute(world) for id, record in world:queryChanged(Components.Model) do if record.new then - record.new.model:SetAttribute(name, id) + record.new:SetAttribute(name, id) end end end From 7ce68820aa78d0538e20a85f04fccb973fc4325e Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 17 Nov 2024 11:58:26 -0500 Subject: [PATCH 09/14] Format non-table values in debugger --- example/src/shared/start.luau | 3 +-- lib/debugger/{formatTable.luau => formatValue.luau} | 12 ++++++++---- lib/debugger/widgets/entityInspect.luau | 8 ++++---- lib/debugger/widgets/hoverInspect.luau | 8 ++++---- lib/debugger/widgets/valueInspect.luau | 8 ++++---- lib/debugger/widgets/worldInspect.luau | 8 ++++---- lib/hooks/log.luau | 4 ++-- 7 files changed, 27 insertions(+), 24 deletions(-) rename lib/debugger/{formatTable.luau => formatValue.luau} (92%) diff --git a/example/src/shared/start.luau b/example/src/shared/start.luau index 07df6552..0d2a383e 100644 --- a/example/src/shared/start.luau +++ b/example/src/shared/start.luau @@ -19,8 +19,7 @@ local function start(containers) end local model = world:get(id, components.Model) - - return model and model.model or nil + return model end local loop = Matter.Loop.new(world, state, debugger:getWidgets()) diff --git a/lib/debugger/formatTable.luau b/lib/debugger/formatValue.luau similarity index 92% rename from lib/debugger/formatTable.luau rename to lib/debugger/formatValue.luau index 01dacd2e..0c893a2b 100644 --- a/lib/debugger/formatTable.luau +++ b/lib/debugger/formatValue.luau @@ -38,7 +38,11 @@ local FormatMode = { Short = "Short", Long = "Long", } -local function formatTable(object, mode, _padLength, _depth) +local function formatValue(object, mode, _padLength, _depth) + if typeof(object) ~= "table" then + return tostring(object) + end + mode = mode or FormatMode.Short _padLength = _padLength or 0 _depth = _depth or 1 @@ -87,7 +91,7 @@ local function formatTable(object, mode, _padLength, _depth) part ..= "[{..}]=" else part ..= "[" - part ..= formatTable(key, FormatMode.Short, #str + #part + _padLength, _depth + 1) + part ..= formatValue(key, FormatMode.Short, #str + #part + _padLength, _depth + 1) part ..= "] = " end end @@ -107,7 +111,7 @@ local function formatTable(object, mode, _padLength, _depth) if mode == FormatMode.Short then part ..= "{..}" else - part ..= formatTable(value, FormatMode.Long, #str + #part + _padLength, _depth + 1) + part ..= formatValue(value, FormatMode.Long, #str + #part + _padLength, _depth + 1) end elseif mode == FormatMode.Long and (luaType == "userdata" or luaType == "vector") then if robloxType == "CFrame" then @@ -157,6 +161,6 @@ local function formatTable(object, mode, _padLength, _depth) end return { - formatTable = formatTable, + formatValue = formatValue, FormatMode = FormatMode, } diff --git a/lib/debugger/widgets/entityInspect.luau b/lib/debugger/widgets/entityInspect.luau index 966c7c75..d6f202aa 100644 --- a/lib/debugger/widgets/entityInspect.luau +++ b/lib/debugger/widgets/entityInspect.luau @@ -1,7 +1,7 @@ -local formatTableModule = require(script.Parent.Parent.formatTable) +local formatValueModule = require(script.Parent.Parent.formatValue) local getAllComponentData = require(script.Parent.Parent.getAllComponentData) -local formatTable = formatTableModule.formatTable -local FormatMode = formatTableModule.FormatMode +local formatValue = formatValueModule.formatValue +local FormatMode = formatValueModule.FormatMode return function(plasma) return plasma.widget(function(debugger, world) @@ -42,7 +42,7 @@ return function(plasma) for component, componentData in getAllComponentData(world, debugger.debugEntity) do table.insert(items, { tostring(component), - formatTable(componentData, FormatMode.Long), + formatValue(componentData, FormatMode.Long), }) end diff --git a/lib/debugger/widgets/hoverInspect.luau b/lib/debugger/widgets/hoverInspect.luau index cbfc3074..2b45b112 100644 --- a/lib/debugger/widgets/hoverInspect.luau +++ b/lib/debugger/widgets/hoverInspect.luau @@ -1,7 +1,7 @@ -local formatTableModule = require(script.Parent.Parent.formatTable) +local formatValueModule = require(script.Parent.Parent.formatValue) local getAllComponentData = require(script.Parent.Parent.getAllComponentData) -local formatTable = formatTableModule.formatTable -local FormatMode = formatTableModule.FormatMode +local formatValue = formatValueModule.formatValue +local FormatMode = formatValueModule.FormatMode return function(plasma) return plasma.widget(function(world, id, custom) @@ -15,7 +15,7 @@ return function(plasma) if next(componentData) == nil then str ..= "{ }\n" else - str ..= (formatTable(componentData, FormatMode.Long, 0, 2) .. "\n") + str ..= (formatValue(componentData, FormatMode.Long, 0, 2) .. "\n") end end diff --git a/lib/debugger/widgets/valueInspect.luau b/lib/debugger/widgets/valueInspect.luau index 20466ff5..8ee628e2 100644 --- a/lib/debugger/widgets/valueInspect.luau +++ b/lib/debugger/widgets/valueInspect.luau @@ -1,5 +1,5 @@ -local formatTableModule = require(script.Parent.Parent.formatTable) -local formatTable = formatTableModule.formatTable +local formatValueModule = require(script.Parent.Parent.formatValue) +local formatValue = formatValueModule.formatValue return function(plasma) return plasma.widget(function(objectStack, custom) @@ -41,13 +41,13 @@ return function(plasma) valueItem = function() if custom - .link(formatTable(value), { + .link(formatValue(value), { font = Enum.Font.Code, }) :clicked() then table.insert(objectStack, { - key = if type(key) == "table" then formatTable(key) else tostring(key), + key = if type(key) == "table" then formatValue(key) else tostring(key), value = value, }) end diff --git a/lib/debugger/widgets/worldInspect.luau b/lib/debugger/widgets/worldInspect.luau index 358f72d2..8bb29024 100644 --- a/lib/debugger/widgets/worldInspect.luau +++ b/lib/debugger/widgets/worldInspect.luau @@ -1,6 +1,6 @@ -local formatTableModule = require(script.Parent.Parent.formatTable) +local formatValueModule = require(script.Parent.Parent.formatValue) local getAllComponentData = require(script.Parent.Parent.getAllComponentData) -local formatTable = formatTableModule.formatTable +local formatValue = formatValueModule.formatValue local BY_COMPONENT_NAME = "ComponentName" local BY_ENTITY_COUNT = "EntityCount" @@ -130,7 +130,7 @@ return function(plasma) for entityId, data in world:query(debugComponent) do table.insert(items, { entityId, - formatTable(data), + formatValue(data), selected = debugger.debugEntity == entityId, }) @@ -170,7 +170,7 @@ return function(plasma) for i = 1, #intersectingComponents do local data = intersectingData[item[1]][i] - table.insert(item, if data then formatTable(data) else "") + table.insert(item, if data then formatValue(data) else "") end end diff --git a/lib/hooks/log.luau b/lib/hooks/log.luau index e0d944cd..e4de8cd1 100644 --- a/lib/hooks/log.luau +++ b/lib/hooks/log.luau @@ -1,5 +1,5 @@ local topoRuntime = require(script.Parent.Parent.topoRuntime) -local format = require(script.Parent.Parent.debugger.formatTable) +local format = require(script.Parent.Parent.debugger.formatValue) --[=[ @within Matter @@ -25,7 +25,7 @@ local function log(...) local value = select(i, ...) if type(value) == "table" then - segments[i] = format.formatTable(value) + segments[i] = format.formatValue(value) else segments[i] = tostring(value) end From b196b4f868f6473d213554ce5e0feeaad8ac259c Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 17 Nov 2024 11:59:39 -0500 Subject: [PATCH 10/14] Remove print --- lib/World.luau | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/World.luau b/lib/World.luau index e50192f1..0359d39e 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -1213,7 +1213,6 @@ function World._trackChanged(self: World, metatable, id, old, new) return end - --print("Track changed called", tostring(metatable), id, old, new, debug.traceback("", 2)) local record = table.freeze({ old = old, new = new, From 8e8cf67ff539e419e387e659e458d3bb23a495ae Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 18 Nov 2024 17:11:48 -0500 Subject: [PATCH 11/14] Remove unneeded code --- lib/Component.luau | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Component.luau b/lib/Component.luau index 171aee9d..72ccd342 100644 --- a/lib/Component.luau +++ b/lib/Component.luau @@ -60,7 +60,6 @@ local function new(name: string, defaultData) -- If no default data is provided, we want to default a table. data = if data == nil then defaultData or {} else data - local component = getmetatable(ComponentInstance :: any) if typeof(data) == "table" then if defaultData then data = merge(defaultData, data) @@ -68,7 +67,6 @@ local function new(name: string, defaultData) return table.freeze(setmetatable(data, ComponentInstance)) else - component[PRIMITIVE_MARKER] = true return table.freeze(setmetatable({ data = data, [PRIMITIVE_MARKER] = true }, ComponentInstance)) end end From 672977720402bb7c6febe7db7bafa0111dbd5664 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 18 Nov 2024 17:31:10 -0500 Subject: [PATCH 12/14] Rename class --- lib/Component.luau | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/Component.luau b/lib/Component.luau index 72ccd342..35952653 100644 --- a/lib/Component.luau +++ b/lib/Component.luau @@ -52,10 +52,10 @@ local lastId = 0 local function new(name: string, defaultData) name = name or debug.info(2, "s") .. "@" .. debug.info(2, "l") - local ComponentInstance = {} - ComponentInstance.__index = ComponentInstance + local Component = {} + Component.__index = Component - function ComponentInstance.new(data) + function Component.new(data) -- If we aren't passed data, then we use the default data. -- If no default data is provided, we want to default a table. data = if data == nil then defaultData or {} else data @@ -65,9 +65,9 @@ local function new(name: string, defaultData) data = merge(defaultData, data) end - return table.freeze(setmetatable(data, ComponentInstance)) + return table.freeze(setmetatable(data, Component)) else - return table.freeze(setmetatable({ data = data, [PRIMITIVE_MARKER] = true }, ComponentInstance)) + return table.freeze(setmetatable({ data = data, [PRIMITIVE_MARKER] = true }, Component)) end end @@ -100,15 +100,15 @@ local function new(name: string, defaultData) @param partialNewData {} -- The table to be merged with the existing component data. @return ComponentInstance -- A copy of the component instance with values from `partialNewData` overriding existing values. ]=] - function ComponentInstance:patch(partialNewData) + function Component:patch(partialNewData) return getmetatable(self).new(merge(self, partialNewData)) end lastId += 1 local id = lastId - setmetatable(ComponentInstance, { + setmetatable(Component, { __call = function(_, ...) - return ComponentInstance.new(...) + return Component.new(...) end, __tostring = function() @@ -122,7 +122,7 @@ local function new(name: string, defaultData) [DIAGNOSTIC_COMPONENT_MARKER] = true, }) - return ComponentInstance + return Component end local function assertValidType(value, position) From 78a1238ce995f173cead777c57d53df74ee7debe Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 20 Nov 2024 12:19:21 -0500 Subject: [PATCH 13/14] Start updating docs --- lib/Component.luau | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Component.luau b/lib/Component.luau index 35952653..b7e277fe 100644 --- a/lib/Component.luau +++ b/lib/Component.luau @@ -6,11 +6,13 @@ local merge = require(script.Parent.immutable).merge A component is a named piece of data that exists on an entity. Components are created and removed in the [World](/api/World). - In the docs, the terms "Component" and "ComponentInstance" are used: + In the docs, the terms "Component", "ComponentInstance", and "ComponentValue" are used: - **"Component"** refers to the base class of a specific type of component you've created. This is what [`Matter.component`](/api/Matter#component) returns. - - **"Component Instance"** refers to an actual piece of data that can exist on an entity. + - **"Component Instance"** refers to an instance of a component with a table value. The metatable of a component instance table is its respective Component table. + - **"Component Value"** refers to an actual piece of data that can exist on an entity. It can be + a "ComponentInstance" or another datatype. Component instances are *plain-old data*: they do not contain behaviors or methods. From e233ade3c2714786091a70f3da9eebb7f327417f Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 12 Dec 2024 13:14:28 -0500 Subject: [PATCH 14/14] Prototype new docs structure --- lib/Component.luau | 78 +++++++++++++++++++++------------------------- moonwave.toml | 12 +++++-- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/lib/Component.luau b/lib/Component.luau index b7e277fe..1e9e7504 100644 --- a/lib/Component.luau +++ b/lib/Component.luau @@ -6,15 +6,11 @@ local merge = require(script.Parent.immutable).merge A component is a named piece of data that exists on an entity. Components are created and removed in the [World](/api/World). - In the docs, the terms "Component", "ComponentInstance", and "ComponentValue" are used: + In the docs, the terms "Component" and "ComponentInstance" are used: - **"Component"** refers to the base class of a specific type of component you've created. This is what [`Matter.component`](/api/Matter#component) returns. - - **"Component Instance"** refers to an instance of a component with a table value. - The metatable of a component instance table is its respective Component table. - - **"Component Value"** refers to an actual piece of data that can exist on an entity. It can be - a "ComponentInstance" or another datatype. - - Component instances are *plain-old data*: they do not contain behaviors or methods. + - **"Component Instance"** refers to what encapsulates data you pass into a Component. + You only really interact with a [TableComponentInstance](/api/TableComponentInstance). Since component instances are immutable, one helper function exists on all component instances, `patch`, which allows reusing data from an existing component instance to make up for the ergonomic loss of mutations. @@ -22,24 +18,22 @@ local merge = require(script.Parent.immutable).merge --[=[ @within Component - @type ComponentInstance {} - - The `ComponentInstance` type refers to an actual piece of data that can exist on an entity. - The metatable of the component instance table is set to its particular Component table. + @type ComponentInstance TableComponentInstance | ValueComponentInstance +]=] - A component instance can be created by calling the Component table: +--[=[ + @within Component + @type ValueComponentInstance {} - ```lua - -- Component: - local MyComponent = Matter.component("My component") + If you pass anything other than a table into a Component, then you will get a ValueComponentInstance back. + Unlike a [TableComponentInstance](/api/TableComponentInstance), you shouldn't need to interact with this + and it will not be passed back to you in queries. - -- component instance: - local myComponentInstance = MyComponent({ - some = "data" - }) + This is strictly used for insertions. +]=] - print(getmetatable(myComponentInstance) == MyComponent) --> true - ``` +--[=[ + @class TableComponentInstance ]=] -- This is a special value we set inside the component's metatable that will allow us to detect when @@ -74,33 +68,33 @@ local function new(name: string, defaultData) end --[=[ - @within Component - - ```lua - for id, target in world:query(Target) do - if shouldChangeTarget(target) then - world:insert(id, target:patch({ -- modify the existing component - currentTarget = getNewTarget() - })) + @within TableComponentInstance + + ```lua + for id, target in world:query(Target) do + if shouldChangeTarget(target) then + world:insert(id, target:patch({ -- modify the existing component + currentTarget = getNewTarget() + })) + end end - end - ``` + ``` - A utility function used to immutably modify an existing component instance. Key/value pairs from the passed table - will override those of the existing component instance. + A utility function used to immutably modify an existing component instance. Key/value pairs from the passed table + will override those of the existing component instance. - As all components are immutable and frozen, it is not possible to modify the existing component directly. + As all components are immutable and frozen, it is not possible to modify the existing component directly. - You can use the `Matter.None` constant to remove a value from the component instance: + You can use the `Matter.None` constant to remove a value from the component instance: - ```lua - target:patch({ - currentTarget = Matter.None -- sets currentTarget to nil - }) - ``` + ```lua + target:patch({ + currentTarget = Matter.None -- sets currentTarget to nil + }) + ``` - @param partialNewData {} -- The table to be merged with the existing component data. - @return ComponentInstance -- A copy of the component instance with values from `partialNewData` overriding existing values. + @param partialNewData {} -- The table to be merged with the existing component data. + @return TableComponentInstance -- A copy of the component instance with values from `partialNewData` overriding existing values. ]=] function Component:patch(partialNewData) return getmetatable(self).new(merge(self, partialNewData)) diff --git a/moonwave.toml b/moonwave.toml index 14d7c13c..48c0257b 100644 --- a/moonwave.toml +++ b/moonwave.toml @@ -1,10 +1,17 @@ -classOrder = ["Matter", "World", "QueryResult", "Loop"] gitSourceBranch = "main" [docusaurus] -url = "https://eryn.io" +url = "https://matter-ecs.github.io/" +baseUrl = "/matter" tagline = "A modern ECS library for Roblox" +[[classOrder]] +classes = ["Matter", "World", "QueryResult", "Loop"] + +[[classOrder]] +section = "Components" +classes = ["Component", "TableComponentInstance"] + [[navbar.items]] href = "https://discord.gg/aQwDAYhqtJ" label = "Roblox Open Source Discord" @@ -25,4 +32,3 @@ src = "/logo.svg" [home] enabled = true includeReadme = false -