From cfa85ac2f588e8816bdb80fde6ed6c2e54942c26 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 14 Jul 2024 00:03:15 -0400 Subject: [PATCH 01/87] initial files --- lib/NewStorage.luau | 23 +++++ lib/World.luau | 205 +++++++++++++++++++++++++++--------------- lib/World.spec.luau | 11 +++ testez-companion.toml | 1 + 4 files changed, 168 insertions(+), 72 deletions(-) create mode 100644 lib/NewStorage.luau create mode 100644 testez-companion.toml diff --git a/lib/NewStorage.luau b/lib/NewStorage.luau new file mode 100644 index 00000000..6887f0f7 --- /dev/null +++ b/lib/NewStorage.luau @@ -0,0 +1,23 @@ +--!strict +type EntityId = number +type ComponentInstance = { [any]: any } +type ComponentMetatable = { [any]: any } + +type ComponentId = ComponentMetatable +type ArchetypeId = number +--type Archetype = { id: number, componentMetatables: { ComponentId } } + +type ContiguousEntityId = number +type Archetype = { + id: number, + componentMetatables: { ComponentId }, + + -- component ids are contiguous by nature + components: { [ComponentId]: { [ContiguousEntityId]: ComponentInstance } }, +} + +-- find archetypes containing component +type ComponentIndex = { [ComponentId]: { ArchetypeId } } + +-- find archetype for entity +type EntityIndex = { [EntityId]: Archetype } diff --git a/lib/World.luau b/lib/World.luau index 6ad7447c..6f22063e 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -8,6 +8,40 @@ local archetypeOf = archetypeModule.archetypeOf local negateArchetypeOf = archetypeModule.negateArchetypeOf local areArchetypesCompatible = archetypeModule.areArchetypesCompatible +type EntityId = number +type DenseEntityId = number + +type Component = { [any]: any } +type ComponentInstance = { [any]: any } +type ComponentMetatable = { [any]: any } +type ComponentMetatables = { ComponentMetatable } +type ComponentId = ComponentMetatable + +type ArchetypeId = string +type Archetype = { + id: number, + + componentMetatables: ComponentMetatables, + components: { [ComponentId]: { [DenseEntityId]: ComponentInstance } }, +} + +type EntityRecord = { + indexInArchetype: DenseEntityId?, + archetype: Archetype?, +} + +-- find archetype for entity +type EntityIndex = { [EntityId]: EntityRecord? } + +-- find archetypes containing component +type ComponentIndex = { [ComponentId]: { [ArchetypeId]: number } } + +-- find archetype from all components +type Archetypes = { [ArchetypeId]: Archetype } + +type Storage = { [string]: { [EntityId]: ComponentInstance } } +type Storages = { Storage } + local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" --[=[ @@ -24,11 +58,16 @@ World.__index = World Creates a new World. ]=] function World.new() - local firstStorage = {} + local firstStorage: Storage = {} return setmetatable({ + _entityIndex = {} :: EntityIndex, + _componentIndex = {} :: ComponentIndex, + _archetypes = {} :: Archetypes, + -- List of maps from archetype string --> entity ID --> entity data - _storages = { firstStorage }, + _storages = { firstStorage } :: Storages, + -- The most recent storage that has not been dirtied by an iterator _pristineStorage = firstStorage, @@ -208,45 +247,62 @@ function World:_updateQueryCache(entityArchetype) end end -function World:_transitionArchetype(id, components) +function World._transitionArchetype(self: typeof(World.new()), id: EntityId, metatableToInstance: { [any]: any }?) + print("transitionArchetype", id, metatableToInstance) debug.profilebegin("transitionArchetype") - local newArchetype = nil - local oldArchetype = self._entityArchetypes[id] - local oldStorage - if oldArchetype then - oldStorage = self:_getStorageWithEntity(oldArchetype, id) + if metatableToInstance == nil then + -- Remove all components + local entityRecord = self._entityIndex[id] + assert(entityRecord, "..") - if not components then - oldStorage[oldArchetype][id] = nil + for metatable in entityRecord.archetype.components do + -- TODO: + -- swap remove + table.remove(entityRecord.archetype.components[metatable], entityRecord.indexInArchetype) end - end - if components then - newArchetype = archetypeOf(unpack(self._entityMetatablesCache[id])) + entityRecord.archetype = nil + entityRecord.indexInArchetype = nil + else + local metatables = {} + for metatable in metatableToInstance do + table.insert(metatables, metatable) + end - if oldArchetype ~= newArchetype then - if oldStorage then - oldStorage[oldArchetype][id] = nil - end + local entityRecord = self._entityIndex[id] + if entityRecord == nil then + entityRecord = {} + self._entityIndex[id] = entityRecord + end - if self._pristineStorage[newArchetype] == nil then - self._pristineStorage[newArchetype] = {} + -- Find the archetype that matches these components + local newArchetypeId = archetypeOf(unpack(metatables)) + local newArchetype = self._archetypes[newArchetypeId] + if newArchetype == nil then + local archetypeComponents = {} + for metatable, _ in metatableToInstance do + archetypeComponents[metatable] = {} end - if self._entityArchetypeCache[newArchetype] == nil then - debug.profilebegin("update query cache") - self._entityArchetypeCache[newArchetype] = true - self:_updateQueryCache(newArchetype) - debug.profileend() - end - self._pristineStorage[newArchetype][id] = components - else - oldStorage[newArchetype][id] = components + newArchetype = { + components = archetypeComponents, + } :: Archetype + + self._archetypes[newArchetypeId] = newArchetype + end + + -- Add entity to archetype + local indexInArchetype = #newArchetype.components[metatables[1]] + 1 + for componentMetatable, componentInstance in metatableToInstance do + newArchetype.components[componentMetatable][indexInArchetype] = componentInstance end - end - self._entityArchetypes[id] = newArchetype + entityRecord.indexInArchetype = indexInArchetype + entityRecord.archetype = newArchetype + + print("New archetype", newArchetype) + end debug.profileend() end @@ -336,8 +392,8 @@ end @param id number -- The entity ID @return bool -- `true` if the entity exists ]=] -function World:contains(id) - return self._entityArchetypes[id] ~= nil +function World.contains(self: typeof(World.new()), id) + return self._entityIndex[id] ~= nil end --[=[ @@ -347,28 +403,34 @@ end @param ... Component -- The components to fetch @return ... -- Returns the component values in the same order they were passed in ]=] -function World:get(id, ...) - if not self:contains(id) then +function World.get(self: typeof(World.new()), id, ...: Component) + local entityRecord = self._entityIndex[id] + if entityRecord == nil then error(ERROR_NO_ENTITY, 2) end - local entity = self:_getEntity(id) + local archetype = entityRecord.archetype + print("Get archetype:", archetype) + -- TODO: + -- handle nil archetype local length = select("#", ...) - - if length == 1 then - assertValidComponent((...), 1) - return entity[...] - end - - local components = {} + local componentInstances = {} for i = 1, length do - local metatable = select(i, ...) - assertValidComponent(metatable, i) - components[i] = entity[metatable] + local component = select(i, ...) + assertValidComponent(component, i) + + -- Does this component belong to the archetype that this entity is in? + if archetype.components[component] == nil then + -- No, so it doesn't have the value + table.insert(componentInstances, nil) + else + -- Yes + table.insert(componentInstances, archetype.components[component][entityRecord.indexInArchetype]) + end end - return unpack(components, 1, length) + return unpack(componentInstances, 1, length) end local function noop() end @@ -1033,7 +1095,7 @@ function World:insert(id, ...) entity[metatable] = newComponent end - if wasNew then -- wasNew + if wasNew then self:_transitionArchetype(id, entity) end @@ -1051,42 +1113,41 @@ end @param ... Component -- The components to remove @return ...ComponentInstance -- Returns the component instance values that were removed in the order they were passed. ]=] -function World:remove(id, ...) - if not self:contains(id) then +function World.remove(self: typeof(World.new()), id, ...: ComponentMetatable) + local entityRecord = self._entityIndex[id] + if entityRecord == nil then error(ERROR_NO_ENTITY, 2) end - local entity = self:_getEntity(id) - + local oldArchetype = entityRecord.archetype local length = select("#", ...) - local removed = {} + local removedComponentInstances = {} + + -- TODO: + -- lookups + local myComponentsAfter = {} + for metatable, map in oldArchetype.components do + myComponentsAfter[metatable] = map[entityRecord.indexInArchetype] + end for i = 1, length do local metatable = select(i, ...) + if oldArchetype.components[metatable] == nil then + table.insert(removedComponentInstances, nil) + else + -- Cache old value + table.insert(removedComponentInstances, oldArchetype.components[metatable][entityRecord.indexInArchetype]) - assertValidComponent(metatable, i) - - local oldComponent = entity[metatable] - - removed[i] = oldComponent - - self:_trackChanged(metatable, id, oldComponent, nil) - - entity[metatable] = nil - end - - -- Rebuild entity metatable cache - local metatables = {} + -- Now remove + table.remove(oldArchetype.components[metatable], entityRecord.indexInArchetype) - for metatable in pairs(entity) do - table.insert(metatables, metatable) + myComponentsAfter[metatable] = nil + end end - self._entityMetatablesCache[id] = metatables - - self:_transitionArchetype(id, entity) - - return unpack(removed, 1, length) + print("Transitioning after remove", myComponentsAfter) + self:_transitionArchetype(id, myComponentsAfter) + return unpack(removedComponentInstances, 1, length) end --[=[ diff --git a/lib/World.spec.luau b/lib/World.spec.luau index e2e79ad3..0babc7b1 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -41,6 +41,17 @@ end return function() describe("World", function() + itFOCUS("inspection", function() + local world = World.new() + local A, B = component(), component() + local entityId = world:spawnAt(3, A({ a = true }), B({ b = true })) + --world:_transitionArchetype(entityId, nil) + print("After spawn:", world:get(entityId, A, B)) + --world:_transitionArchetype(entityId, nil) + print("Removed value", world:remove(entityId, A)) + print("After remove", world:get(entityId, A, B, A, B)) + --local query = world:query(A, B) + end) it("should be iterable", function() local world = World.new() local A = component() diff --git a/testez-companion.toml b/testez-companion.toml new file mode 100644 index 00000000..9270d111 --- /dev/null +++ b/testez-companion.toml @@ -0,0 +1 @@ +roots = ["ReplicatedStorage/Matter"] From 1ba2008862830fcd0b1504eb228512d29d4575b0 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 14 Jul 2024 00:06:46 -0400 Subject: [PATCH 02/87] delete unused file --- lib/NewStorage.luau | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 lib/NewStorage.luau diff --git a/lib/NewStorage.luau b/lib/NewStorage.luau deleted file mode 100644 index 6887f0f7..00000000 --- a/lib/NewStorage.luau +++ /dev/null @@ -1,23 +0,0 @@ ---!strict -type EntityId = number -type ComponentInstance = { [any]: any } -type ComponentMetatable = { [any]: any } - -type ComponentId = ComponentMetatable -type ArchetypeId = number ---type Archetype = { id: number, componentMetatables: { ComponentId } } - -type ContiguousEntityId = number -type Archetype = { - id: number, - componentMetatables: { ComponentId }, - - -- component ids are contiguous by nature - components: { [ComponentId]: { [ContiguousEntityId]: ComponentInstance } }, -} - --- find archetypes containing component -type ComponentIndex = { [ComponentId]: { ArchetypeId } } - --- find archetype for entity -type EntityIndex = { [EntityId]: Archetype } From 208a7a96b11b0b42de72f59fec2d3156438d8155 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 14 Jul 2024 23:17:17 -0400 Subject: [PATCH 03/87] assign an id to components --- lib/World.luau | 103 ++++++++++++++++++++++++++------------------ lib/World.spec.luau | 4 +- 2 files changed, 63 insertions(+), 44 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 6f22063e..9688df40 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -1,5 +1,6 @@ local Component = require(script.Parent.component) local archetypeModule = require(script.Parent.archetype) +local entityInspect = require(script.Parent.debugger.widgets.entityInspect) local topoRuntime = require(script.Parent.topoRuntime) local assertValidComponentInstance = Component.assertValidComponentInstance @@ -15,14 +16,14 @@ type Component = { [any]: any } type ComponentInstance = { [any]: any } type ComponentMetatable = { [any]: any } type ComponentMetatables = { ComponentMetatable } -type ComponentId = ComponentMetatable +type ComponentToId = { [Component]: number } type ArchetypeId = string type Archetype = { id: number, - componentMetatables: ComponentMetatables, - components: { [ComponentId]: { [DenseEntityId]: ComponentInstance } }, + componentToStorageIndex: { [ComponentMetatable]: number }, + storage: { [number]: { [DenseEntityId]: ComponentInstance } }, } type EntityRecord = { @@ -61,9 +62,9 @@ function World.new() local firstStorage: Storage = {} return setmetatable({ - _entityIndex = {} :: EntityIndex, - _componentIndex = {} :: ComponentIndex, - _archetypes = {} :: Archetypes, + entityIndex = {} :: EntityIndex, + componentIndex = {} :: ComponentIndex, + archetypes = {} :: Archetypes, -- List of maps from archetype string --> entity ID --> entity data _storages = { firstStorage } :: Storages, @@ -96,6 +97,8 @@ function World.new() }, World) end +export type World = typeof(World.new()) + -- Searches all archetype storages for the entity with the given archetype -- Returns the storage that the entity is in if it exists, otherwise nil function World:_getStorageWithEntity(archetype, id) @@ -247,13 +250,33 @@ function World:_updateQueryCache(entityArchetype) end end +local function createArchetype(world: World, components: { Component }): Archetype + local componentToStorageIndex = {} + local length = #components + local storage = table.create(length) + for index, component in components do + componentToStorageIndex[component] = index + storage[index] = {} + end + + local archetypeId = archetypeOf(components) + local archetype: Archetype = { + id = archetypeId, + componentToStorageIndex = componentToStorageIndex, + storage = storage, + } + + world.archetypes[archetypeId] = archetype + return archetype +end + function World._transitionArchetype(self: typeof(World.new()), id: EntityId, metatableToInstance: { [any]: any }?) print("transitionArchetype", id, metatableToInstance) debug.profilebegin("transitionArchetype") if metatableToInstance == nil then -- Remove all components - local entityRecord = self._entityIndex[id] + local entityRecord = self.entityIndex[id] assert(entityRecord, "..") for metatable in entityRecord.archetype.components do @@ -265,37 +288,29 @@ function World._transitionArchetype(self: typeof(World.new()), id: EntityId, met entityRecord.archetype = nil entityRecord.indexInArchetype = nil else - local metatables = {} - for metatable in metatableToInstance do - table.insert(metatables, metatable) + local components = {} + for component in metatableToInstance do + table.insert(components, component) end - local entityRecord = self._entityIndex[id] + local entityRecord = self.entityIndex[id] if entityRecord == nil then entityRecord = {} - self._entityIndex[id] = entityRecord + self.entityIndex[id] = entityRecord end -- Find the archetype that matches these components - local newArchetypeId = archetypeOf(unpack(metatables)) - local newArchetype = self._archetypes[newArchetypeId] - if newArchetype == nil then - local archetypeComponents = {} - for metatable, _ in metatableToInstance do - archetypeComponents[metatable] = {} - end + local newArchetypeId = archetypeOf(unpack(components)) + local newArchetype = self.archetypes[newArchetypeId] or createArchetype(self, components) - newArchetype = { - components = archetypeComponents, - } :: Archetype - - self._archetypes[newArchetypeId] = newArchetype - end + print("Archetype:", newArchetype) + print(newArchetype.componentToStorageIndex[components[1]]) + print(newArchetype.storage, newArchetype.component) -- Add entity to archetype - local indexInArchetype = #newArchetype.components[metatables[1]] + 1 - for componentMetatable, componentInstance in metatableToInstance do - newArchetype.components[componentMetatable][indexInArchetype] = componentInstance + local indexInArchetype = #newArchetype.storage[newArchetype.componentToStorageIndex[components[1]]] + 1 + for component, componentInstance in metatableToInstance do + newArchetype.storage[newArchetype.componentToStorageIndex[component]][indexInArchetype] = componentInstance end entityRecord.indexInArchetype = indexInArchetype @@ -393,7 +408,7 @@ end @return bool -- `true` if the entity exists ]=] function World.contains(self: typeof(World.new()), id) - return self._entityIndex[id] ~= nil + return self.entityIndex[id] ~= nil end --[=[ @@ -404,30 +419,32 @@ end @return ... -- Returns the component values in the same order they were passed in ]=] function World.get(self: typeof(World.new()), id, ...: Component) - local entityRecord = self._entityIndex[id] + local entityRecord = self.entityIndex[id] if entityRecord == nil then error(ERROR_NO_ENTITY, 2) end + local length = select("#", ...) + local componentInstances = table.create(length, nil) + local archetype = entityRecord.archetype - print("Get archetype:", archetype) - -- TODO: - -- handle nil archetype + if archetype == nil then + return componentInstances + end - local length = select("#", ...) - local componentInstances = {} + local componentToStorageIndex = archetype.componentToStorageIndex for i = 1, length do local component = select(i, ...) assertValidComponent(component, i) -- Does this component belong to the archetype that this entity is in? - if archetype.components[component] == nil then - -- No, so it doesn't have the value - table.insert(componentInstances, nil) - else - -- Yes - table.insert(componentInstances, archetype.components[component][entityRecord.indexInArchetype]) + local storageIndex = componentToStorageIndex[component] + if storageIndex == nil then + continue end + + -- Yes + componentInstances[i] = archetype.storage[storageIndex][entityRecord.indexInArchetype] end return unpack(componentInstances, 1, length) @@ -1114,7 +1131,9 @@ end @return ...ComponentInstance -- Returns the component instance values that were removed in the order they were passed. ]=] function World.remove(self: typeof(World.new()), id, ...: ComponentMetatable) - local entityRecord = self._entityIndex[id] + error("unimplemented") + + local entityRecord = self.entityIndex[id] if entityRecord == nil then error(ERROR_NO_ENTITY, 2) end diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 0babc7b1..ab128729 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -48,8 +48,8 @@ return function() --world:_transitionArchetype(entityId, nil) print("After spawn:", world:get(entityId, A, B)) --world:_transitionArchetype(entityId, nil) - print("Removed value", world:remove(entityId, A)) - print("After remove", world:get(entityId, A, B, A, B)) + -- print("Removed value", world:remove(entityId, A)) + -- print("After remove", world:get(entityId, A, B, A, B)) --local query = world:query(A, B) end) it("should be iterable", function() From 141f6d4f13a9df156da51d3eb04f31dd183cecb3 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 15 Jul 2024 00:14:57 -0400 Subject: [PATCH 04/87] start query --- lib/World.luau | 194 +++++++++++++++++++++++++++++++------------- lib/World.spec.luau | 4 + lib/component.luau | 6 ++ 3 files changed, 149 insertions(+), 55 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 9688df40..14494bf9 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -11,6 +11,7 @@ local areArchetypesCompatible = archetypeModule.areArchetypesCompatible type EntityId = number type DenseEntityId = number +type GlobalComponentId = number type Component = { [any]: any } type ComponentInstance = { [any]: any } @@ -35,7 +36,7 @@ type EntityRecord = { type EntityIndex = { [EntityId]: EntityRecord? } -- find archetypes containing component -type ComponentIndex = { [ComponentId]: { [ArchetypeId]: number } } +type ComponentIndex = { [GlobalComponentId]: { [ArchetypeId]: number } } -- find archetype from all components type Archetypes = { [ArchetypeId]: Archetype } @@ -251,21 +252,26 @@ function World:_updateQueryCache(entityArchetype) end local function createArchetype(world: World, components: { Component }): Archetype + local archetypeId = archetypeOf(components) local componentToStorageIndex = {} local length = #components local storage = table.create(length) - for index, component in components do - componentToStorageIndex[component] = index - storage[index] = {} - end - - local archetypeId = archetypeOf(components) local archetype: Archetype = { id = archetypeId, componentToStorageIndex = componentToStorageIndex, storage = storage, } + for index, component in components do + local globalComponentId = #component + local associatedArchetypes = world.componentIndex[globalComponentId] or {} + associatedArchetypes[archetypeId] = index + world.componentIndex[globalComponentId] = associatedArchetypes + + componentToStorageIndex[component] = index + storage[index] = {} + end + world.archetypes[archetypeId] = archetype return archetype end @@ -303,9 +309,9 @@ function World._transitionArchetype(self: typeof(World.new()), id: EntityId, met local newArchetypeId = archetypeOf(unpack(components)) local newArchetype = self.archetypes[newArchetypeId] or createArchetype(self, components) - print("Archetype:", newArchetype) - print(newArchetype.componentToStorageIndex[components[1]]) - print(newArchetype.storage, newArchetype.component) + -- print("Archetype:", newArchetype) + -- print(newArchetype.componentToStorageIndex[components[1]]) + -- print(newArchetype.storage, newArchetype.component) -- Add entity to archetype local indexInArchetype = #newArchetype.storage[newArchetype.componentToStorageIndex[components[1]]] + 1 @@ -848,70 +854,148 @@ end @return QueryResult -- See [QueryResult](/api/QueryResult) docs. ]=] -function World:query(...) - debug.profilebegin("World:query") +function World.query(self: World, ...) + -- TODO: + -- cache queries assertValidComponent((...), 1) - local metatables = { ... } + local components = { ... } local queryLength = select("#", ...) - local archetype = archetypeOf(...) + local archetypeId = archetypeOf(...) + local archetypesToSearch + for _, component in components do + local associatedArchetypes = self.componentIndex[#component] + if associatedArchetypes == nil then + error("No-op query unimplemented") + end - if self._queryCache[archetype] == nil then - self:_newQueryArchetype(archetype) + if archetypesToSearch == nil or #archetypesToSearch > #associatedArchetypes then + archetypesToSearch = associatedArchetypes + end end - local compatibleArchetypes = self._queryCache[archetype] - - debug.profileend() - - if next(compatibleArchetypes) == nil then - -- If there are no compatible storages avoid creating our complicated iterator - return noopQuery - end + -- TODO: + -- Narrow compatible archetypes + print("Archetypes to search:", archetypesToSearch) + -- for archetypeId in archetypesToSearch do + -- local compatibleArchetype = self.archetypes[archetypeId] + -- for index, + -- end + local currentArchetype = self.archetypes[next(archetypesToSearch)] + local currentEntity = 0 + local function iter() end + + local function nextEntity() + currentEntity += 1 + if currentArchetype.storage[1][currentEntity] == nil then + currentArchetype = self.archetypes[next(archetypesToSearch, currentArchetype.id)] + currentEntity = 0 + if currentArchetype == nil then + print("Out Of Entities") + return nil + end - local queryOutput = table.create(queryLength) + return nextEntity() + end - local function expand(entityId, entityData) - if not entityId then - return + -- print( + -- "next found:", + -- currentArchetype.componentToStorageIndex[components[1]] == 1, + -- currentArchetype.storage[currentArchetype.componentToStorageIndex[components[1]]][currentEntity] + -- ) + + local entityId: number + for eId, record in self.entityIndex do + if record.archetype == currentArchetype and record.indexInArchetype == currentEntity then + entityId = eId + break + end end + print("Called next on archetype..", currentArchetype) + local storage, componentToStorageIndex = currentArchetype.storage, currentArchetype.componentToStorageIndex if queryLength == 1 then - return entityId, entityData[metatables[1]] + return entityId, storage[componentToStorageIndex[components[1]]][currentEntity] elseif queryLength == 2 then - return entityId, entityData[metatables[1]], entityData[metatables[2]] - elseif queryLength == 3 then - return entityId, entityData[metatables[1]], entityData[metatables[2]], entityData[metatables[3]] - elseif queryLength == 4 then - return entityId, - entityData[metatables[1]], - entityData[metatables[2]], - entityData[metatables[3]], - entityData[metatables[4]] - elseif queryLength == 5 then return entityId, - entityData[metatables[1]], - entityData[metatables[2]], - entityData[metatables[3]], - entityData[metatables[4]], - entityData[metatables[5]] - end - - for i, metatable in ipairs(metatables) do - queryOutput[i] = entityData[metatable] + storage[componentToStorageIndex[components[1]]][currentEntity], + storage[componentToStorageIndex[components[2]]][currentEntity] + else + error("Unimplemented Query Length") end - - return entityId, unpack(queryOutput, 1, queryLength) end - if self._pristineStorage == self._storages[1] then - self:_markStorageDirty() - end - - return QueryResult.new(self, expand, archetype, compatibleArchetypes, metatables) + return { + __iter = iter, + next = nextEntity, + } end +-- function World:query(...) +-- debug.profilebegin("World:query") +-- assertValidComponent((...), 1) + +-- local metatables = { ... } +-- local queryLength = select("#", ...) + +-- local archetype = archetypeOf(...) + +-- if self._queryCache[archetype] == nil then +-- self:_newQueryArchetype(archetype) +-- end + +-- local compatibleArchetypes = self._queryCache[archetype] + +-- debug.profileend() + +-- if next(compatibleArchetypes) == nil then +-- -- If there are no compatible storages avoid creating our complicated iterator +-- return noopQuery +-- end + +-- local queryOutput = table.create(queryLength) + +-- local function expand(entityId, entityData) +-- if not entityId then +-- return +-- end + +-- if queryLength == 1 then +-- return entityId, entityData[metatables[1]] +-- elseif queryLength == 2 then +-- return entityId, entityData[metatables[1]], entityData[metatables[2]] +-- elseif queryLength == 3 then +-- return entityId, entityData[metatables[1]], entityData[metatables[2]], entityData[metatables[3]] +-- elseif queryLength == 4 then +-- return entityId, +-- entityData[metatables[1]], +-- entityData[metatables[2]], +-- entityData[metatables[3]], +-- entityData[metatables[4]] +-- elseif queryLength == 5 then +-- return entityId, +-- entityData[metatables[1]], +-- entityData[metatables[2]], +-- entityData[metatables[3]], +-- entityData[metatables[4]], +-- entityData[metatables[5]] +-- end + +-- for i, metatable in ipairs(metatables) do +-- queryOutput[i] = entityData[metatable] +-- end + +-- return entityId, unpack(queryOutput, 1, queryLength) +-- end + +-- if self._pristineStorage == self._storages[1] then +-- self:_markStorageDirty() +-- end + +-- return QueryResult.new(self, expand, archetype, compatibleArchetypes, metatables) +-- end + local function cleanupQueryChanged(hookState) local world = hookState.world local componentToTrack = hookState.componentToTrack diff --git a/lib/World.spec.luau b/lib/World.spec.luau index ab128729..62db9529 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -45,8 +45,12 @@ return function() local world = World.new() local A, B = component(), component() local entityId = world:spawnAt(3, A({ a = true }), B({ b = true })) + world:spawnAt(5, A({ a = true })) --world:_transitionArchetype(entityId, nil) print("After spawn:", world:get(entityId, A, B)) + local query = world:query(A, B) + print(query:next()) + print(query:next()) --world:_transitionArchetype(entityId, nil) -- print("Removed value", world:remove(entityId, A)) -- print("After remove", world:get(entityId, A, B, A, B)) diff --git a/lib/component.luau b/lib/component.luau index 5e9a61da..e2e87ab2 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 = {} +local lastId = 0 local function newComponent(name, defaultData) name = name or debug.info(2, "s") .. "@" .. debug.info(2, "l") @@ -102,6 +103,8 @@ local function newComponent(name, defaultData) return patch end + lastId += 1 + local id = lastId setmetatable(component, { __call = function(_, ...) return component.new(...) @@ -109,6 +112,9 @@ local function newComponent(name, defaultData) __tostring = function() return name end, + __len = function() + return id + end, [DIAGNOSTIC_COMPONENT_MARKER] = true, }) From 87459d8ef479a95b6541aa8a3bcc1194b352cf8b Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 15 Jul 2024 13:27:08 -0400 Subject: [PATCH 05/87] start keeping track of owned entities --- PinnedMatter.rbxm | Bin 0 -> 63606 bytes bench.project.json | 29 +++++++++++ benchmarks/query.bench.luau | 51 +++++++++++++++++++ lib/World.luau | 95 ++++++++++++++++++++---------------- lib/World.spec.luau | 11 +++-- 5 files changed, 138 insertions(+), 48 deletions(-) create mode 100644 PinnedMatter.rbxm create mode 100644 bench.project.json create mode 100644 benchmarks/query.bench.luau diff --git a/PinnedMatter.rbxm b/PinnedMatter.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..892d5cf147feb214cf43907bd85a877c7ad10cd5 GIT binary patch literal 63606 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 entity ID --> entity data - _storages = { firstStorage } :: Storages, + _storages = { firstStorage }, -- The most recent storage that has not been dirtied by an iterator _pristineStorage = firstStorage, @@ -251,7 +249,7 @@ function World:_updateQueryCache(entityArchetype) end end -local function createArchetype(world: World, components: { Component }): Archetype +local function createArchetype(world: World, components: { ComponentId }): Archetype local archetypeId = archetypeOf(components) local componentToStorageIndex = {} local length = #components @@ -265,9 +263,8 @@ local function createArchetype(world: World, components: { Component }): Archety ownedEntities = {}, } - for index, component in components do - local componentId = #component - local associatedArchetypes = world.componentIndex[componentId] or {} + for index, componentId in components do + local associatedArchetypes = world.componentIndex[componentId] or ({} :: any) associatedArchetypes[archetypeId] = index world.componentIndex[componentId] = associatedArchetypes @@ -279,30 +276,42 @@ local function createArchetype(world: World, components: { Component }): Archety return archetype end -function World._transitionArchetype(self: typeof(World.new()), id: EntityId, metatableToInstance: { [any]: any }?) - --print("transitionArchetype", id, metatableToInstance) +function World._transitionArchetype( + self: typeof(World.new()), + id: EntityId, + myComponents: { [Component]: ComponentInstance }? +) debug.profilebegin("transitionArchetype") - - if metatableToInstance == nil then + if myComponents == nil then -- Remove all components local entityRecord = self.entityIndex[id] - assert(entityRecord, "..") + if entityRecord == nil then + warn("Tried to transitionArchetype a dead entity") + return + end - for metatable in entityRecord.archetype.components do + local archetype = entityRecord.archetype + if archetype == nil then + warn("Already have no archetype") + return + end + + for _, storageIndex in archetype.componentToStorageIndex do -- TODO: -- swap remove - table.remove(entityRecord.archetype.components[metatable], entityRecord.indexInArchetype) + table.remove(archetype.storage[storageIndex], entityRecord.indexInArchetype) end entityRecord.archetype = nil entityRecord.indexInArchetype = nil -- TODO: - -- Remove entity from owned entities + -- This is slow (and unsafe) + table.remove(archetype.ownedEntities, table.find(archetype.ownedEntities, id)) else - local components = {} - for component in metatableToInstance do - table.insert(components, component) + local componentIds: { ComponentId } = {} + for component in myComponents do + table.insert(componentIds, #component) end local entityRecord = self.entityIndex[id] @@ -310,18 +319,15 @@ function World._transitionArchetype(self: typeof(World.new()), id: EntityId, met entityRecord = {} self.entityIndex[id] = entityRecord end + assert(entityRecord, "Make typechecker happy") -- Find the archetype that matches these components - local newArchetypeId = archetypeOf(components) - local newArchetype = self.archetypes[newArchetypeId] or createArchetype(self, components) - - -- print("Archetype:", newArchetype) - -- print(newArchetype.componentToStorageIndex[components[1]]) - -- print(newArchetype.storage, newArchetype.component) + local newArchetypeId = archetypeOf(componentIds) + local newArchetype = self.archetypes[newArchetypeId] or createArchetype(self, componentIds) -- Add entity to archetype - local indexInArchetype = #newArchetype.storage[newArchetype.componentToStorageIndex[#components[1]]] + 1 - for component, componentInstance in metatableToInstance do + local indexInArchetype = #newArchetype.storage[newArchetype.componentToStorageIndex[componentIds[1]]] + 1 + for component, componentInstance in myComponents do newArchetype.storage[newArchetype.componentToStorageIndex[#component]][indexInArchetype] = componentInstance end diff --git a/lib/archetype.luau b/lib/archetype.luau index 11878e58..61cd9c03 100644 --- a/lib/archetype.luau +++ b/lib/archetype.luau @@ -16,14 +16,15 @@ local function getValueId(value) return valueId end -function archetypeOf(components) - local archetype = "" - for _, component in components do - archetype = archetype .. "_" .. #component - end +function archetypeOf(componentIds: { number }) + return table.concat(componentIds, "_") + -- local archetype = "" + -- for _, component in componentIds do + -- archetype = archetype .. "_" .. componentId + -- end - --print("Archetype for", components, archetype) - return archetype + -- --print("Archetype for", components, archetype) + -- return archetype -- debug.profilebegin("archetypeOf") -- local length = select("#", ...) From e85f5d898378ec8552bd2b35de9643870ccbeedf Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 15 Jul 2024 23:24:53 -0400 Subject: [PATCH 11/87] add spawn, despawn, and insert commands --- lib/Loop.luau | 11 ++- lib/World.luau | 217 ++++++++++++++++++++++++++---------------- lib/World.spec.luau | 28 ++++++ lib/init.luau | 2 +- testez-companion.toml | 1 + 5 files changed, 174 insertions(+), 85 deletions(-) create mode 100644 testez-companion.toml diff --git a/lib/Loop.luau b/lib/Loop.luau index 4534d63b..6e52fb4b 100644 --- a/lib/Loop.luau +++ b/lib/Loop.luau @@ -300,9 +300,7 @@ function Loop:_sortSystems() error( `Unable to schedule "{systemName(system)}" because the system "{systemName(dependency)}" is not scheduled.\n\nEither schedule "{systemName( dependency - )}" before "{systemName( - system - )}" or consider scheduling these systems together with Loop:scheduleSystems` + )}" before "{systemName(system)}" or consider scheduling these systems together with Loop:scheduleSystems` ) end end @@ -369,6 +367,10 @@ function Loop:begin(events) local dirtyWorlds: { [any]: true } = {} local profiling = self.profiling + -- TODO: + -- Technically, this doesn't have to be the world. + local primaryWorld = self._state[1] + for _, system in ipairs(self._orderedSystemsByEvent[eventName]) do topoRuntime.start({ system = self._systemState[system], @@ -392,6 +394,7 @@ function Loop:begin(events) local thread = coroutine.create(fn) + primaryWorld:deferCommands() local startTime = os.clock() local success, errorValue = coroutine.resume(thread, unpack(self._state, 1, self._stateLength)) @@ -417,6 +420,8 @@ function Loop:begin(events) ) end + primaryWorld:commitCommands() + for world in dirtyWorlds do world:optimizeQueries() end diff --git a/lib/World.luau b/lib/World.luau index 6ad7447c..0e8da4ec 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -10,6 +10,19 @@ local areArchetypesCompatible = archetypeModule.areArchetypesCompatible local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" +type DebugContext = { [any]: any } + +type SpawnCommand = { type: "spawn", debugContext: DebugContext?, entityId: number } +type DespawnCommand = { type: "despawn", debugContext: DebugContext?, entityId: number } +type InsertCommand = { + type: "insert", + debugContext: DebugContext?, + entityId: number, + componentInstances: { [any]: any }, + [string]: nil, +} +type Command = SpawnCommand | DespawnCommand | InsertCommand + --[=[ @class World @@ -29,6 +42,11 @@ function World.new() return setmetatable({ -- List of maps from archetype string --> entity ID --> entity data _storages = { firstStorage }, + + deferring = false, + commands = {} :: { Command }, + debugContext = nil :: DebugContext?, + -- The most recent storage that has not been dirtied by an iterator _pristineStorage = firstStorage, @@ -57,6 +75,8 @@ function World.new() }, World) end +export type World = typeof(World.new()) + -- Searches all archetype storages for the entity with the given archetype -- Returns the storage that the entity is in if it exists, otherwise nil function World:_getStorageWithEntity(archetype, id) @@ -121,27 +141,10 @@ function World:__iter() return World._next, self end ---[=[ - Spawns a new entity in the world with the given components. - - @param ... ComponentInstance -- The component values to spawn the entity with. - @return number -- The new entity ID. -]=] -function World:spawn(...) - return self:spawnAt(self._nextId, ...) -end - ---[=[ - Spawns a new entity in the world with a specific entity ID and given components. - - The next ID generated from [World:spawn] will be increased as needed to never collide with a manually specified ID. - - @param id number -- The entity ID to spawn with - @param ... ComponentInstance -- The component values to spawn the entity with. - @return number -- The same entity ID that was passed in -]=] -function World:spawnAt(id, ...) - if self:contains(id) then +local function executeSpawn(world: World, spawnCommand: SpawnCommand) + print("Execute spawn", spawnCommand) + local id = spawnCommand.entityId + if world:contains(id) then error( string.format( "The world already contains an entity with ID %d. Use World:replace instead if this is intentional.", @@ -151,36 +154,129 @@ function World:spawnAt(id, ...) ) end - self._size += 1 + world._entityMetatablesCache[id] = {} + world:_transitionArchetype(id, {}) +end + +local function executeDespawn(world: World, despawnCommand: DespawnCommand) + local id = despawnCommand.entityId + local entity = world:_getEntity(id) - if id >= self._nextId then - self._nextId = id + 1 + for metatable, component in pairs(entity) do + world:_trackChanged(metatable, id, component, nil) end - local components = {} - local metatables = {} + world._entityMetatablesCache[id] = nil + world:_transitionArchetype(id, nil) - for i = 1, select("#", ...) do - local newComponent = select(i, ...) + world._size -= 1 +end - assertValidComponentInstance(newComponent, i) +local function executeInsert(world: World, insertCommand: InsertCommand) + print("Execute insert", insertCommand) - local metatable = getmetatable(newComponent) + debug.profilebegin("insert") + local id = insertCommand.entityId + if not world:contains(id) then + error(ERROR_NO_ENTITY, 2) + end - if components[metatable] then - error(("Duplicate component type at index %d"):format(i), 2) + local entity = world:_getEntity(id) + local wasNew = false + for i, componentInstance in insertCommand.componentInstances do + assertValidComponentInstance(componentInstance, i) + + local metatable = getmetatable(componentInstance) + local oldComponent = entity[metatable] + + if not oldComponent then + wasNew = true + table.insert(world._entityMetatablesCache[id], metatable) end - self:_trackChanged(metatable, id, nil, newComponent) + world:_trackChanged(metatable, id, oldComponent, componentInstance) + entity[metatable] = componentInstance + end - components[metatable] = newComponent - table.insert(metatables, metatable) + if wasNew then + world:_transitionArchetype(id, entity) end - self._entityMetatablesCache[id] = metatables + debug.profileend() +end - self:_transitionArchetype(id, components) +local function processCommand(world: World, command: Command) + if command.type == "spawn" then + executeSpawn(world, command) + elseif command.type == "insert" then + executeInsert(world, command) + elseif command.type == "despawn" then + executeDespawn(world, command) + else + error(`Unknown command type: {command.type}`) + end +end + +local function bufferCommand(world: World, command: Command) + if world.deferring then + -- Attach current debug context because we don't know when this command will be processed. + (command :: any).debugContext = world.debugContext + table.insert(world.commands, command) + else + processCommand(world, command) + end +end + +function World:setDebugContext(debugContext: DebugContext?) + self.debugContext = debugContext +end + +function World:deferCommands() + if self.deferring then + return + end + self.deferring = true +end + +function World:commitCommands() + if not self.deferring then + return + end + + for _, command in self.commands do + processCommand(self, command) + end + + self.deferring = false + self.commands = {} +end + +--[=[ + Spawns a new entity in the world with the given components. + + @param ... ComponentInstance -- The component values to spawn the entity with. + @return number -- The new entity ID. +]=] +function World:spawn(...) + local entityId = self._nextId + self._nextId += 1 + + return self:spawnAt(entityId, ...) +end + +--[=[ + Spawns a new entity in the world with a specific entity ID and given components. + + The next ID generated from [World:spawn] will be increased as needed to never collide with a manually specified ID. + + @param id number -- The entity ID to spawn with + @param ... ComponentInstance -- The component values to spawn the entity with. + @return number -- The same entity ID that was passed in +]=] +function World:spawnAt(id, ...) + bufferCommand(self, { type = "spawn", entityId = id }) + bufferCommand(self, { type = "insert", entityId = id, componentInstances = { ... } }) return id end @@ -301,16 +397,7 @@ end @param id number -- The entity ID ]=] function World:despawn(id) - local entity = self:_getEntity(id) - - for metatable, component in pairs(entity) do - self:_trackChanged(metatable, id, component, nil) - end - - self._entityMetatablesCache[id] = nil - self:_transitionArchetype(id, nil) - - self._size -= 1 + bufferCommand(self, { type = "despawn", entityId = id }) end --[=[ @@ -1000,44 +1087,12 @@ end }) ) ``` - + @param id number -- The entity ID @param ... ComponentInstance -- The component values to insert ]=] function World:insert(id, ...) - debug.profilebegin("insert") - if not self:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - - local entity = self:_getEntity(id) - - local wasNew = false - for i = 1, select("#", ...) do - local newComponent = select(i, ...) - - assertValidComponentInstance(newComponent, i) - - local metatable = getmetatable(newComponent) - - local oldComponent = entity[metatable] - - if not oldComponent then - wasNew = true - - table.insert(self._entityMetatablesCache[id], metatable) - end - - self:_trackChanged(metatable, id, oldComponent, newComponent) - - entity[metatable] = newComponent - end - - if wasNew then -- wasNew - self:_transitionArchetype(id, entity) - end - - debug.profileend() + bufferCommand(self, { type = "insert", entityId = id, componentInstances = { ... } }) end --[=[ diff --git a/lib/World.spec.luau b/lib/World.spec.luau index e2e79ad3..61aee330 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -41,6 +41,34 @@ end return function() describe("World", function() + itFOCUS("should buffer commands if enabled", function() + local world = World.new() + world:deferCommands() + + local A = component() + local B = component() + world:spawn(A({ + a = 1, + })) + world:spawn(A({ + a = 2, + })) + + world:commitCommands() + world:deferCommands() + world:despawn(1) + expect(world:contains(1)).to.equal(true) + world:commitCommands() + expect(world:contains(1)).to.equal(false) + world:deferCommands() + + -- Insert + local b = B({}) + world:insert(2, b) + expect(world:get(2, B)).to.never.be.ok() + world:commitCommands() + expect(world:get(2, B)).to.equal(b) + end) it("should be iterable", function() local world = World.new() local A = component() diff --git a/lib/init.luau b/lib/init.luau index 9eb2b33d..644e9859 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -58,7 +58,7 @@ local Loop = require(script.Loop) local newComponent = require(script.component).newComponent local topoRuntime = require(script.topoRuntime) -export type World = typeof(World.new()) +export type World = World.World export type Loop = typeof(Loop.new()) return table.freeze({ diff --git a/testez-companion.toml b/testez-companion.toml new file mode 100644 index 00000000..119295fc --- /dev/null +++ b/testez-companion.toml @@ -0,0 +1 @@ +roots = ["ReplicatedStorage/Matter"] \ No newline at end of file From a3dfe88c66fe399124ff0fcf8e4ff98296653404 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 15 Jul 2024 23:31:45 -0400 Subject: [PATCH 12/87] start remove command --- lib/World.luau | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 0e8da4ec..8d1b5a43 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -12,6 +12,8 @@ local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if ne type DebugContext = { [any]: any } +-- I would want to use an intersection to attach debugContext & entityId, but +-- old solver is not great at resolving intersections w/ unions type SpawnCommand = { type: "spawn", debugContext: DebugContext?, entityId: number } type DespawnCommand = { type: "despawn", debugContext: DebugContext?, entityId: number } type InsertCommand = { @@ -19,9 +21,14 @@ type InsertCommand = { debugContext: DebugContext?, entityId: number, componentInstances: { [any]: any }, - [string]: nil, } -type Command = SpawnCommand | DespawnCommand | InsertCommand +type RemoveCommand = { + type: "remove", + DebugContext: DebugContext?, + entityId: number, + components: { [any]: any }, +} +type Command = SpawnCommand | DespawnCommand | InsertCommand | RemoveCommand --[=[ @class World @@ -1087,7 +1094,7 @@ end }) ) ``` - + @param id number -- The entity ID @param ... ComponentInstance -- The component values to insert ]=] From 7ecc77222bfb5ade0f8783d7021f7eb964ed708c Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Jul 2024 00:24:27 -0400 Subject: [PATCH 13/87] add remove command --- lib/World.luau | 73 ++++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 8d1b5a43..8c3a8a44 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -212,6 +212,35 @@ local function executeInsert(world: World, insertCommand: InsertCommand) debug.profileend() end +local function executeRemove(world: World, removeCommand: RemoveCommand) + local id = removeCommand.entityId + if not world:contains(id) then + error(ERROR_NO_ENTITY, 2) + end + + local entity = world:_getEntity(id) + + local removed = {} + for index, metatable in removeCommand.components do + assertValidComponent(metatable, index) + + local oldComponent = entity[metatable] + removed[index] = oldComponent + + world:_trackChanged(metatable, id, oldComponent, nil) + entity[metatable] = nil + end + + -- Rebuild entity metatable cache + local metatables = {} + for metatable in pairs(entity) do + table.insert(metatables, metatable) + end + + world._entityMetatablesCache[id] = metatables + world:_transitionArchetype(id, entity) +end + local function processCommand(world: World, command: Command) if command.type == "spawn" then executeSpawn(world, command) @@ -219,6 +248,8 @@ local function processCommand(world: World, command: Command) executeInsert(world, command) elseif command.type == "despawn" then executeDespawn(world, command) + elseif command.type == "remove" then + executeRemove(world, command) else error(`Unknown command type: {command.type}`) end @@ -282,6 +313,9 @@ end @return number -- The same entity ID that was passed in ]=] function World:spawnAt(id, ...) + -- TODO: + -- Spawn should probably insert at the same time, because spawn used to check that you + -- didn't pass any duplicate components bufferCommand(self, { type = "spawn", entityId = id }) bufferCommand(self, { type = "insert", entityId = id, componentInstances = { ... } }) return id @@ -1111,44 +1145,13 @@ end @param id number -- The entity ID @param ... Component -- The components to remove - @return ...ComponentInstance -- Returns the component instance values that were removed in the order they were passed. ]=] function World:remove(id, ...) - if not self:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - - local entity = self:_getEntity(id) - - local length = select("#", ...) - local removed = {} - - for i = 1, length do - local metatable = select(i, ...) - - assertValidComponent(metatable, i) - - local oldComponent = entity[metatable] - - removed[i] = oldComponent - - self:_trackChanged(metatable, id, oldComponent, nil) - - entity[metatable] = nil - end - - -- Rebuild entity metatable cache - local metatables = {} - - for metatable in pairs(entity) do - table.insert(metatables, metatable) - end - - self._entityMetatablesCache[id] = metatables - - self:_transitionArchetype(id, entity) + local components = { ... } + bufferCommand(self, { type = "remove", entityId = id, components = components }) - return unpack(removed, 1, length) + -- TODO: + -- Return current state of components you are removing end --[=[ From 194538841ff7390ca9a679512dd1d90bbfa86c90 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Jul 2024 12:55:38 -0400 Subject: [PATCH 14/87] get all world tests passing --- lib/World.luau | 29 ++++++++++++++++++++++++++--- lib/World.spec.luau | 4 ++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 8c3a8a44..8ca8ff1c 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -161,6 +161,7 @@ local function executeSpawn(world: World, spawnCommand: SpawnCommand) ) end + world._size += 1 world._entityMetatablesCache[id] = {} world:_transitionArchetype(id, {}) end @@ -316,6 +317,10 @@ function World:spawnAt(id, ...) -- TODO: -- Spawn should probably insert at the same time, because spawn used to check that you -- didn't pass any duplicate components + if id >= self._nextId then + self._nextId = id + 1 + end + bufferCommand(self, { type = "spawn", entityId = id }) bufferCommand(self, { type = "insert", entityId = id, componentInstances = { ... } }) return id @@ -1147,11 +1152,29 @@ end @param ... Component -- The components to remove ]=] function World:remove(id, ...) + if not self:contains(id) then + -- TODO: + -- What should we do? Should we error still? + return + end + + -- NOTE: + -- This functionality is deprecated and will be removed in a future release. + local entity = self:_getEntity(id) local components = { ... } - bufferCommand(self, { type = "remove", entityId = id, components = components }) + local length = #components + local removed = table.create(length, nil) + for index, component in components do + -- TODO: + -- Should this check also be here, or no? + assertValidComponent(component, index) + + local oldComponent = entity[component] + removed[index] = oldComponent + end - -- TODO: - -- Return current state of components you are removing + bufferCommand(self, { type = "remove", entityId = id, components = components }) + return unpack(removed, 1, length) end --[=[ diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 61aee330..c09e25ff 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -40,8 +40,8 @@ local function assertDeepEqual(a, b) end return function() - describe("World", function() - itFOCUS("should buffer commands if enabled", function() + describeFOCUS("World", function() + itSKIP("should buffer commands if enabled", function() local world = World.new() world:deferCommands() From 7986fd86a9c059c1e09e222927bd529e76725262 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Jul 2024 13:11:05 -0400 Subject: [PATCH 15/87] remove idea of multiple storages --- lib/World.luau | 154 +++++++++----------------------------------- lib/World.spec.luau | 4 +- 2 files changed, 35 insertions(+), 123 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 8ca8ff1c..b4f6dd3e 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -44,19 +44,14 @@ World.__index = World Creates a new World. ]=] function World.new() - local firstStorage = {} - return setmetatable({ - -- List of maps from archetype string --> entity ID --> entity data - _storages = { firstStorage }, + -- Map from archetype string --> entity ID --> entity data + storage = {}, deferring = false, commands = {} :: { Command }, debugContext = nil :: DebugContext?, - -- The most recent storage that has not been dirtied by an iterator - _pristineStorage = firstStorage, - -- Map from entity ID -> archetype string _entityArchetypes = {}, @@ -84,37 +79,9 @@ end export type World = typeof(World.new()) --- Searches all archetype storages for the entity with the given archetype --- Returns the storage that the entity is in if it exists, otherwise nil -function World:_getStorageWithEntity(archetype, id) - for _, storage in self._storages do - local archetypeStorage = storage[archetype] - if archetypeStorage then - if archetypeStorage[id] then - return storage - end - end - end - return nil -end - -function World:_markStorageDirty() - local newStorage = {} - table.insert(self._storages, newStorage) - self._pristineStorage = newStorage - - if topoRuntime.withinTopoContext() then - local frameState = topoRuntime.useFrameState() - - frameState.dirtyWorlds[self] = true - end -end - function World:_getEntity(id) local archetype = self._entityArchetypes[id] - local storage = self:_getStorageWithEntity(archetype, id) - - return storage[archetype][id] + return self.storage[archetype][id] end function World:_next(last) @@ -124,9 +91,7 @@ function World:_next(last) return nil end - local storage = self:_getStorageWithEntity(archetype, entityId) - - return entityId, storage[archetype][entityId] + return entityId, self.storage[archetype][entityId] end --[=[ @@ -149,7 +114,6 @@ function World:__iter() end local function executeSpawn(world: World, spawnCommand: SpawnCommand) - print("Execute spawn", spawnCommand) local id = spawnCommand.entityId if world:contains(id) then error( @@ -181,8 +145,6 @@ local function executeDespawn(world: World, despawnCommand: DespawnCommand) end local function executeInsert(world: World, insertCommand: InsertCommand) - print("Execute insert", insertCommand) - debug.profilebegin("insert") local id = insertCommand.entityId if not world:contains(id) then @@ -333,11 +295,9 @@ function World:_newQueryArchetype(queryArchetype) return -- Archetype isn't actually new end - for _, storage in self._storages do - for entityArchetype in storage do - if areArchetypesCompatible(queryArchetype, entityArchetype) then - self._queryCache[queryArchetype][entityArchetype] = true - end + for entityArchetype in self.storage do + if areArchetypesCompatible(queryArchetype, entityArchetype) then + self._queryCache[queryArchetype][entityArchetype] = true end end end @@ -357,7 +317,7 @@ function World:_transitionArchetype(id, components) local oldStorage if oldArchetype then - oldStorage = self:_getStorageWithEntity(oldArchetype, id) + oldStorage = self.storage if not components then oldStorage[oldArchetype][id] = nil @@ -372,8 +332,8 @@ function World:_transitionArchetype(id, components) oldStorage[oldArchetype][id] = nil end - if self._pristineStorage[newArchetype] == nil then - self._pristineStorage[newArchetype] = {} + if self.storage[newArchetype] == nil then + self.storage[newArchetype] = {} end if self._entityArchetypeCache[newArchetype] == nil then @@ -382,7 +342,7 @@ function World:_transitionArchetype(id, components) self:_updateQueryCache(newArchetype) debug.profileend() end - self._pristineStorage[newArchetype][id] = components + self.storage[newArchetype][id] = components else oldStorage[newArchetype][id] = components end @@ -454,9 +414,7 @@ end ::: ]=] function World:clear() - local firstStorage = {} - self._storages = { firstStorage } - self._pristineStorage = firstStorage + self.storage = {} self._entityArchetypes = {} self._entityMetatablesCache = {} self._size = 0 @@ -555,7 +513,6 @@ QueryResult.__index = QueryResult function QueryResult.new(world, expand, queryArchetype, compatibleArchetypes, metatables) return setmetatable({ world = world, - seenEntities = {}, currentCompatibleArchetype = next(compatibleArchetypes), compatibleArchetypes = compatibleArchetypes, storageIndex = 1, @@ -568,53 +525,32 @@ end local function nextItem(query) local world = query.world local currentCompatibleArchetype = query.currentCompatibleArchetype - local seenEntities = query.seenEntities local compatibleArchetypes = query.compatibleArchetypes local entityId, entityData - local storages = world._storages - repeat - local nextStorage = storages[query.storageIndex] - local currently = nextStorage[currentCompatibleArchetype] - if currently then - entityId, entityData = next(currently, query.lastEntityId) - end - - while entityId == nil do - currentCompatibleArchetype = next(compatibleArchetypes, currentCompatibleArchetype) - - if currentCompatibleArchetype == nil then - query.storageIndex += 1 - - nextStorage = storages[query.storageIndex] - - if nextStorage == nil or next(nextStorage) == nil then - return - end - - currentCompatibleArchetype = nil - - if world._pristineStorage == nextStorage then - world:_markStorageDirty() - end + local storage = world.storage + local currently = storage[currentCompatibleArchetype] + if currently then + entityId, entityData = next(currently, query.lastEntityId) + end - continue - elseif nextStorage[currentCompatibleArchetype] == nil then - continue - end + while entityId == nil do + currentCompatibleArchetype = next(compatibleArchetypes, currentCompatibleArchetype) - entityId, entityData = next(nextStorage[currentCompatibleArchetype]) + if currentCompatibleArchetype == nil then + return nil + elseif storage[currentCompatibleArchetype] == nil then + continue end - query.lastEntityId = entityId + entityId, entityData = next(storage[currentCompatibleArchetype]) + end - until seenEntities[entityId] == nil + query.lastEntityId = entityId query.currentCompatibleArchetype = currentCompatibleArchetype - seenEntities[entityId] = true - return entityId, entityData end @@ -959,9 +895,9 @@ function World:query(...) return entityId, unpack(queryOutput, 1, queryLength) end - if self._pristineStorage == self._storages[1] then - self:_markStorageDirty() - end + -- if self._pristineStorage == self._storages[1] then + -- self:_markStorageDirty() + -- end return QueryResult.new(self, expand, archetype, compatibleArchetypes, metatables) end @@ -1197,35 +1133,9 @@ end [World:query]. While inside a query, any changes to the World are stored in a separate location from the rest of the World. Calling this function combines the separate storage back into the main storage, which speeds things up again. -]=] -function World:optimizeQueries() - if #self._storages == 1 then - return - end - local firstStorage = self._storages[1] - - for i = 2, #self._storages do - local storage = self._storages[i] - - for archetype, entities in storage do - if firstStorage[archetype] == nil then - firstStorage[archetype] = entities - else - for entityId, entityData in entities do - if firstStorage[archetype][entityId] then - error("Entity ID already exists in first storage...") - end - firstStorage[archetype][entityId] = entityData - end - end - end - end - - table.clear(self._storages) - - self._storages[1] = firstStorage - self._pristineStorage = firstStorage -end + @deprecated v0.9 -- This method no longer does anything. +]=] +function World:optimizeQueries() end return World diff --git a/lib/World.spec.luau b/lib/World.spec.luau index c09e25ff..cd845a64 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -751,7 +751,7 @@ return function() expect(world:get(child, Transform).pos).to.equal(Vector2.new(4, 5)) end) - it("should not invalidate iterators", function() + it("should not invalidate iterators when deferring", function() local world = World.new() local A = component() local B = component() @@ -761,12 +761,14 @@ return function() world:spawn(A(), B()) end + world:deferCommands() local count = 0 for id in world:query(A) do count += 1 world:insert(id, C()) world:remove(id, B) end + world:commitCommands() expect(count).to.equal(10) end) end) From db171d58190a01a4582307ba4164563408d8a9b9 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Jul 2024 13:24:54 -0400 Subject: [PATCH 16/87] remove the idea of oldStorage from transitionArchetype --- lib/World.luau | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index b4f6dd3e..06c297b4 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -312,15 +312,13 @@ end function World:_transitionArchetype(id, components) debug.profilebegin("transitionArchetype") + local storage = self.storage local newArchetype = nil local oldArchetype = self._entityArchetypes[id] - local oldStorage if oldArchetype then - oldStorage = self.storage - if not components then - oldStorage[oldArchetype][id] = nil + storage[oldArchetype][id] = nil end end @@ -328,12 +326,14 @@ function World:_transitionArchetype(id, components) newArchetype = archetypeOf(unpack(self._entityMetatablesCache[id])) if oldArchetype ~= newArchetype then - if oldStorage then - oldStorage[oldArchetype][id] = nil + -- NOTE: + -- This seems... maybe wrong + if oldArchetype then + storage[oldArchetype][id] = nil end - if self.storage[newArchetype] == nil then - self.storage[newArchetype] = {} + if storage[newArchetype] == nil then + storage[newArchetype] = {} end if self._entityArchetypeCache[newArchetype] == nil then @@ -342,9 +342,10 @@ function World:_transitionArchetype(id, components) self:_updateQueryCache(newArchetype) debug.profileend() end - self.storage[newArchetype][id] = components + + storage[newArchetype][id] = components else - oldStorage[newArchetype][id] = components + storage[newArchetype][id] = components end end From 62068635f3685ade64e985631650345b4a6490c9 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Jul 2024 15:37:07 -0400 Subject: [PATCH 17/87] rename defer methods --- lib/Loop.luau | 9 +-------- lib/World.luau | 16 ++++++++++------ lib/World.spec.luau | 15 +++++++++------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/Loop.luau b/lib/Loop.luau index 6e52fb4b..4e96b84b 100644 --- a/lib/Loop.luau +++ b/lib/Loop.luau @@ -364,12 +364,12 @@ function Loop:begin(events) generation = not generation - local dirtyWorlds: { [any]: true } = {} local profiling = self.profiling -- TODO: -- Technically, this doesn't have to be the world. local primaryWorld = self._state[1] + primaryWorld:startDeferring() for _, system in ipairs(self._orderedSystemsByEvent[eventName]) do topoRuntime.start({ @@ -377,7 +377,6 @@ function Loop:begin(events) frame = { generation = generation, deltaTime = deltaTime, - dirtyWorlds = dirtyWorlds, logs = self._systemLogs[system], }, currentSystem = system, @@ -394,7 +393,6 @@ function Loop:begin(events) local thread = coroutine.create(fn) - primaryWorld:deferCommands() local startTime = os.clock() local success, errorValue = coroutine.resume(thread, unpack(self._state, 1, self._stateLength)) @@ -422,11 +420,6 @@ function Loop:begin(events) primaryWorld:commitCommands() - for world in dirtyWorlds do - world:optimizeQueries() - end - table.clear(dirtyWorlds) - if not success then if os.clock() - recentErrorLastTime > 10 then recentErrorLastTime = os.clock() diff --git a/lib/World.luau b/lib/World.luau index 06c297b4..b36caa0c 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -232,7 +232,7 @@ function World:setDebugContext(debugContext: DebugContext?) self.debugContext = debugContext end -function World:deferCommands() +function World:startDeferring() if self.deferring then return end @@ -241,16 +241,20 @@ function World:deferCommands() end function World:commitCommands() - if not self.deferring then - return - end - for _, command in self.commands do processCommand(self, command) end + table.clear(self.commands) +end + +function World:stopDeferring() + if not self.deferring then + return + end + + self:commitCommands() self.deferring = false - self.commands = {} end --[=[ diff --git a/lib/World.spec.luau b/lib/World.spec.luau index cd845a64..8d6d72e7 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -41,9 +41,9 @@ end return function() describeFOCUS("World", function() - itSKIP("should buffer commands if enabled", function() + itFOCUS("should buffer commands if enabled", function() local world = World.new() - world:deferCommands() + world:startDeferring() local A = component() local B = component() @@ -55,18 +55,21 @@ return function() })) world:commitCommands() - world:deferCommands() + world:despawn(1) expect(world:contains(1)).to.equal(true) + world:commitCommands() + expect(world:contains(1)).to.equal(false) - world:deferCommands() -- Insert local b = B({}) world:insert(2, b) expect(world:get(2, B)).to.never.be.ok() + world:commitCommands() + expect(world:get(2, B)).to.equal(b) end) it("should be iterable", function() @@ -761,14 +764,14 @@ return function() world:spawn(A(), B()) end - world:deferCommands() + world:startDeferring() local count = 0 for id in world:query(A) do count += 1 world:insert(id, C()) world:remove(id, B) end - world:commitCommands() + world:stopDeferring() expect(count).to.equal(10) end) end) From 18c9473751edc6b5b0247693b3908faa5e2a23be Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Jul 2024 15:46:18 -0400 Subject: [PATCH 18/87] fix world tests to account for command queue --- lib/World.luau | 1 + lib/World.spec.luau | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/World.luau b/lib/World.luau index b36caa0c..286b75ca 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -146,6 +146,7 @@ end local function executeInsert(world: World, insertCommand: InsertCommand) debug.profilebegin("insert") + local id = insertCommand.entityId if not world:contains(id) then error(ERROR_NO_ENTITY, 2) diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 8d6d72e7..616c4b10 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -41,7 +41,7 @@ end return function() describeFOCUS("World", function() - itFOCUS("should buffer commands if enabled", function() + it("should buffer commands if enabled", function() local world = World.new() world:startDeferring() @@ -348,6 +348,8 @@ return function() c = 3, })) + world:commitCommands() + defaultBindable:Fire() expect(runCount).to.equal(2) end) @@ -519,6 +521,8 @@ return function() C() ) + world:commitCommands() + defaultBindable:Fire() additionalQuery = nil @@ -544,15 +548,21 @@ return function() C() ) + world:commitCommands() + defaultBindable:Fire() defaultBindable:Fire() world:replace(secondEntityId, B()) + world:commitCommands() + infrequentBindable:Fire() world:despawn(entityId) + world:commitCommands() + defaultBindable:Fire() infrequentBindable:Fire() From 76a2a7c5a4d52f98061938cf13ccd064ab9740a3 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Jul 2024 15:51:02 -0400 Subject: [PATCH 19/87] make loop tests pass --- lib/Loop.luau | 19 +++++++++++++++---- lib/World.spec.luau | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/Loop.luau b/lib/Loop.luau index 4e96b84b..f6df1c13 100644 --- a/lib/Loop.luau +++ b/lib/Loop.luau @@ -1,4 +1,6 @@ local RunService = game:GetService("RunService") + +local World = require(script.Parent.World) local rollingAverage = require(script.Parent.rollingAverage) local topoRuntime = require(script.Parent.topoRuntime) @@ -367,9 +369,16 @@ function Loop:begin(events) local profiling = self.profiling -- TODO: - -- Technically, this doesn't have to be the world. - local primaryWorld = self._state[1] - primaryWorld:startDeferring() + -- Should probably handle other worlds. + -- Also, TBD if this is a good way to detect this, or if it should even handle this at all. + local primaryWorld: World.World? = if typeof(self._state[1]) == "table" + and getmetatable(self._state[1]) == World + then self._state[1] + else nil + + if primaryWorld then + primaryWorld:startDeferring() + end for _, system in ipairs(self._orderedSystemsByEvent[eventName]) do topoRuntime.start({ @@ -418,7 +427,9 @@ function Loop:begin(events) ) end - primaryWorld:commitCommands() + if primaryWorld then + primaryWorld:commitCommands() + end if not success then if os.clock() - recentErrorLastTime > 10 then diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 616c4b10..fb6781fe 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -40,7 +40,7 @@ local function assertDeepEqual(a, b) end return function() - describeFOCUS("World", function() + describe("World", function() it("should buffer commands if enabled", function() local world = World.new() world:startDeferring() From 0b5bec4bb17a544e6d93c52e2203f12277b02296 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Jul 2024 15:51:15 -0400 Subject: [PATCH 20/87] remove outdated loop test --- lib/Loop.spec.luau | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/lib/Loop.spec.luau b/lib/Loop.spec.luau index 37ad9c96..d714f7c9 100644 --- a/lib/Loop.spec.luau +++ b/lib/Loop.spec.luau @@ -609,27 +609,5 @@ return function() expect(called[2]).to.equal(2) expect(called[3]).to.equal(3) end) - - it("should optimize queries of worlds used inside it", function() - local world = World.new() - local loop = Loop.new(world) - - local A = component() - - world:spawn(A()) - - loop:scheduleSystem(function(world) - world:query(A) - end) - - local bindable = BindableEvent.new() - loop:begin({ - default = bindable.Event, - }) - - bindable:Fire() - - expect(#world._storages).to.equal(1) - end) end) end From 67f62c9e5b1ba6b3267c82108bddaf32c52f2133 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Jul 2024 15:53:00 -0400 Subject: [PATCH 21/87] fix ci --- lib/Loop.spec.luau | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Loop.spec.luau b/lib/Loop.spec.luau index d714f7c9..cde05c40 100644 --- a/lib/Loop.spec.luau +++ b/lib/Loop.spec.luau @@ -1,7 +1,5 @@ local Loop = require(script.Parent.Loop) local useHookState = require(script.Parent.topoRuntime).useHookState -local World = require(script.Parent.World) -local component = require(script.Parent).component local BindableEvent = require(script.Parent.mock.BindableEvent) local bindable = BindableEvent.new() From 1e1d74eff7724e840818e3d799b784e8ea2ad812 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Jul 2024 19:54:40 -0400 Subject: [PATCH 22/87] add docs --- lib/World.luau | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 286b75ca..2f8abf7b 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -24,10 +24,15 @@ type InsertCommand = { } type RemoveCommand = { type: "remove", - DebugContext: DebugContext?, + debugContext: DebugContext?, entityId: number, components: { [any]: any }, } +-- type ReplaceCommand = { +-- type: "replace", +-- debugContext: DebugContext?, +-- entityId: number +-- } type Command = SpawnCommand | DespawnCommand | InsertCommand | RemoveCommand --[=[ @@ -147,6 +152,7 @@ end local function executeInsert(world: World, insertCommand: InsertCommand) debug.profilebegin("insert") + print("Insert context", insertCommand) local id = insertCommand.entityId if not world:contains(id) then error(ERROR_NO_ENTITY, 2) @@ -233,6 +239,11 @@ function World:setDebugContext(debugContext: DebugContext?) self.debugContext = debugContext end +--[=[ + Starts deferring entity commands. + + If you are using a [`Loop`](/api/Loop), this is done for you. +]=] function World:startDeferring() if self.deferring then return @@ -241,6 +252,12 @@ function World:startDeferring() self.deferring = true end +--[=[ + Sequentially processes all of the commands in the buffer. + + If you are using a [`Loop`](/api/Loop), this is called after every system. + However, you can call it more often if you want. +]=] function World:commitCommands() for _, command in self.commands do processCommand(self, command) @@ -249,6 +266,9 @@ function World:commitCommands() table.clear(self.commands) end +--[=[ + Stops deferring entity commands and processes all commands left in the buffer. +]=] function World:stopDeferring() if not self.deferring then return @@ -283,7 +303,7 @@ end function World:spawnAt(id, ...) -- TODO: -- Spawn should probably insert at the same time, because spawn used to check that you - -- didn't pass any duplicate components + -- didn't pass any duplicate components. if id >= self._nextId then self._nextId = id + 1 end @@ -367,6 +387,9 @@ end @param ... ComponentInstance -- The component values to spawn the entity with. ]=] function World:replace(id, ...) + -- TODO: + -- Buffer this + if not self:contains(id) then error(ERROR_NO_ENTITY, 2) end @@ -1140,7 +1163,7 @@ end the World. Calling this function combines the separate storage back into the main storage, which speeds things up again. - @deprecated v0.9 -- This method no longer does anything. + @deprecated v0.9.0 -- This method no longer does anything. ]=] function World:optimizeQueries() end From 5d4f9ae94cc722493fcc6416b6ba66d1372c1d70 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Jul 2024 23:16:42 -0400 Subject: [PATCH 23/87] handle error in commit --- lib/Loop.luau | 30 +++++++++++++++++++++++------- lib/World.luau | 9 +++++++++ lib/World.spec.luau | 14 ++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/lib/Loop.luau b/lib/Loop.luau index f6df1c13..376a2728 100644 --- a/lib/Loop.luau +++ b/lib/Loop.luau @@ -398,9 +398,24 @@ function Loop:begin(events) end local fn = systemFn(system) - debug.profilebegin("system: " .. systemName(system)) + local name = systemName(system) + if primaryWorld then + primaryWorld:setDebugContext({ system = name }) + end - local thread = coroutine.create(fn) + debug.profilebegin("system: " .. name) + + local thread = coroutine.create(function() + fn() + + if primaryWorld then + primaryWorld:commitCommands() + -- local ok, err = pcall(primaryWorld.commitCommands, primaryWorld) + -- if not ok then + -- error(`Encountered an error while committing changes: {err}`) + -- end + end + end) local startTime = os.clock() local success, errorValue = coroutine.resume(thread, unpack(self._state, 1, self._stateLength)) @@ -427,9 +442,9 @@ function Loop:begin(events) ) end - if primaryWorld then - primaryWorld:commitCommands() - end + -- if primaryWorld then + -- primaryWorld:commitCommands() + -- end if not success then if os.clock() - recentErrorLastTime > 10 then @@ -437,9 +452,10 @@ function Loop:begin(events) recentErrors = {} end - local errorString = systemName(system) + local errorString = "System encountered an error: " + .. name .. ": " - .. tostring(errorValue) + .. string.gsub(tostring(errorValue), "%[.+%]:%d+: ", "") .. "\n" .. debug.traceback(thread) diff --git a/lib/World.luau b/lib/World.luau index 2f8abf7b..bdc47595 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -259,11 +259,20 @@ end However, you can call it more often if you want. ]=] function World:commitCommands() + --local errors for _, command in self.commands do processCommand(self, command) + -- if not ok then + -- if errors == nil then + -- errors = {} + -- end + + -- table.insert(errors, err) + -- end end table.clear(self.commands) + --return errors end --[=[ diff --git a/lib/World.spec.luau b/lib/World.spec.luau index fb6781fe..efb91a56 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -41,6 +41,20 @@ end return function() describe("World", function() + itFOCUS("debug context", function() + local world = World.new() + local loop = Loop.new(world) + local bindable = BindableEvent.new() + + loop:scheduleSystem(function() + --error("hi") + world:spawn({}) + end) + + loop:begin({ default = bindable.Event }) + bindable:Fire() + end) + it("should buffer commands if enabled", function() local world = World.new() world:startDeferring() From 9cc4523fb3deadf392e6d15338f8913c75851ef9 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 18 Jul 2024 16:34:56 -0400 Subject: [PATCH 24/87] add replace command --- lib/Loop.luau | 30 ++++++------- lib/Loop.spec.luau | 2 +- lib/World.luau | 102 ++++++++++++++++++++------------------------ lib/World.spec.luau | 9 ++-- 4 files changed, 66 insertions(+), 77 deletions(-) diff --git a/lib/Loop.luau b/lib/Loop.luau index 376a2728..a66eacdb 100644 --- a/lib/Loop.luau +++ b/lib/Loop.luau @@ -368,7 +368,7 @@ function Loop:begin(events) local profiling = self.profiling - -- TODO: + -- FIXME: -- Should probably handle other worlds. -- Also, TBD if this is a good way to detect this, or if it should even handle this at all. local primaryWorld: World.World? = if typeof(self._state[1]) == "table" @@ -376,6 +376,7 @@ function Loop:begin(events) then self._state[1] else nil + print("PrimaryWorld", primaryWorld) if primaryWorld then primaryWorld:startDeferring() end @@ -394,26 +395,24 @@ function Loop:begin(events) if profiling then profiling[system] = nil end + return end local fn = systemFn(system) local name = systemName(system) - if primaryWorld then - primaryWorld:setDebugContext({ system = name }) - end debug.profilebegin("system: " .. name) - + local commitFailed = false local thread = coroutine.create(function() fn() if primaryWorld then - primaryWorld:commitCommands() - -- local ok, err = pcall(primaryWorld.commitCommands, primaryWorld) - -- if not ok then - -- error(`Encountered an error while committing changes: {err}`) - -- end + local ok, err = pcall(primaryWorld.commitCommands, primaryWorld) + if not ok then + commitFailed = true + error(err) + end end end) @@ -442,20 +441,17 @@ function Loop:begin(events) ) end - -- if primaryWorld then - -- primaryWorld:commitCommands() - -- end - if not success then if os.clock() - recentErrorLastTime > 10 then recentErrorLastTime = os.clock() recentErrors = {} end - local errorString = "System encountered an error: " - .. name + local errorString = name .. ": " - .. string.gsub(tostring(errorValue), "%[.+%]:%d+: ", "") + .. (if commitFailed + then string.gsub(tostring(errorValue), "%[.+%]:%d+: ", "Failed to apply commands: ") + else errorValue) .. "\n" .. debug.traceback(thread) diff --git a/lib/Loop.spec.luau b/lib/Loop.spec.luau index cde05c40..4d9f8a00 100644 --- a/lib/Loop.spec.luau +++ b/lib/Loop.spec.luau @@ -71,7 +71,7 @@ return function() expect(counts[2]).to.equal(2) end) - it("should allow replacing systems", function() + itFOCUS("should allow replacing systems", function() local state = {} local loop = Loop.new(state) diff --git a/lib/World.luau b/lib/World.luau index bdc47595..309baab2 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -10,30 +10,26 @@ local areArchetypesCompatible = archetypeModule.areArchetypesCompatible local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" -type DebugContext = { [any]: any } - --- I would want to use an intersection to attach debugContext & entityId, but --- old solver is not great at resolving intersections w/ unions -type SpawnCommand = { type: "spawn", debugContext: DebugContext?, entityId: number } -type DespawnCommand = { type: "despawn", debugContext: DebugContext?, entityId: number } +-- The old solver is not great at resolving intersections, so we redefine entityId each time. +type SpawnCommand = { type: "spawn", entityId: number } +type DespawnCommand = { type: "despawn", entityId: number } type InsertCommand = { type: "insert", - debugContext: DebugContext?, entityId: number, componentInstances: { [any]: any }, } type RemoveCommand = { type: "remove", - debugContext: DebugContext?, entityId: number, components: { [any]: any }, } --- type ReplaceCommand = { --- type: "replace", --- debugContext: DebugContext?, --- entityId: number --- } -type Command = SpawnCommand | DespawnCommand | InsertCommand | RemoveCommand +type ReplaceCommand = { + type: "replace", + entityId: number, + components: { [any]: any }, +} + +type Command = SpawnCommand | DespawnCommand | InsertCommand | RemoveCommand | ReplaceCommand --[=[ @class World @@ -55,7 +51,6 @@ function World.new() deferring = false, commands = {} :: { Command }, - debugContext = nil :: DebugContext?, -- Map from entity ID -> archetype string _entityArchetypes = {}, @@ -152,7 +147,6 @@ end local function executeInsert(world: World, insertCommand: InsertCommand) debug.profilebegin("insert") - print("Insert context", insertCommand) local id = insertCommand.entityId if not world:contains(id) then error(ERROR_NO_ENTITY, 2) @@ -182,6 +176,40 @@ local function executeInsert(world: World, insertCommand: InsertCommand) debug.profileend() end +local function executeReplace(world: World, replaceCommand: ReplaceCommand) + local id = replaceCommand.entityId + if not world:contains(id) then + error(ERROR_NO_ENTITY, 2) + end + + local components = {} + local metatables = {} + local entity = world:_getEntity(id) + + for index, component in replaceCommand.components do + assertValidComponentInstance(component, index) + + local metatable = getmetatable(component) + if components[metatable] then + error(("Duplicate component type at index %d"):format(index), 2) + end + + world:_trackChanged(metatable, id, entity[metatable], component) + + components[metatable] = component + table.insert(metatables, metatable) + end + + for metatable, component in pairs(entity) do + if not components[metatable] then + world:_trackChanged(metatable, id, component, nil) + end + end + + world._entityMetatablesCache[id] = metatables + world:_transitionArchetype(id, components) +end + local function executeRemove(world: World, removeCommand: RemoveCommand) local id = removeCommand.entityId if not world:contains(id) then @@ -220,6 +248,8 @@ local function processCommand(world: World, command: Command) executeDespawn(world, command) elseif command.type == "remove" then executeRemove(world, command) + elseif command.type == "replace" then + executeReplace(world, command) else error(`Unknown command type: {command.type}`) end @@ -235,10 +265,6 @@ local function bufferCommand(world: World, command: Command) end end -function World:setDebugContext(debugContext: DebugContext?) - self.debugContext = debugContext -end - --[=[ Starts deferring entity commands. @@ -398,41 +424,7 @@ end function World:replace(id, ...) -- TODO: -- Buffer this - - if not self:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - - local components = {} - local metatables = {} - local entity = self:_getEntity(id) - - for i = 1, select("#", ...) do - local newComponent = select(i, ...) - - assertValidComponentInstance(newComponent, i) - - local metatable = getmetatable(newComponent) - - if components[metatable] then - error(("Duplicate component type at index %d"):format(i), 2) - end - - self:_trackChanged(metatable, id, entity[metatable], newComponent) - - components[metatable] = newComponent - table.insert(metatables, metatable) - end - - for metatable, component in pairs(entity) do - if not components[metatable] then - self:_trackChanged(metatable, id, component, nil) - end - end - - self._entityMetatablesCache[id] = metatables - - self:_transitionArchetype(id, components) + bufferCommand(self, { type = "replace", entityId = id, components = { ... } }) end --[=[ diff --git a/lib/World.spec.luau b/lib/World.spec.luau index efb91a56..d28addfe 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -41,16 +41,17 @@ end return function() describe("World", function() - itFOCUS("debug context", function() + itSKIP("debug context", function() local world = World.new() local loop = Loop.new(world) local bindable = BindableEvent.new() - loop:scheduleSystem(function() - --error("hi") + local function thisIsASystem() + -- error("hi") world:spawn({}) - end) + end + loop:scheduleSystem(thisIsASystem) loop:begin({ default = bindable.Event }) bindable:Fire() end) From 134aa839be9e1456731817bd85027646f1633423 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 18 Jul 2024 16:39:25 -0400 Subject: [PATCH 25/87] pass arguments to systems --- lib/Loop.luau | 7 +++---- lib/Loop.spec.luau | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/Loop.luau b/lib/Loop.luau index a66eacdb..321d656e 100644 --- a/lib/Loop.luau +++ b/lib/Loop.luau @@ -368,7 +368,7 @@ function Loop:begin(events) local profiling = self.profiling - -- FIXME: + -- TODO: -- Should probably handle other worlds. -- Also, TBD if this is a good way to detect this, or if it should even handle this at all. local primaryWorld: World.World? = if typeof(self._state[1]) == "table" @@ -376,7 +376,6 @@ function Loop:begin(events) then self._state[1] else nil - print("PrimaryWorld", primaryWorld) if primaryWorld then primaryWorld:startDeferring() end @@ -404,8 +403,8 @@ function Loop:begin(events) debug.profilebegin("system: " .. name) local commitFailed = false - local thread = coroutine.create(function() - fn() + local thread = coroutine.create(function(...) + fn(...) if primaryWorld then local ok, err = pcall(primaryWorld.commitCommands, primaryWorld) diff --git a/lib/Loop.spec.luau b/lib/Loop.spec.luau index 4d9f8a00..cde05c40 100644 --- a/lib/Loop.spec.luau +++ b/lib/Loop.spec.luau @@ -71,7 +71,7 @@ return function() expect(counts[2]).to.equal(2) end) - itFOCUS("should allow replacing systems", function() + it("should allow replacing systems", function() local state = {} local loop = Loop.new(state) From 8dc880350bd369f49767090adf124a627d664054 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 18 Jul 2024 16:40:14 -0400 Subject: [PATCH 26/87] remove old comments --- lib/World.luau | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 309baab2..484638a3 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -422,8 +422,6 @@ end @param ... ComponentInstance -- The component values to spawn the entity with. ]=] function World:replace(id, ...) - -- TODO: - -- Buffer this bufferCommand(self, { type = "replace", entityId = id, components = { ... } }) end From 8bb9749a3edaaccfdd9d85a5dc7d6332e36ba662 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 18 Jul 2024 16:43:10 -0400 Subject: [PATCH 27/87] polish --- lib/Loop.luau | 9 ++++++--- lib/World.spec.luau | 18 ++---------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/lib/Loop.luau b/lib/Loop.luau index 321d656e..ec25ab88 100644 --- a/lib/Loop.luau +++ b/lib/Loop.luau @@ -448,9 +448,12 @@ function Loop:begin(events) local errorString = name .. ": " - .. (if commitFailed - then string.gsub(tostring(errorValue), "%[.+%]:%d+: ", "Failed to apply commands: ") - else errorValue) + .. ( + if commitFailed + -- Strip irrelevant line numbers that point to Loop / World + then string.gsub(errorValue, "%[.+%]:%d+: ", "Failed to apply commands: ") + else errorValue + ) .. "\n" .. debug.traceback(thread) diff --git a/lib/World.spec.luau b/lib/World.spec.luau index d28addfe..b3857002 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -41,21 +41,8 @@ end return function() describe("World", function() - itSKIP("debug context", function() - local world = World.new() - local loop = Loop.new(world) - local bindable = BindableEvent.new() - - local function thisIsASystem() - -- error("hi") - world:spawn({}) - end - - loop:scheduleSystem(thisIsASystem) - loop:begin({ default = bindable.Event }) - bindable:Fire() - end) - + -- TODO: + -- Probably need more tests for buffering it("should buffer commands if enabled", function() local world = World.new() world:startDeferring() @@ -78,7 +65,6 @@ return function() expect(world:contains(1)).to.equal(false) - -- Insert local b = B({}) world:insert(2, b) expect(world:get(2, B)).to.never.be.ok() From 0bdf9462f4483c98c57368ff205f41238345ce3f Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 18 Jul 2024 16:47:10 -0400 Subject: [PATCH 28/87] remove comments --- lib/World.luau | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 484638a3..717add0d 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -285,20 +285,11 @@ end However, you can call it more often if you want. ]=] function World:commitCommands() - --local errors for _, command in self.commands do processCommand(self, command) - -- if not ok then - -- if errors == nil then - -- errors = {} - -- end - - -- table.insert(errors, err) - -- end end table.clear(self.commands) - --return errors end --[=[ From 6edeee24b1ec010d874101517eb407f9ff72c372 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 18 Jul 2024 16:48:01 -0400 Subject: [PATCH 29/87] clear commands when we clear the world --- lib/World.luau | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/World.luau b/lib/World.luau index 717add0d..0582b334 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -434,6 +434,7 @@ end ]=] function World:clear() self.storage = {} + self.commands = {} self._entityArchetypes = {} self._entityMetatablesCache = {} self._size = 0 From 9c2d83046fba032565be44755078fc668d2c523e Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 21 Jul 2024 15:54:32 -0400 Subject: [PATCH 30/87] allow multiple worlds --- lib/Loop.luau | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/Loop.luau b/lib/Loop.luau index ec25ab88..c1ccd5e0 100644 --- a/lib/Loop.luau +++ b/lib/Loop.luau @@ -368,16 +368,15 @@ function Loop:begin(events) local profiling = self.profiling - -- TODO: - -- Should probably handle other worlds. - -- Also, TBD if this is a good way to detect this, or if it should even handle this at all. - local primaryWorld: World.World? = if typeof(self._state[1]) == "table" - and getmetatable(self._state[1]) == World - then self._state[1] - else nil - - if primaryWorld then - primaryWorld:startDeferring() + local worlds: { World.World } = {} + for _, stateArgument in self._state do + if typeof(stateArgument) == "table" and getmetatable(stateArgument) == World then + table.insert(worlds, stateArgument) + end + end + + for _, world in worlds do + world:startDeferring() end for _, system in ipairs(self._orderedSystemsByEvent[eventName]) do @@ -406,8 +405,8 @@ function Loop:begin(events) local thread = coroutine.create(function(...) fn(...) - if primaryWorld then - local ok, err = pcall(primaryWorld.commitCommands, primaryWorld) + for _, world in worlds do + local ok, err = pcall(world.commitCommands, world) if not ok then commitFailed = true error(err) From 020cd76cbdb0b14250b73d73b7140e41a7451fb8 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 22 Jul 2024 13:55:36 -0400 Subject: [PATCH 31/87] address review comments --- lib/World.luau | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 0582b334..f2868fb7 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -271,10 +271,6 @@ end If you are using a [`Loop`](/api/Loop), this is done for you. ]=] function World:startDeferring() - if self.deferring then - return - end - self.deferring = true end @@ -296,10 +292,6 @@ end Stops deferring entity commands and processes all commands left in the buffer. ]=] function World:stopDeferring() - if not self.deferring then - return - end - self:commitCommands() self.deferring = false end From 24b4a615e0360ede0953c9da36cfff4d5415952d Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 22 Jul 2024 19:56:06 -0400 Subject: [PATCH 32/87] validate arguments closer to source --- lib/World.luau | 102 +++++++++++++++++++-------------------------- lib/component.luau | 7 ++++ 2 files changed, 49 insertions(+), 60 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index f2868fb7..cd9695fb 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -2,7 +2,7 @@ local Component = require(script.Parent.component) local archetypeModule = require(script.Parent.archetype) local topoRuntime = require(script.Parent.topoRuntime) -local assertValidComponentInstance = Component.assertValidComponentInstance +local assertValidComponentInstances = Component.assertValidComponentInstances local assertValidComponent = Component.assertValidComponent local archetypeOf = archetypeModule.archetypeOf local negateArchetypeOf = archetypeModule.negateArchetypeOf @@ -11,7 +11,6 @@ local areArchetypesCompatible = archetypeModule.areArchetypesCompatible local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" -- The old solver is not great at resolving intersections, so we redefine entityId each time. -type SpawnCommand = { type: "spawn", entityId: number } type DespawnCommand = { type: "despawn", entityId: number } type InsertCommand = { type: "insert", @@ -26,10 +25,10 @@ type RemoveCommand = { type ReplaceCommand = { type: "replace", entityId: number, - components: { [any]: any }, + componentInstances: { [any]: any }, } -type Command = SpawnCommand | DespawnCommand | InsertCommand | RemoveCommand | ReplaceCommand +type Command = DespawnCommand | InsertCommand | RemoveCommand | ReplaceCommand --[=[ @class World @@ -113,23 +112,6 @@ function World:__iter() return World._next, self end -local function executeSpawn(world: World, spawnCommand: SpawnCommand) - local id = spawnCommand.entityId - if world:contains(id) then - error( - string.format( - "The world already contains an entity with ID %d. Use World:replace instead if this is intentional.", - id - ), - 2 - ) - end - - world._size += 1 - world._entityMetatablesCache[id] = {} - world:_transitionArchetype(id, {}) -end - local function executeDespawn(world: World, despawnCommand: DespawnCommand) local id = despawnCommand.entityId local entity = world:_getEntity(id) @@ -148,15 +130,9 @@ local function executeInsert(world: World, insertCommand: InsertCommand) debug.profilebegin("insert") local id = insertCommand.entityId - if not world:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - local entity = world:_getEntity(id) local wasNew = false - for i, componentInstance in insertCommand.componentInstances do - assertValidComponentInstance(componentInstance, i) - + for _, componentInstance in insertCommand.componentInstances do local metatable = getmetatable(componentInstance) local oldComponent = entity[metatable] @@ -186,17 +162,11 @@ local function executeReplace(world: World, replaceCommand: ReplaceCommand) local metatables = {} local entity = world:_getEntity(id) - for index, component in replaceCommand.components do - assertValidComponentInstance(component, index) - - local metatable = getmetatable(component) - if components[metatable] then - error(("Duplicate component type at index %d"):format(index), 2) - end - - world:_trackChanged(metatable, id, entity[metatable], component) + for _, componentInstance in replaceCommand.componentInstances do + local metatable = getmetatable(componentInstance) + world:_trackChanged(metatable, id, entity[metatable], componentInstance) - components[metatable] = component + components[metatable] = componentInstance table.insert(metatables, metatable) end @@ -212,16 +182,10 @@ end local function executeRemove(world: World, removeCommand: RemoveCommand) local id = removeCommand.entityId - if not world:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - local entity = world:_getEntity(id) local removed = {} for index, metatable in removeCommand.components do - assertValidComponent(metatable, index) - local oldComponent = entity[metatable] removed[index] = oldComponent @@ -240,9 +204,7 @@ local function executeRemove(world: World, removeCommand: RemoveCommand) end local function processCommand(world: World, command: Command) - if command.type == "spawn" then - executeSpawn(world, command) - elseif command.type == "insert" then + if command.type == "insert" then executeInsert(world, command) elseif command.type == "despawn" then executeDespawn(world, command) @@ -319,15 +281,22 @@ end @return number -- The same entity ID that was passed in ]=] function World:spawnAt(id, ...) - -- TODO: - -- Spawn should probably insert at the same time, because spawn used to check that you - -- didn't pass any duplicate components. if id >= self._nextId then self._nextId = id + 1 end - bufferCommand(self, { type = "spawn", entityId = id }) - bufferCommand(self, { type = "insert", entityId = id, componentInstances = { ... } }) + if self:contains(id) then + error(ERROR_NO_ENTITY) + end + + local componentInstances = { ... } + assertValidComponentInstances(componentInstances) + + self._size += 1 + self._entityMetatablesCache[id] = {} + self:_transitionArchetype(id, {}) + + bufferCommand(self, { type = "insert", entityId = id, componentInstances = componentInstances }) return id end @@ -405,7 +374,10 @@ end @param ... ComponentInstance -- The component values to spawn the entity with. ]=] function World:replace(id, ...) - bufferCommand(self, { type = "replace", entityId = id, components = { ... } }) + local componentInstances = { ... } + assertValidComponentInstances(componentInstances) + + bufferCommand(self, { type = "replace", entityId = id, componentInstances = componentInstances }) end --[=[ @@ -414,6 +386,10 @@ end @param id number -- The entity ID ]=] function World:despawn(id) + if not self:contains(id) then + error(ERROR_NO_ENTITY, 2) + end + bufferCommand(self, { type = "despawn", entityId = id }) end @@ -427,6 +403,8 @@ end function World:clear() self.storage = {} self.commands = {} + self.markedForDeletion = {} + self._entityArchetypes = {} self._entityMetatablesCache = {} self._size = 0 @@ -1086,7 +1064,14 @@ end @param ... ComponentInstance -- The component values to insert ]=] function World:insert(id, ...) - bufferCommand(self, { type = "insert", entityId = id, componentInstances = { ... } }) + if not self:contains(id) then + error(ERROR_NO_ENTITY, 2) + end + + local componentInstances = { ... } + assertValidComponentInstances(componentInstances) + + bufferCommand(self, { type = "insert", entityId = id, componentInstances = componentInstances }) end --[=[ @@ -1101,20 +1086,17 @@ end ]=] function World:remove(id, ...) if not self:contains(id) then - -- TODO: - -- What should we do? Should we error still? - return + error(ERROR_NO_ENTITY, 2) end -- NOTE: -- This functionality is deprecated and will be removed in a future release. - local entity = self:_getEntity(id) local components = { ... } local length = #components + + local entity = self:_getEntity(id) local removed = table.create(length, nil) for index, component in components do - -- TODO: - -- Should this check also be here, or no? assertValidComponent(component, index) local oldComponent = entity[component] diff --git a/lib/component.luau b/lib/component.luau index 5e9a61da..03c97c34 100644 --- a/lib/component.luau +++ b/lib/component.luau @@ -159,8 +159,15 @@ local function assertValidComponentInstance(value, position) end end +local function assertValidComponentInstances(componentInstances) + for position, componentInstance in componentInstances do + assertValidComponentInstance(componentInstance, position) + end +end + return { newComponent = newComponent, assertValidComponentInstance = assertValidComponentInstance, + assertValidComponentInstances = assertValidComponentInstances, assertValidComponent = assertValidComponent, } From 1fdcb824cb94c4226dddde873b9ac97888a53348 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 22 Jul 2024 20:00:28 -0400 Subject: [PATCH 33/87] add more tests --- lib/World.spec.luau | 1172 ++++++++++++++++++++++--------------------- 1 file changed, 596 insertions(+), 576 deletions(-) diff --git a/lib/World.spec.luau b/lib/World.spec.luau index b3857002..2cbd8aec 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -41,749 +41,769 @@ end return function() describe("World", function() - -- TODO: - -- Probably need more tests for buffering - it("should buffer commands if enabled", function() - local world = World.new() - world:startDeferring() + describe("buffered", function() + it("should spawn immediately", function() + local world = World.new() + world:startDeferring() + world:spawnAt(1) + expect(world:contains(1)).to.equal(true) + end) + + it("should not despawn immediately", function() + local world = World.new() + world:startDeferring() + world:spawnAt(1) + world:despawn(1) + expect(world:contains(1)).to.equal(true) + world:commitCommands() + expect(world:contains(1)).to.equal(false) + end) + + it("should not invalidate iterators when deferring", function() + local world = World.new() + local A = component() + local B = component() + local C = component() + + for _ = 1, 10 do + world:spawn(A(), B()) + end + + world:startDeferring() + local count = 0 + for id in world:query(A) do + count += 1 + world:insert(id, C()) + world:remove(id, B) + end + world:stopDeferring() + expect(count).to.equal(10) + end) - local A = component() - local B = component() - world:spawn(A({ - a = 1, - })) - world:spawn(A({ - a = 2, - })) + it("should handle many operations", function() + local world = World.new() + world:startDeferring() - world:commitCommands() + local A = component() + local B = component() + world:spawn(A({ + a = 1, + })) + world:spawn(A({ + a = 2, + })) - world:despawn(1) - expect(world:contains(1)).to.equal(true) + world:commitCommands() - world:commitCommands() + world:despawn(1) + expect(world:contains(1)).to.equal(true) - expect(world:contains(1)).to.equal(false) + world:commitCommands() - local b = B({}) - world:insert(2, b) - expect(world:get(2, B)).to.never.be.ok() + expect(world:contains(1)).to.equal(false) - world:commitCommands() + local b = B({}) + world:insert(2, b) + expect(world:get(2, B)).to.never.be.ok() - expect(world:get(2, B)).to.equal(b) - end) - it("should be iterable", function() - local world = World.new() - local A = component() - local B = component() - - local eA = world:spawn(A()) - local eB = world:spawn(B()) - local eAB = world:spawn(A(), B()) - - local count = 0 - for id, data in world do - count += 1 - if id == eA then - expect(data[A]).to.be.ok() - expect(data[B]).to.never.be.ok() - elseif id == eB then - expect(data[B]).to.be.ok() - expect(data[A]).to.never.be.ok() - elseif id == eAB then - expect(data[A]).to.be.ok() - expect(data[B]).to.be.ok() - else - error("unknown entity", id) - end - end + world:commitCommands() - expect(count).to.equal(3) + expect(world:get(2, B)).to.equal(b) + end) end) - it("should have correct size", function() - local world = World.new() - world:spawn() - world:spawn() - world:spawn() + describe("immediate", function() + it("should be iterable", function() + local world = World.new() + local A = component() + local B = component() - local id = world:spawn() - world:despawn(id) + local eA = world:spawn(A()) + local eB = world:spawn(B()) + local eAB = world:spawn(A(), B()) - expect(world:size()).to.equal(3) + local count = 0 + for id, data in world do + count += 1 + if id == eA then + expect(data[A]).to.be.ok() + expect(data[B]).to.never.be.ok() + elseif id == eB then + expect(data[B]).to.be.ok() + expect(data[A]).to.never.be.ok() + elseif id == eAB then + expect(data[A]).to.be.ok() + expect(data[B]).to.be.ok() + else + error("unknown entity", id) + end + end - world:clear() + expect(count).to.equal(3) + end) - expect(world:size()).to.equal(0) - end) + it("should have correct size", function() + local world = World.new() + world:spawn() + world:spawn() + world:spawn() - it("should report contains correctly", function() - local world = World.new() - local id = world:spawn() + local id = world:spawn() + world:despawn(id) - expect(world:contains(id)).to.equal(true) - expect(world:contains(1234124124124124124124)).to.equal(false) - end) + expect(world:size()).to.equal(3) - it("should allow spawning entities at a specific ID", function() - local world = World.new() + world:clear() - local A = component() - local id = world:spawnAt(5, A()) + expect(world:size()).to.equal(0) + end) - expect(function() - world:spawnAt(5, A()) - end).to.throw() + it("should report contains correctly", function() + local world = World.new() + local id = world:spawn() - expect(id).to.equal(5) + expect(world:contains(id)).to.equal(true) + expect(world:contains(1234124124124124124124)).to.equal(false) + end) - local nextId = world:spawn(A()) - expect(nextId).to.equal(6) - end) + it("should allow spawning entities at a specific ID", function() + local world = World.new() - it("should allow inserting and removing components from existing entities", function() - local world = World.new() + local A = component() + local id = world:spawnAt(5, A()) - local Player = component() - local Health = component() - local Poison = component() + expect(function() + world:spawnAt(5, A()) + end).to.throw() - local id = world:spawn(Player(), Poison()) + expect(id).to.equal(5) - expect(world:query(Player):next()).to.be.ok() - expect(world:query(Health):next()).to.never.be.ok() + local nextId = world:spawn(A()) + expect(nextId).to.equal(6) + end) - world:insert(id, Health()) + it("should allow inserting and removing components from existing entities", function() + local world = World.new() - expect(world:query(Player):next()).to.be.ok() - expect(world:query(Health):next()).to.be.ok() - expect(world:size()).to.equal(1) + local Player = component() + local Health = component() + local Poison = component() - local player, poison = world:remove(id, Player, Poison) + local id = world:spawn(Player(), Poison()) - expect(getmetatable(player)).to.equal(Player) - expect(getmetatable(poison)).to.equal(Poison) + expect(world:query(Player):next()).to.be.ok() + expect(world:query(Health):next()).to.never.be.ok() - expect(world:query(Player):next()).to.never.be.ok() - expect(world:query(Health):next()).to.be.ok() - expect(world:size()).to.equal(1) - end) + world:insert(id, Health()) - it("should not find any entities", function() - local world = World.new() + expect(world:query(Player):next()).to.be.ok() + expect(world:query(Health):next()).to.be.ok() + expect(world:size()).to.equal(1) - local Hello = component() - local Bob = component() - local Shirley = component() + local player, poison = world:remove(id, Player, Poison) - local _helloBob = world:spawn(Hello(), Bob()) - local _helloShirley = world:spawn(Hello(), Shirley()) + expect(getmetatable(player)).to.equal(Player) + expect(getmetatable(poison)).to.equal(Poison) - local withoutCount = 0 - for _ in world:query(Hello):without(Bob, Shirley) do - withoutCount += 1 - end + expect(world:query(Player):next()).to.never.be.ok() + expect(world:query(Health):next()).to.be.ok() + expect(world:size()).to.equal(1) + end) - expect(withoutCount).to.equal(0) - end) + it("should not find any entities", function() + local world = World.new() - it("should be queryable", function() - local world = World.new() - - local Player = component() - local Health = component() - local Poison = component() - - local one = world:spawn( - Player({ - name = "alice", - }), - Health({ - value = 100, - }), - Poison() - ) - - world:spawn( -- Spawn something we don't want to get back - component()(), - component()() - ) - - local two = world:spawn( - Player({ - name = "bob", - }), - Health({ - value = 99, - }) - ) + local Hello = component() + local Bob = component() + local Shirley = component() - local found = {} - local foundCount = 0 + local _helloBob = world:spawn(Hello(), Bob()) + local _helloShirley = world:spawn(Hello(), Shirley()) - for entityId, player, health in world:query(Player, Health) do - foundCount += 1 - found[entityId] = { - [Player] = player, - [Health] = health, - } - end + local withoutCount = 0 + for _ in world:query(Hello):without(Bob, Shirley) do + withoutCount += 1 + end - expect(foundCount).to.equal(2) + expect(withoutCount).to.equal(0) + end) - expect(found[one]).to.be.ok() - expect(found[one][Player].name).to.equal("alice") - expect(found[one][Health].value).to.equal(100) + it("should be queryable", function() + local world = World.new() + + local Player = component() + local Health = component() + local Poison = component() + + local one = world:spawn( + Player({ + name = "alice", + }), + Health({ + value = 100, + }), + Poison() + ) + + world:spawn( -- Spawn something we don't want to get back + component()(), + component()() + ) + + local two = world:spawn( + Player({ + name = "bob", + }), + Health({ + value = 99, + }) + ) + + local found = {} + local foundCount = 0 + + for entityId, player, health in world:query(Player, Health) do + foundCount += 1 + found[entityId] = { + [Player] = player, + [Health] = health, + } + end - expect(found[two]).to.be.ok() - expect(found[two][Player].name).to.equal("bob") - expect(found[two][Health].value).to.equal(99) + expect(foundCount).to.equal(2) - local count = 0 - for id, player in world:query(Player) do - expect(type(player.name)).to.equal("string") - expect(type(id)).to.equal("number") - count += 1 - end - expect(count).to.equal(2) + expect(found[one]).to.be.ok() + expect(found[one][Player].name).to.equal("alice") + expect(found[one][Health].value).to.equal(100) - local withoutCount = 0 - for _id, _player in world:query(Player):without(Poison) do - withoutCount += 1 - end + expect(found[two]).to.be.ok() + expect(found[two][Player].name).to.equal("bob") + expect(found[two][Health].value).to.equal(99) - expect(withoutCount).to.equal(1) - end) + local count = 0 + for id, player in world:query(Player) do + expect(type(player.name)).to.equal("string") + expect(type(id)).to.equal("number") + count += 1 + end + expect(count).to.equal(2) - it("should return an empty query with the same methods", function() - local world = World.new() + local withoutCount = 0 + for _id, _player in world:query(Player):without(Poison) do + withoutCount += 1 + end - local Player = component() - local Enemy = component() + expect(withoutCount).to.equal(1) + end) - expect(world:query(Player):next()).to.equal(nil) - expect(#world:query(Player):snapshot()).to.equal(0) + it("should return an empty query with the same methods", function() + local world = World.new() - expect(world:query(Player):without(Enemy):next()).to.equal(world:query(Player):next()) + local Player = component() + local Enemy = component() - expect(world:query(Player):view():get()).to.equal(nil) - expect(world:query(Player):view():contains()).to.equal(false) + expect(world:query(Player):next()).to.equal(nil) + expect(#world:query(Player):snapshot()).to.equal(0) - local viewCount = 0 - for _ in world:query(Player):view() do - viewCount += 1 - end + expect(world:query(Player):without(Enemy):next()).to.equal(world:query(Player):next()) - expect(viewCount).to.equal(0) - end) + expect(world:query(Player):view():get()).to.equal(nil) + expect(world:query(Player):view():contains()).to.equal(false) - it("should allow getting single components", function() - local world = World.new() + local viewCount = 0 + for _ in world:query(Player):view() do + viewCount += 1 + end - local Player = component() - local Health = component() - local Other = component() + expect(viewCount).to.equal(0) + end) - local id = world:spawn(Other({ a = 1 }), Player({ b = 2 }), Health({ c = 3 })) + it("should allow getting single components", function() + local world = World.new() - expect(world:get(id, Player).b).to.equal(2) - expect(world:get(id, Health).c).to.equal(3) + local Player = component() + local Health = component() + local Other = component() - local one, two = world:get(id, Health, Player) + local id = world:spawn(Other({ a = 1 }), Player({ b = 2 }), Health({ c = 3 })) - expect(one.c).to.equal(3) - expect(two.b).to.equal(2) - end) + expect(world:get(id, Player).b).to.equal(2) + expect(world:get(id, Health).c).to.equal(3) - it("should return existing entities when creating queryChanged", function() - local world = World.new() + local one, two = world:get(id, Health, Player) - local loop = Loop.new(world) + expect(one.c).to.equal(3) + expect(two.b).to.equal(2) + end) - local A = component() + it("should return existing entities when creating queryChanged", function() + local world = World.new() - local initial = { - world:spawn(A({ - a = 1, - })), - world:spawn(A({ - b = 2, - })), - } + local loop = Loop.new(world) - local third + local A = component() - local runCount = 0 - loop:scheduleSystem(function(world) - runCount += 1 + local initial = { + world:spawn(A({ + a = 1, + })), + world:spawn(A({ + b = 2, + })), + } - local map = {} - local count = 0 + local third - for entityId, record in world:queryChanged(A) do - count += 1 - map[entityId] = record - end + local runCount = 0 + loop:scheduleSystem(function(world) + runCount += 1 - if runCount == 1 then - expect(count).to.equal(2) - expect(map[initial[1]].new.a).to.equal(1) - expect(map[initial[1]].old).to.equal(nil) - expect(map[initial[2]].new.b).to.equal(2) - else - expect(count).to.equal(1) - expect(map[third].new.c).to.equal(3) - end - end) + local map = {} + local count = 0 - local defaultBindable = BindableEvent.new() + for entityId, record in world:queryChanged(A) do + count += 1 + map[entityId] = record + end - loop:begin({ default = defaultBindable.Event }) + if runCount == 1 then + expect(count).to.equal(2) + expect(map[initial[1]].new.a).to.equal(1) + expect(map[initial[1]].old).to.equal(nil) + expect(map[initial[2]].new.b).to.equal(2) + else + expect(count).to.equal(1) + expect(map[third].new.c).to.equal(3) + end + end) - defaultBindable:Fire() + local defaultBindable = BindableEvent.new() - expect(runCount).to.equal(1) + loop:begin({ default = defaultBindable.Event }) - third = world:spawn(A({ - c = 3, - })) + defaultBindable:Fire() - world:commitCommands() + expect(runCount).to.equal(1) - defaultBindable:Fire() - expect(runCount).to.equal(2) - end) + third = world:spawn(A({ + c = 3, + })) - it("should find entity without and with component", function() - local world = World.new() + world:commitCommands() - local Character = component("Character") - local LocalOwned = component("LocalOwned") + defaultBindable:Fire() + expect(runCount).to.equal(2) + end) - local _helloBob = world:spawn(Character(), LocalOwned()) + it("should find entity without and with component", function() + local world = World.new() - local withoutCount = 0 - for _ in world:query(Character):without(LocalOwned) do - withoutCount += 1 - end + local Character = component("Character") + local LocalOwned = component("LocalOwned") - expect(withoutCount).to.equal(0) + local _helloBob = world:spawn(Character(), LocalOwned()) - world:remove(_helloBob, LocalOwned) + local withoutCount = 0 + for _ in world:query(Character):without(LocalOwned) do + withoutCount += 1 + end - local withoutCount = 0 - for _ in world:query(Character):without(LocalOwned) do - withoutCount += 1 - end + expect(withoutCount).to.equal(0) - expect(withoutCount).to.equal(1) + world:remove(_helloBob, LocalOwned) - world:insert(_helloBob, LocalOwned()) + local withoutCount = 0 + for _ in world:query(Character):without(LocalOwned) do + withoutCount += 1 + end - local withoutCount = 0 - for _ in world:query(Character):without(LocalOwned) do - withoutCount += 1 - end + expect(withoutCount).to.equal(1) - expect(withoutCount).to.equal(0) - end) + world:insert(_helloBob, LocalOwned()) + + local withoutCount = 0 + for _ in world:query(Character):without(LocalOwned) do + withoutCount += 1 + end - it("should track changes", function() - local world = World.new() + expect(withoutCount).to.equal(0) + end) + + it("should track changes", function() + local world = World.new() - local loop = Loop.new(world) + local loop = Loop.new(world) - local A = component() - local B = component() - local C = component() + local A = component() + local B = component() + local C = component() - local expectedResults = { - nil, - { - 1, + local expectedResults = { + nil, { - new = { - generation = 1, + 1, + { + new = { + generation = 1, + }, }, }, - }, - { - 1, { - new = { - generation = 2, - }, - old = { - generation = 1, + 1, + { + new = { + generation = 2, + }, + old = { + generation = 1, + }, }, }, - }, - { - 2, { - new = { - generation = 1, + 2, + { + new = { + generation = 1, + }, }, }, - }, - nil, - { - 1, + nil, { - old = { - generation = 2, + 1, + { + old = { + generation = 2, + }, }, }, - }, - { - 2, { - old = { - generation = 1, + 2, + { + old = { + generation = 1, + }, }, }, - }, - } + } - local resultIndex = 0 + local resultIndex = 0 - local additionalQuery = C - loop:scheduleSystem(function(w) - local ran = false + local additionalQuery = C + loop:scheduleSystem(function(w) + local ran = false - for entityId, record in w:queryChanged(A) do - if additionalQuery then - if w:get(entityId, additionalQuery) == nil then - continue + for entityId, record in w:queryChanged(A) do + if additionalQuery then + if w:get(entityId, additionalQuery) == nil then + continue + end end - end - - ran = true - resultIndex += 1 - expect(entityId).to.equal(expectedResults[resultIndex][1]) - - assertDeepEqual(record, expectedResults[resultIndex][2]) - end + ran = true + resultIndex += 1 - if not ran then - resultIndex += 1 - expect(expectedResults[resultIndex]).to.equal(nil) - end - end) + expect(entityId).to.equal(expectedResults[resultIndex][1]) - local infrequentCount = 0 - loop:scheduleSystem({ - system = function(w) - infrequentCount += 1 + assertDeepEqual(record, expectedResults[resultIndex][2]) + end - local count = 0 - local results = {} - for entityId, record in w:queryChanged(A) do - count += 1 - results[entityId - 1] = record + if not ran then + resultIndex += 1 + expect(expectedResults[resultIndex]).to.equal(nil) end + end) + + local infrequentCount = 0 + loop:scheduleSystem({ + system = function(w) + infrequentCount += 1 + + local count = 0 + local results = {} + for entityId, record in w:queryChanged(A) do + count += 1 + results[entityId - 1] = record + end - if count == 0 then - expect(infrequentCount).to.equal(1) - else - if infrequentCount == 2 then - expect(count).to.equal(2) - - expect(results[0].old).to.equal(nil) - expect(results[0].new.generation).to.equal(2) - expect(results[1].old).to.equal(nil) - expect(results[1].new).to.equal(nil) - elseif infrequentCount == 3 then - expect(results[0].old.generation).to.equal(2) - expect(results[0].new).to.equal(nil) - expect(count).to.equal(1) + if count == 0 then + expect(infrequentCount).to.equal(1) else - error("infrequentCount too high") + if infrequentCount == 2 then + expect(count).to.equal(2) + + expect(results[0].old).to.equal(nil) + expect(results[0].new.generation).to.equal(2) + expect(results[1].old).to.equal(nil) + expect(results[1].new).to.equal(nil) + elseif infrequentCount == 3 then + expect(results[0].old.generation).to.equal(2) + expect(results[0].new).to.equal(nil) + expect(count).to.equal(1) + else + error("infrequentCount too high") + end end - end - end, - event = "infrequent", - }) + end, + event = "infrequent", + }) - local defaultBindable = BindableEvent.new() - local infrequentBindable = BindableEvent.new() + local defaultBindable = BindableEvent.new() + local infrequentBindable = BindableEvent.new() - loop:begin({ default = defaultBindable.Event, infrequent = infrequentBindable.Event }) + loop:begin({ default = defaultBindable.Event, infrequent = infrequentBindable.Event }) - defaultBindable:Fire() - infrequentBindable:Fire() + defaultBindable:Fire() + infrequentBindable:Fire() - local entityId = world:spawn( - A({ - generation = 1, - }), - C() - ) + local entityId = world:spawn( + A({ + generation = 1, + }), + C() + ) - world:commitCommands() + world:commitCommands() - defaultBindable:Fire() + defaultBindable:Fire() - additionalQuery = nil + additionalQuery = nil - world:insert( - entityId, - A({ - generation = 2, - }) - ) + world:insert( + entityId, + A({ + generation = 2, + }) + ) - world:insert( - entityId, - B({ - foo = "bar", - }) - ) + world:insert( + entityId, + B({ + foo = "bar", + }) + ) - local secondEntityId = world:spawn( - A({ - generation = 1, - }), - C() - ) + local secondEntityId = world:spawn( + A({ + generation = 1, + }), + C() + ) - world:commitCommands() + world:commitCommands() - defaultBindable:Fire() - defaultBindable:Fire() + defaultBindable:Fire() + defaultBindable:Fire() - world:replace(secondEntityId, B()) + world:replace(secondEntityId, B()) - world:commitCommands() + world:commitCommands() - infrequentBindable:Fire() + infrequentBindable:Fire() - world:despawn(entityId) + world:despawn(entityId) - world:commitCommands() + world:commitCommands() - defaultBindable:Fire() - - infrequentBindable:Fire() - end) + defaultBindable:Fire() - it("should error when passing nil to query", function() - expect(function() - World.new():query(nil) - end).to.throw() - end) - - it("should error when passing an invalid table", function() - local world = World.new() - local id = world:spawn() - - expect(function() - world:insert(id, {}) - end).to.throw() - end) + infrequentBindable:Fire() + end) - it("should error when passing a Component instead of Component instance", function() - expect(function() - World.new():spawn(component()) - end).to.throw() - end) + it("should error when passing nil to query", function() + expect(function() + World.new():query(nil) + end).to.throw() + end) - it("should allow snapshotting a query", function() - local world = World.new() - - local Player = component() - local Health = component() - local Poison = component() - - local one = world:spawn( - Player({ - name = "alice", - }), - Health({ - value = 100, - }), - Poison() - ) - - world:spawn( -- Spawn something we don't want to get back - component()(), - component()() - ) - - local two = world:spawn( - Player({ - name = "bob", - }), - Health({ - value = 99, - }) - ) + it("should error when passing an invalid table", function() + local world = World.new() + local id = world:spawn() - local query = world:query(Health, Player) - local snapshot = query:snapshot() + expect(function() + world:insert(id, {}) + end).to.throw() + end) - for entityId, health, player in snapshot do - expect(type(entityId)).to.equal("number") - expect(type(player.name)).to.equal("string") - expect(type(health.value)).to.equal("number") - end + it("should error when passing a Component instead of Component instance", function() + expect(function() + World.new():spawn(component()) + end).to.throw() + end) - world:remove(two, Health) - world:despawn(one) + it("should allow snapshotting a query", function() + local world = World.new() + + local Player = component() + local Health = component() + local Poison = component() + + local one = world:spawn( + Player({ + name = "alice", + }), + Health({ + value = 100, + }), + Poison() + ) + + world:spawn( -- Spawn something we don't want to get back + component()(), + component()() + ) + + local two = world:spawn( + Player({ + name = "bob", + }), + Health({ + value = 99, + }) + ) + + local query = world:query(Health, Player) + local snapshot = query:snapshot() + + for entityId, health, player in snapshot do + expect(type(entityId)).to.equal("number") + expect(type(player.name)).to.equal("string") + expect(type(health.value)).to.equal("number") + end - if snapshot[2][1] == 3 then - expect(snapshot[1][1]).to.equal(1) - else - expect(snapshot[2][1]).to.equal(1) - end + world:remove(two, Health) + world:despawn(one) - expect(#world:query(Player):without(Poison):snapshot()).to.equal(1) - end) + if snapshot[2][1] == 3 then + expect(snapshot[1][1]).to.equal(1) + else + expect(snapshot[2][1]).to.equal(1) + end - it("should contain entity in view", function() - local ComponentA = component("ComponentA") - local ComponentB = component("ComponentB") + expect(#world:query(Player):without(Poison):snapshot()).to.equal(1) + end) - local world = World.new() + it("should contain entity in view", function() + local ComponentA = component("ComponentA") + local ComponentB = component("ComponentB") - local entityA = world:spawn(ComponentA()) - local entityB = world:spawn(ComponentB()) + local world = World.new() - local viewA = world:query(ComponentA):view() - local viewB = world:query(ComponentB):view() + local entityA = world:spawn(ComponentA()) + local entityB = world:spawn(ComponentB()) - expect(viewA:contains(entityA)).to.equal(true) - expect(viewA:contains(entityB)).to.equal(false) - expect(viewB:contains(entityB)).to.equal(true) - expect(viewB:contains(entityA)).to.equal(false) - end) + local viewA = world:query(ComponentA):view() + local viewB = world:query(ComponentB):view() - it("should get entity data from view", function() - local numComponents = 20 - local components = {} + expect(viewA:contains(entityA)).to.equal(true) + expect(viewA:contains(entityB)).to.equal(false) + expect(viewB:contains(entityB)).to.equal(true) + expect(viewB:contains(entityA)).to.equal(false) + end) - for i = 1, numComponents do - table.insert(components, component("Component" .. i)) - end + it("should get entity data from view", function() + local numComponents = 20 + local components = {} - local world = World.new() + for i = 1, numComponents do + table.insert(components, component("Component" .. i)) + end - local componentInstances = {} + local world = World.new() - for _, componentFn in components do - table.insert(componentInstances, componentFn()) - end + local componentInstances = {} - local entityA = world:spawn(table.unpack(componentInstances)) + for _, componentFn in components do + table.insert(componentInstances, componentFn()) + end - local viewA = world:query(table.unpack(components)):view() - local viewB = world:query(components[1]):view() + local entityA = world:spawn(table.unpack(componentInstances)) - expect(select("#", viewA:get(entityA))).to.equal(numComponents) - expect(select("#", viewB:get(entityA))).to.equal(1) + local viewA = world:query(table.unpack(components)):view() + local viewB = world:query(components[1]):view() - local viewAEntityAData = { viewA:get(entityA) } + expect(select("#", viewA:get(entityA))).to.equal(numComponents) + expect(select("#", viewB:get(entityA))).to.equal(1) - for index, componentData in viewAEntityAData do - expect(getmetatable(componentData)).to.equal(components[index]) - end + local viewAEntityAData = { viewA:get(entityA) } - local viewBEntityAData = { viewB:get(entityA) } + for index, componentData in viewAEntityAData do + expect(getmetatable(componentData)).to.equal(components[index]) + end - expect(getmetatable(viewBEntityAData[1])).to.equal(components[1]) - end) + local viewBEntityAData = { viewB:get(entityA) } - it("should return view results in query order", function() - local Parent = component("Parent") - local Transform = component("Transform") - local Root = component("Root") - - local world = World.new() - - local root = world:spawn(Transform({ pos = Vector2.new(3, 4) }), Root()) - local _otherRoot = world:spawn(Transform({ pos = Vector2.new(1, 2) }), Root()) - - local child = world:spawn( - Parent({ - entity = root, - fromChild = Transform({ pos = Vector2.one }), - }), - Transform.new({ pos = Vector2.zero }) - ) - - local _otherChild = world:spawn( - Parent({ - entity = root, - fromChild = Transform({ pos = Vector2.new(0, 0) }), - }), - Transform.new({ pos = Vector2.zero }) - ) - - local _grandChild = world:spawn( - Parent({ - entity = child, - fromChild = Transform({ pos = Vector2.new(-1, 0) }), - }), - Transform.new({ pos = Vector2.zero }) - ) - - local parents = world:query(Parent):view() - local roots = world:query(Transform, Root):view() - - expect(parents:contains(root)).to.equal(false) - - local orderOfIteration = {} - - for id in world:query(Transform, Parent) do - table.insert(orderOfIteration, id) - end - - local view = world:query(Transform, Parent):view() - local i = 0 - for id in view do - i += 1 - expect(orderOfIteration[i]).to.equal(id) - end + expect(getmetatable(viewBEntityAData[1])).to.equal(components[1]) + end) - for id, absolute, parent in world:query(Transform, Parent) do - local relative = parent.fromChild.pos - local ancestor = parent.entity - local current = parents:get(ancestor) - while current do - relative = current.fromChild.pos * relative - ancestor = current.entity - current = parents:get(ancestor) + it("should return view results in query order", function() + local Parent = component("Parent") + local Transform = component("Transform") + local Root = component("Root") + + local world = World.new() + + local root = world:spawn(Transform({ pos = Vector2.new(3, 4) }), Root()) + local _otherRoot = world:spawn(Transform({ pos = Vector2.new(1, 2) }), Root()) + + local child = world:spawn( + Parent({ + entity = root, + fromChild = Transform({ pos = Vector2.one }), + }), + Transform.new({ pos = Vector2.zero }) + ) + + local _otherChild = world:spawn( + Parent({ + entity = root, + fromChild = Transform({ pos = Vector2.new(0, 0) }), + }), + Transform.new({ pos = Vector2.zero }) + ) + + local _grandChild = world:spawn( + Parent({ + entity = child, + fromChild = Transform({ pos = Vector2.new(-1, 0) }), + }), + Transform.new({ pos = Vector2.zero }) + ) + + local parents = world:query(Parent):view() + local roots = world:query(Transform, Root):view() + + expect(parents:contains(root)).to.equal(false) + + local orderOfIteration = {} + + for id in world:query(Transform, Parent) do + table.insert(orderOfIteration, id) end - local pos = roots:get(ancestor).pos - - world:insert(id, absolute:patch({ pos = Vector2.new(pos.x + relative.x, pos.y + relative.y) })) - end + local view = world:query(Transform, Parent):view() + local i = 0 + for id in view do + i += 1 + expect(orderOfIteration[i]).to.equal(id) + end - expect(world:get(child, Transform).pos).to.equal(Vector2.new(4, 5)) - end) + for id, absolute, parent in world:query(Transform, Parent) do + local relative = parent.fromChild.pos + local ancestor = parent.entity + local current = parents:get(ancestor) + while current do + relative = current.fromChild.pos * relative + ancestor = current.entity + current = parents:get(ancestor) + end - it("should not invalidate iterators when deferring", function() - local world = World.new() - local A = component() - local B = component() - local C = component() + local pos = roots:get(ancestor).pos - for _ = 1, 10 do - world:spawn(A(), B()) - end + world:insert(id, absolute:patch({ pos = Vector2.new(pos.x + relative.x, pos.y + relative.y) })) + end - world:startDeferring() - local count = 0 - for id in world:query(A) do - count += 1 - world:insert(id, C()) - world:remove(id, B) - end - world:stopDeferring() - expect(count).to.equal(10) + expect(world:get(child, Transform).pos).to.equal(Vector2.new(4, 5)) + end) end) end) end From 500d3cd3b2b74cc77ba8a8ad6d4d6730988f0b6b Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 22 Jul 2024 21:19:04 -0400 Subject: [PATCH 34/87] track entities that are marked for deletion --- lib/World.luau | 36 ++++++++++++++++++++++++++++-------- lib/World.spec.luau | 19 +++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index cd9695fb..d4c25bab 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -50,6 +50,7 @@ function World.new() deferring = false, commands = {} :: { Command }, + markedForDeletion = {}, -- Map from entity ID -> archetype string _entityArchetypes = {}, @@ -80,6 +81,7 @@ export type World = typeof(World.new()) function World:_getEntity(id) local archetype = self._entityArchetypes[id] + print("Entity archetype:", archetype, self._entityArchetypes) return self.storage[archetype][id] end @@ -120,8 +122,9 @@ local function executeDespawn(world: World, despawnCommand: DespawnCommand) world:_trackChanged(metatable, id, component, nil) end - world._entityMetatablesCache[id] = nil - world:_transitionArchetype(id, nil) + local shouldOnlyClear = world.deferring and world.markedForDeletion[id] ~= true + world._entityMetatablesCache[id] = if shouldOnlyClear then {} else nil + world:_transitionArchetype(id, if shouldOnlyClear then {} else nil) world._size -= 1 end @@ -130,6 +133,7 @@ local function executeInsert(world: World, insertCommand: InsertCommand) debug.profilebegin("insert") local id = insertCommand.entityId + print("Execute command", id) local entity = world:_getEntity(id) local wasNew = false for _, componentInstance in insertCommand.componentInstances do @@ -219,8 +223,17 @@ end local function bufferCommand(world: World, command: Command) if world.deferring then - -- Attach current debug context because we don't know when this command will be processed. - (command :: any).debugContext = world.debugContext + -- We want to ignore commands that succeed a deletion. + -- Spawn isn't considered a command, and so it never reaches here. + local markedForDeletion = world.markedForDeletion + if markedForDeletion[command.entityId] then + return + end + + if command.type == "despawn" then + markedForDeletion[command.entityId] = true + end + table.insert(world.commands, command) else processCommand(world, command) @@ -285,14 +298,19 @@ function World:spawnAt(id, ...) self._nextId = id + 1 end - if self:contains(id) then + local componentInstances = { ... } + assertValidComponentInstances(componentInstances) + + local willBeDeleted = self.markedForDeletion[id] ~= nil + if self:contains(id) and not willBeDeleted then error(ERROR_NO_ENTITY) end - local componentInstances = { ... } - assertValidComponentInstances(componentInstances) + if not willBeDeleted then + self._size += 1 + end - self._size += 1 + self.markedForDeletion[id] = nil self._entityMetatablesCache[id] = {} self:_transitionArchetype(id, {}) @@ -386,6 +404,8 @@ end @param id number -- The entity ID ]=] function World:despawn(id) + -- TODO: + -- should this also check if it's marked for deletion already? if not self:contains(id) then error(ERROR_NO_ENTITY, 2) end diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 2cbd8aec..faf18498 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -42,6 +42,13 @@ end return function() describe("World", function() describe("buffered", function() + local function createDeferredWorld() + local world = World.new() + world:startDeferring() + + return world + end + it("should spawn immediately", function() local world = World.new() world:startDeferring() @@ -59,6 +66,18 @@ return function() expect(world:contains(1)).to.equal(false) end) + it("should allow spawnAt on an entity marked for deletion", function() + local world = createDeferredWorld() + world:spawnAt(1) + world:despawn(1) + world:spawnAt(1) + world:despawn(1) + world:spawnAt(1) + world:commitCommands() + + expect(world:contains(1)).to.equal(true) + end) + it("should not invalidate iterators when deferring", function() local world = World.new() local A = component() From 00faa150bee8b8b9e4e13e8f81eb2a2c6294d61f Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 23 Jul 2024 16:08:16 -0400 Subject: [PATCH 35/87] update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92e91eb4..6f13f4d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,18 @@ The format is based on [Keep a Changelog][kac], and this project adheres to ## [Unreleased] +### Added + +- Implemented a deferred command mode for the registry. + - The Loop turns deferring on for all worlds given to it. + - The command buffer is flushed between systems. + - Iterator invalidation is now only prevented in deferred mode. + +### Changed + +- Deprecated the return type of `World:remove()`. +- Deprecated `World:optimizeQueries()` because it no longer does anything. + ## [0.8.3] - 2024-07-02 ### Fixed From f39172ae57258107de6d5179dd6ff9276e6c783d Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 23 Jul 2024 16:08:45 -0400 Subject: [PATCH 36/87] remove prints --- lib/World.luau | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index d4c25bab..7c0c653d 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -81,7 +81,6 @@ export type World = typeof(World.new()) function World:_getEntity(id) local archetype = self._entityArchetypes[id] - print("Entity archetype:", archetype, self._entityArchetypes) return self.storage[archetype][id] end @@ -133,7 +132,6 @@ local function executeInsert(world: World, insertCommand: InsertCommand) debug.profilebegin("insert") local id = insertCommand.entityId - print("Execute command", id) local entity = world:_getEntity(id) local wasNew = false for _, componentInstance in insertCommand.componentInstances do From a2670375c283c2373136516e0bdf7dc88cc01fbf Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 23 Jul 2024 16:09:04 -0400 Subject: [PATCH 37/87] remove commented out code --- lib/World.luau | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 7c0c653d..caabeceb 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -903,10 +903,6 @@ function World:query(...) return entityId, unpack(queryOutput, 1, queryLength) end - -- if self._pristineStorage == self._storages[1] then - -- self:_markStorageDirty() - -- end - return QueryResult.new(self, expand, archetype, compatibleArchetypes, metatables) end From 6b1d1b77d744a2865856a24770a201d00ff58a6f Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 23 Jul 2024 16:40:07 -0400 Subject: [PATCH 38/87] apply reviewed changes --- CHANGELOG.md | 4 ++-- testez-companion.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f13f4d2..4679f8ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,9 @@ The format is based on [Keep a Changelog][kac], and this project adheres to - The command buffer is flushed between systems. - Iterator invalidation is now only prevented in deferred mode. -### Changed +### Deprecated -- Deprecated the return type of `World:remove()`. +- Deprecated the return type of `World:remove()` because it can now be inaccurate. - Deprecated `World:optimizeQueries()` because it no longer does anything. ## [0.8.3] - 2024-07-02 diff --git a/testez-companion.toml b/testez-companion.toml index 119295fc..9270d111 100644 --- a/testez-companion.toml +++ b/testez-companion.toml @@ -1 +1 @@ -roots = ["ReplicatedStorage/Matter"] \ No newline at end of file +roots = ["ReplicatedStorage/Matter"] From 2fceafe3e566755a681c0eff45ce14168f5a4afd Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 26 Jul 2024 22:08:56 -0400 Subject: [PATCH 39/87] apply feedback from review --- lib/World.luau | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index caabeceb..d7b7ccf1 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -9,19 +9,24 @@ local negateArchetypeOf = archetypeModule.negateArchetypeOf local areArchetypesCompatible = archetypeModule.areArchetypesCompatible local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" +local ERROR_EXISTING_ENTITY = + "The world already contains an entity with ID %. Use world:replace instead if this is intentional." -- The old solver is not great at resolving intersections, so we redefine entityId each time. type DespawnCommand = { type: "despawn", entityId: number } + type InsertCommand = { type: "insert", entityId: number, componentInstances: { [any]: any }, } + type RemoveCommand = { type: "remove", entityId: number, components: { [any]: any }, } + type ReplaceCommand = { type: "replace", entityId: number, @@ -276,10 +281,7 @@ end @return number -- The new entity ID. ]=] function World:spawn(...) - local entityId = self._nextId - self._nextId += 1 - - return self:spawnAt(entityId, ...) + return self:spawnAt(self._nextId, ...) end --[=[ @@ -301,7 +303,7 @@ function World:spawnAt(id, ...) local willBeDeleted = self.markedForDeletion[id] ~= nil if self:contains(id) and not willBeDeleted then - error(ERROR_NO_ENTITY) + error(string.format(ERROR_EXISTING_ENTITY, id), 2) end if not willBeDeleted then @@ -354,12 +356,6 @@ function World:_transitionArchetype(id, components) newArchetype = archetypeOf(unpack(self._entityMetatablesCache[id])) if oldArchetype ~= newArchetype then - -- NOTE: - -- This seems... maybe wrong - if oldArchetype then - storage[oldArchetype][id] = nil - end - if storage[newArchetype] == nil then storage[newArchetype] = {} end @@ -402,8 +398,6 @@ end @param id number -- The entity ID ]=] function World:despawn(id) - -- TODO: - -- should this also check if it's marked for deletion already? if not self:contains(id) then error(ERROR_NO_ENTITY, 2) end @@ -1103,8 +1097,6 @@ function World:remove(id, ...) error(ERROR_NO_ENTITY, 2) end - -- NOTE: - -- This functionality is deprecated and will be removed in a future release. local components = { ... } local length = #components @@ -1142,7 +1134,7 @@ end the World. Calling this function combines the separate storage back into the main storage, which speeds things up again. - @deprecated v0.9.0 -- This method no longer does anything. + @deprecated v0.9.0 -- With the introduction of command buffering only one storage will ever exist at a time. ]=] function World:optimizeQueries() end From e822b4bfe11e230d3604eadcf62d87d1c5e5e0bc Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 27 Jul 2024 15:33:36 -0400 Subject: [PATCH 40/87] add test for correct size while deferring --- lib/World.spec.luau | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/World.spec.luau b/lib/World.spec.luau index faf18498..70ce066e 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -100,8 +100,7 @@ return function() end) it("should handle many operations", function() - local world = World.new() - world:startDeferring() + local world = createDeferredWorld() local A = component() local B = component() @@ -129,6 +128,18 @@ return function() expect(world:get(2, B)).to.equal(b) end) + + it("should have correct size", function() + local world = createDeferredWorld() + expect(world:size()).to.equal(0) + + world:spawnAt(1) + expect(world:size()).to.equal(1) + world:despawn(1) + expect(world:size()).to.equal(1) + world:commitCommands() + expect(world:size()).to.equal(0) + end) end) describe("immediate", function() From a5a942fb4eba04b8e4972ed53499be0883255962 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 27 Jul 2024 15:33:45 -0400 Subject: [PATCH 41/87] remove entity from old archetype --- lib/World.luau | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/World.luau b/lib/World.luau index d7b7ccf1..ad2c7804 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -356,6 +356,10 @@ function World:_transitionArchetype(id, components) newArchetype = archetypeOf(unpack(self._entityMetatablesCache[id])) if oldArchetype ~= newArchetype then + if oldArchetype then + storage[oldArchetype][id] = nil + end + if storage[newArchetype] == nil then storage[newArchetype] = {} end From e47dc795c3dbc11cec078b91e96adc24e265b31b Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 30 Jul 2024 14:51:53 -0400 Subject: [PATCH 42/87] update debug labels --- lib/World.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index ad2c7804..3f95ea14 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -134,7 +134,7 @@ local function executeDespawn(world: World, despawnCommand: DespawnCommand) end local function executeInsert(world: World, insertCommand: InsertCommand) - debug.profilebegin("insert") + debug.profilebegin("World:insert") local id = insertCommand.entityId local entity = world:_getEntity(id) @@ -341,7 +341,7 @@ function World:_updateQueryCache(entityArchetype) end function World:_transitionArchetype(id, components) - debug.profilebegin("transitionArchetype") + debug.profilebegin("World:transitionArchetype") local storage = self.storage local newArchetype = nil local oldArchetype = self._entityArchetypes[id] From 4d4c40c12cc08f65d1de17bf02fbb1e88c54bc65 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 7 Aug 2024 16:34:11 -0400 Subject: [PATCH 43/87] fix error formatting --- lib/World.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/World.luau b/lib/World.luau index 3f95ea14..404245a5 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -10,7 +10,7 @@ local areArchetypesCompatible = archetypeModule.areArchetypesCompatible local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" local ERROR_EXISTING_ENTITY = - "The world already contains an entity with ID %. Use world:replace instead if this is intentional." + "The world already contains an entity with ID %s. Use world:replace instead if this is intentional." -- The old solver is not great at resolving intersections, so we redefine entityId each time. type DespawnCommand = { type: "despawn", entityId: number } From 4c1ebf1ed4f953ecaca40301d205e57776bb8b86 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 9 Aug 2024 14:28:45 -0400 Subject: [PATCH 44/87] remove old benchmark project --- PinnedJecs.rbxm | Bin 13089 -> 0 bytes PinnedMatter.rbxm | Bin 63606 -> 0 bytes bench.project.json | 32 ---------------------- benchmarks/query.bench.luau | 51 ------------------------------------ 4 files changed, 83 deletions(-) delete mode 100644 PinnedJecs.rbxm delete mode 100644 PinnedMatter.rbxm delete mode 100644 bench.project.json delete mode 100644 benchmarks/query.bench.luau diff --git a/PinnedJecs.rbxm b/PinnedJecs.rbxm deleted file mode 100644 index 052507fa90438eea3fa46986bf32066ebff47bd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13089 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 diff --git a/PinnedMatter.rbxm b/PinnedMatter.rbxm deleted file mode 100644 index 892d5cf147feb214cf43907bd85a877c7ad10cd5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63606 zcmYg&349yX_3k5zwk|W7U zoM0deJN-j8_ALR*QlMo|p=F0c*$R}UEG_&C6bc0rmKJEsJJ+=Dz5HUWqgn1f_nhy1 z=Q}emo$4D(Z7)0T2e(md05AZE(BFUmb)MASQ$lab|MS7O4C$3he~VH>aXSqH5LQ4Q z6NPfqne@e@|Jn2lO}=o^-?CJEbja?Gr4u7rsL2PD!R9>LQH3fDsAkLw7qkEVThXG4EbaF_k~X$P|)8=(P0}M^dfHejSdXhX@y=7rc#?TQ{W32PQ^CM18z&i2kdM{ z?&c5l%jNQd=-(IehyQOk@&bmV*{q%RLHE*BYD9OyyOGR@9g{mgtc(nE1W z2ebfc;pS*MHfU$ZM(i9nD5Y?5EHylmO4`Y6DJT=4>`Dv|k7lENL-XM~Go2b5N+btb zw%Y0FfbFE+W>X`nuF+&RF>L3WfUR;=4|i;(9kizDtlMFFY$%ZnUx-99PQT3j|JPaz zBmJo~opR3>025Zo>t53YRyOl2pb79Hz-{n_dpI?kv6mzU28ZY`E3bVt!Rc`*HQ;i? zFEVz!J(P|1&>8eY9c_9(c-@l#U7*|mzo7R$gXvT@J7in1u$9hY4`3xgGn^Sq#qAz@ zd-g1O)yZr$k+jn_a3-C4B0JWZ{0Zh}w?Vg^PN&kj0Ecizf0|~i2-*izTj>d&!-X^$ zo6BIuNHl2=l|lQKQ9FGCz(3&^X*)VJ+`TQ4jSZd(?`7;EJC;qPl1mertTu!zQbXCq z$Xw{&8Xcl(0my|%w#j+vOv>rX%86e6HNgLl|2WgfrL|5NXm@Hf9kcOIWEHiw>zmgb z`oh>yG?THGO-#ID=sJ8gIbwBYt25TN!D!a9X|BeygEaP5B4Z6l6 zzn&D4yX%#wNwY?~8MD^+w*A*0n?|BEn7tc=)1o7ZX$9aZFM!feE4u3XN0Vu&uwu*M zn2W|^Budj{C26V_Sea~E9<8?4T0U|mc!r7~YLUrVaSNZ6UyO{VM^YI((+tOzxZNKe z9m=*xv(W|C&T;vpb{`x!tvG#RW#w3IOAHOktCLqjCn(2l3cNN#<4^ChDcZ=s5?C0& zW-&4Vx^_Ap8iUbv()tpUfXraq*n+ppvRx|*7bHr+k4h>~t z6%AX|s>bqc>P)!PiY94HGudb|HURh%C>?l?9JkJXt5-vO7+;izlju*_@vxO33paeI z?1k1u#?Y^XbUZ>+MdyRZG@9y+9O7guYt{7Aq57h+%{2q~jlaoC^jl-8QEP}!J#D4! zVX|{N2ihr3YJWO4Y(*EK`WfIS&>?sH0tBqYrNBQ2JO$YQ!uzRYb=H#mwB$}-Mqa>$ z8|ToO&jtLBUDDe-G#WMZ+S+&Vz9vH-O2wi>*0Qm$(AR7Ve`FjN1p~{*jzV1^WGs^I zNO=a7<&ZzT9P&C}!NO)6j=O?8$;dOQVcTl90&;EwhCUu56YA~t33e%b?fS3Bl3S$h zEu)FF9n4HP>PQLrLSbO{33m{%07M1gZNgm+?b2TcU^dN+fzDA-zJy>X5(2&(_WfTM z_c}P|f;JiUhYZ8sK9WjjyEz_#GOO7Qb*cWK31bo3K`;~w0{=De>K}Jo1wP{_$EkQ{ zq(5!jJ8XFcCqf;aodX)ehTaNi$-{?fK3{;@^zn7rRt^dl(nzqo z;E!#i=`=aK?y*eP9*%%s3N7-;-O3EwY&-Kryl^V*i56xN?3K$?e*1x%b3d?;luA3T z!x1_X4gc2J0MD~8Rf+}VGPfpT@;C#u2%8q$Sua!t15+R`aFjjz|A&y9!FeCy3|bB5+y=Y}vY$3~=J`0q8DxSjk4R#p1os1jCB=8t{ zz}BF12Q*Slj)8U>@Tt(6q(iO&j97AaU+sQ_NRjV$*>j31VUCMVg+jF2k7}} zO)G`^WFT}77?0`)bQ4$}M3Csxf6ZAWivtl+l6 zl(miN$MnG{t-FCyTe_8yMT;QV= zK)k9Rr0rAN$SEaG!MidMt34%;mr3!3xaBmY}ryOG}^>UNGBpNh<;O%z(~?T-lM zL^O6gw#dthSiN;{%%COFYei!*dn8LPjsB+xMu%xmGZ#CsUh`&N(ZnoWtL)9BFiaCD5EbJTj8 zT~D6X8W>9TMTc(0CDQ6|(wIQswpUeL4a~2K5@?s!`H5(x^DlrldA=2h(8b3gU=9vDX$ua4`SLfqob%ue@vkngJgR@ zB8On0JP?-Ohspu>2rNeROt@({5hshgmFs>bh4TKy{rrPw z8GUID>)A9p<+*SdIp_?nV|_eq?R4M~AxZ>4;Kt=xrY{_Ivaf(^u)jwx80$1Ecpp9* zvTEmBL2`O}M`AhO8ns4-2rTtED^uxFwu|CsB1z`MIC}}q7hRm6PVvCq*dL7%)H)5y z4Lzke_rNMSdOd4FITOw`^a$aSWcE8(OV6Nf`9V7m?zYmS$&8gsMy!?!cbaC_+H8-_ zaW;_AHSq=tOeyY7{gr>|Y3j2HMD#{zFC*zhDot>0ft4H`?xTf5$5@d^@sd_4L91lU z&TNAgit!1j!iStICm>uguwtyV5_=Uk*L|GRVXbIZ?k(C+fsca!e#D=+54KoC1lP!t zeka&^fe{y90jVQ+wux1m!U5@AJ~KL<3I<>B;>%QM&z=jsoKJNq{Z6- zmEESG#e1Y>_9__d4HJxret~P`LQT^t4P#$_BAv-bcBx{Ti+tfknC_k4Tgn{guu0AT zz=qSTo%cgYQwey(AEK}6C9GT!-UPm;!x&f)&OuMpgQiW}j9cR_?(JsU82Oth+0aHz z_eJH>PF1=05Y{DF6ivLXc*j;CE?3wQ#Bqf$!s8$poE5hTQl@PR&_7`fgri1tw9A=i z>~{RxAWsvtBLk7J)kS{OPIqVRkr4?wbokm>LXIQbN4SUFZ>`lLXT(m+Mf@4f#{@u> zpD(|GGY{jZtBYt}$0jjP%?9SJ`8-DiAr?W{}RY1NbYNXVk%!4z`N6Nu(cvEn$M1iP+$BCg|&P8OC!bN29FF(7yy+ z3v7VlBw_Y5)pRy}5VBCje87Lc_pgBAT{jM$+z3OnI`u#qQ|Pw&>UjU@xI3a}6Uyr4c7bnkFM7 zUl$|H{)+cQa>X(t5fsc4teYX`5Wd|6_2e$=eUD1mNa)zkq=vRGQ!%JDQflxDWMX6u zVXJqS;j`Phx3`8pI+5nk8t5T-IF5aTQJUA}HlIEM_#{3MOQd6?WE2*ycwiUcep*>$ z7M*zFkD{uxpDQ;HlK<_a_+UkUDHcnY3<)gdl(Mo!|0K35 z)o&Q@q zvMKy!DA_oQU&%N{F_W-AWggPf(~rBo@it&Hfmg6PT3NS<^7PpKDjnk*k{g0L$2?yQWIl&o#0I2jwX{+4?T z5;ED6!Ah=#OsdGqulKKvZcQcPlW1AU?GssRn4)fCWQdGL=E^iM{dY0tTHw?D>@lQ) z+G*`(UMrz>rqj`}LCv3ffKQ3+GD@1BV*Cqtb*5Gh#MgN%leCGRE6yMUld;oT!ii*Nzsw2h zdKch9!%IHSv+*j!YwQN~-{w6{otf_JXgrY$h6>Cyfp)7iX%#>nEw6Vtt#pfUWp2S= z32fW(1Vz}PF&exyoIuehvjzBNfY%v#HFP&^qEVaS*^{9NBEi<7Lj88FoknG74=;;Y zQ$<-KlTpvwPWF~b5b!bTWTmJ{iRn^mMuMzarO4K`d==O%Sy( zG*){h%ZqPk%3lqf>t1Pf5PBWqN}h5|qez(|SY$^jFpZATY8s=IGWDeg=K*W-@vD{n zGMOTaOU55I0)6)13Xf0B&>$IO+8#IHAZgq^&B4sy6gn%8%^ebNaB}@oKA^D zD58$Z0ZqgV{nz9QY~o>tQ(z8bVH=%hY$eM%71`DK>@@Io{@Y|LxVeivJjMdAJ_Gdq z93NG81p{qSnSh-E;tbUGDJ}67*%`#QQxpq?1ZU&G{!m~(&pF;iY0xw-$T@xQ);90~ zLI@J8`l1m^G1J*xlRW_J&K#vz=sAMT%MpX-P%vcCWoAIH1D*zaoQs3t{EV~xV7|{4 z^*3xKT|~@&mj4|lSTu{)Zbo9$)B_sNGnxnnmVvriJ4;R|h0o+4QTYw?22VBk6(y~P z-Z%Ct^M>A4ZJA)Fw!XynQeK^kk!k&{I77Z&X3a05x8NUS2Y&UFNYalPEQ-}XY2^=a3gsXimZK1>|yK%hEqW7lAdg>QWz0lMw$CR z{JdOK170=-*g`N%0r@4+o=uP1PjUC(G`|nLP-v;qd&O+c>PlVQ6&_>y*exXJivXAo3JGjbbE22!Wxh{4U zYss{ZwF~zla?M1l1V@u+sk{uFMLIhX*g+vq@YFAhj_h;r@g-MYf}ms=u$gcpI1Yh22WD=xPQL=QKG5HWgGBrdMuwx? zgI|EyX^0-k53Yn*$eL=^g~sVxDU3SF)DRa4JezxmKP}1IX~7=7w{46##n?+Od<{n9 zL@~v(_S>S~s@~~EZAlURxLP$|wkI+Md%GPQ&DwwVc&#WiC>C4^Gsz3y3rZfe(1zi2 z9jKRdOH+-$qWELq6tv1rpv)Z@Nte6vMw49*-b}vE4i@4kIocsOLT)!I(M%#<#%Dx& z!#kig(37&tOt1a8?>LYz8>P@H7cjZAI52fG9;W%AV3plw+jp7%%r0n&Sj&^d7C+-_ zrN}&*&D)oeK5uj+9$jnI*;e&kxrNnXt1^{5=;bd$Wh8JtPldXfo~NY^2FB#$|_|OrGrWPSAtUC>-7yX@&(@#TBCkalp^mAF}Y=@ zhy3-JuhY_Xa@2Y>4_WN*MCa(eb=!1){pfM3EV-p2t;fe{pK7$x8&z&+`S2 zqWP(&h+b67I1<}LC`&himMOa#*yn|!9oc=zivi!~>O4?aftbZqA`0{;d3~^VCox*8 zWckaX}r+8ixO%2CYe2_ z;E2RN(Ftxfl7Ae@KMhAW%TFkGPA%x&3(kjuKcuVERWn84Ddf=dxc;&3XorFjIj5aS z0ilOZVpVdkDpm?J2Nlthqu#9;HK5)DZK3giHQN-cV0S#SA-N&hk?@!DT5c=^aRAI( z4Z7qOeVR+Znph@a$5EItDnFxBc|ql80iQHUeMDVJwmo3)hfR_%wFYR4ev14i!~v!` zfK5@^YYILh{Hf;(`0u!XeH&LkaPkLqGXUsmUgW}lm}G49;pe(JgQ^{6y7C*7X$+q! zQ2t@?Az)EprwHY85G_!U84&s~@Ihg&1&8B9@HTI8u#>rKzmwgJz6>p)ImpM&Ep4e} zlKe)KgMH)2!{Eu3>)*me?33|)Yicva?e7Y%mnq%~Nv}t5<`t>_xv)j@qeMsCp*K>P z`j}OuM;|j@mRYOJSBIk`R_Y8D-_&C=yNy_n1NIm2Z&_K!u2j2;GR>xT3-(wMropkc zq%LB${;Xt?WS#qvPc0Dd7vnOR5x08pRQC505lp#eYIrZOw@UciUXO9aUr#Ahq0$hw zdK=N-o60ldWv`@0e$0m_CVEe9Wv*q_*VfAy=syX1ib{>L~^{mo+5Cc!e&od+1u5+ zwtI1imts3b_uew73#QV)z^ugTl!1Jn$Es?}dzIrnGAlJxs*B6cOt z2t|zf9{d-+N|<;3j|Bb3CTlM|+MO6kMu#Sd_ZCXh?r~xr&~{3SJ#MA?PNnH0)=OXb zxx-vR=6eL@(cyaluPIj7k#pnT*eZ^Xd&MTmdDp`#8O{ZN=2C|`7udhb_(2C&L7QCv z|DK{Y5`{vn3x(W+j{G63`jS*8Lf$U6IhZ!6u0?gAXr1eRZZ6y{F>yCBJHJBy;bh$8C{Lz#dS}oqdkyifL%rX~#D1h#)fysp`QM-* zURC3WfWMX~`;$QRp;ig&Xygf&Bi5BDWhk?f0V25*>Fn4<879+2nZbGjo-{@hOIYK7 zQk1FSxUC$gqv8Qv0cL)&YA#eY?IVSMjMchc6xKfSD+e{RkrFRWE!!B*pjZD~QLi&~ z9aT5*2o&lGRH#HY8Prk?TFtd6%HTvopqYc(`)#3&^J-tjO+xj z{devSB%rLxdOO72;P^7v6AClY&A21>XU7Z&?7jzpvr!PUr z5c1{RKe|V#uktFZmVDz6pwlc|jqcUp)iH(cMx%jP9{gFD@6KaxaYBYO|Vcf{D3miXnvH|Fksl~Q?YaEk= za&I2t)i0q*vQoo|9X8d1sqnU0Wv8MPX&F zr@CamRX4p6N@Au>bOcIWTC|1I1d8SR*W0an= zsmv^3+kAK<7sH5;V&#V9b*Pj?4oXVWmbIb_8NY*xUuoK(pgObuO3=#{2U!@^HiCjZ z13oU#BBT`0 z1iHYy1~f5PgeRkV7FS*`a_CbXuPTC8+ukDA^vOBxLVtFWYZ|C03CBms&vIZ5`nqS7 z@^3VA8!-M9@L5n#mDayb4_M7j$lD;ezI-L*ZG0LflzG*GR{|b(D=pxmIU!`V1bpeO zW*(I$k?Yt5T@hIT{R3Mh)BiV^;(!_lhuO^PrXx z>U*5C*FgUQPPXI}T)~AQcx9<^5v{#KHidFza5zC6%-4>hzE5kQ6ir|qJ(BvMp2~@g z|3X(EsdfCe9PiO-hvHmkfxgf=lTc6@6a}Q^%cbg(@y}YCS4) zD&VOzsU&wbh%ltB%1jek>F-@HQ%s;2vK~ok`I=Iq?)-sw75*BsNr>mvt4CqgM0w;9 zG|NHBA1VjlHB+N9;mIh*eCP&!0JU#j+UJz(H(T$yM0t)*zjWuTu6&L7cY{3YpK~8; ziN|H4PB153J1@5&z8{q*P4;w6UShPW&Wew=bKGBrQ=R1z<8ZKRqU7g4)yX4Vo zeCkv`Isd}&Kdbf=L7gT3Cb8ozcRj(g)62&xo0lZcvfP=p$lt}h;88pjly8S*e%fn;s4$(@NzHl%tR0o1|3NyTEdq|Qzn0Br}rXms;Hw4&#k4}3!#aa z{o&LFiDWVa!hH)(4D&_~28r2`L|+U3L$TF0)CRVtqa&Gs$q11)5PS^6Hd(_- z(0*aba4M6n1F;RutOP-mQYzX}lqKo}$aGqwvFzyK{1!=G&1cGuQ+azxypgMJ<38&{ zys3#;{~x*k8A7mW1csRf=FTe6e}nnKjWDI@6rf6`d3PCC7eZmiuXsZl{{DYT-b*Qw zGA^IKV>aJ9gP&GY7R*$8pNRaY+9$u9G25A(V-lU9eV0Sk)_U+78(jEP)0_{gOt445 zaYvIm46HH~Arm?U_DcPREKE#oG|8hRB2pj|nM#$y)-qpT3(T3YYWv3`Y#WMZ_Prq1 zNsrsI{M%;-1M;u|G}{u{V4!&F47%<&i(OQC%sQOU%}5isRUft*F4kBJZjocsmKr^p z@15QRZat))E4IkgCv3$=MZP{4TBbHzb+4)K=~N3Qa1xA$BH2{ri?Au+mnPh|4J6I=skJGjMrpb=j%qO#RNte=B_$Ti+3eshzH$p4aJ)26Pf#hJm<-fSAR0# z+2kSXJE4^RRQEqwVByU7)f_gi{9XhXL?mH<1)M=i@TJmTN zj|1Dpm9tQLhILIqv9pm^0{a%K>o(H!158}XSQ(VmmjTS(G@6pku|;*CsI7c1(?3>SEDI>(VI zn36~JXS_Lj2WLyD`bo)LJ!pG#c^wsz3n|C20Lc@-WU%pE?9+5YEgI7YInleVG9{bp z-5pG1Xeu+=!?brGe@U~3zvGO~WO`k9vlL!50p3D-iokK`v$Eue0xve!(d-cD_&cx- z;90$TabYt7#BSjKh7z3;VhT0-xj=+(Dn+vppk)0ox!ORLBOon^C@6fzkomj@>Z{25 z{jQ7oxV4V=HRE1m@$y!`P>w zJ|T#T5)XnI7huN7NT^T|GWH-b3)-Wq<5Gv>oozm%TI}aDaapPJPV~1tUC3UQ(Xhwm z3H}j9Yc6kWz!NABu>#Ru-g=5tg9>{eRfCnbu7iq_zIAj_zYjFh1V5;H7OO z?WHv?Z#R@mwWPYXfYt5h}<`KJzZpI+@LzY?`4 zm}mM)%)FH8?gpyq8^kFSf-}~s;_t}sCE5h^uT=2}>H#Qh$k)mOv3r)7U+Et6hzh2c zGiH|WohbWI*8NJrYc(&H22^{D94HU&_tr*i7Z$&`17Y`t<4B8GDGSw{cSu zGu*e)>DTcRPb*RVbzGm%$^6K}aWv{UKSR(vM`^TS`HX}vKdQIE{Jr90=P|FoR1lU{extbO0)NlpY3OT{`OdVXKVCky2V-J2PzBf&;y zG;Qy$U^n^k>;kWK7ZA^t&K}Y{iQzi74%iKJw&+iPn*TDTg`~)mY6ZeQVgR`#S2IfZ&Eq+HTcp2#c(l11GK?9_r;w1cytGLh*H5iKUMdN%FZ zUt24=1`4ewPMS&kt#q1q6aUk4H|pXAgz!-D$9|V=O%q7MTGrCAak=hneaLPKi z5&7{J?SV$TR^!)b=6=m8-Sl@hix^n*w%iuwWZ;K2MfK!7tmx?G?>ogRl`m1v7ny?( z8vJ+6Grq)-;Ozqw_Yu`+>t-7OKh5y8Gy+Yj=51|}p3H#@ir8<2-UF_4fqB8XG9PaQ z_e`cgrPFjIGB*^NkLv+;Av%kS*wxIuz%^N(LmeA4kD_M2fqU>=Pxl0`H0~j>n5l`q z>g`;t%tNz~{mX0Kz^Cv>bb8|EBwon7lu5B;&1MJA#S0DhO&Xc)Om#i-SDdQuRn|M% z4cvU%1^JT2AGVLLc|$ zX;Nno6ep9 zDrWQ}H`yx2ALGjPpuSOMK2~(TU%wO7?LPB%9>(YNim;VeNb6DC4B~X3lNiav0{2r1 z5_4|lMxhX!f#1r_&A>bjHO##kO50}_SCqzg3%5G0*!d2Wc1QALWB30#=fv>J|l2{I|R=^8@WzcLU;k6I=`QVxhW+xMjP|RM) zn{n$TVg}8mcuXv3Y{Z34+?f>OenxSNKAkL{3V35bUuZ@{4s*CvQICdAw~=QSO%>I4 z{6^D#O09T>^P{!+ykcHAjXU4x;;*Z{^$nE5&zQ!~;rz7qgb&~ zV+!)GWj#gza0=Bq40`c5hB3gK(P;6Ck7oz0Eq|R|PvVZUIlKO&UrpfB1>~@xm}D8g$ZcmkTY2N7!lcLU#7-9e1G^>l;}`AC)QY0{L2U`z0!xp)N%tB}|Flvfzm z18$?R=!A5WR2Yv1ANL3wX%P79hlL4Ee_5IaC=S0uNXbI8FqdMU!@qBDFA{m1bu{)t(|HpLm^*U08 za9=2={-x-DMeb6x|2Z?~09Ocu%>nJ<79y2Z5*sS>>+dC^m+IoHWo_r@pqVME5tWh# zR2RPnb15m*+X;cp1Mw{^THbFp=K#}C{T#}d_fH2}KLb<)ZRR5l>7?bGI!@~8>{&-f5a^f?o?p4Hi^!~ueXX*) ziP#G%OMkuNYZ$a+n~iRl%zX6vEqSru#g(737D=?e zM_@g0Od~pYgi=z;?$#!fQrDcJh+=QT@@%~QOdu-sNIZ{ zfWWc>f|0QDPmTO}9wQM(CfnV=0c}uj=I)hB8xg2WwSflW&ClpozC*6{w~~Tuei1Gi zvpU;zsJs`=MhRvVsknElekR~YPH*kKHvVxIi!3a+GS`tlXjWh_3hTZplo}kWk&r-x z65~FpAfm-bOl~%nraf#TrC3HXi*JKtGmzb$3g=QebVAmA7v#@h#QE<@icK4SivF?L zoXu?zi=n5DG#<9HUik+7mUA=7sPOD2@)|crtW{D^MSRn{9aYJJZIBjSn2*e~+f0vVypQvJs|+4D>#*!_A!7BUNApHKShhL&d^rU`{N;RO=_>Nq`4I<_5-sOC z@bt{w&_(=LXZw@oV#&7MWYt8h+wt#dv2k{eeneiRTs@uf2lLnka6qyU*;F<<^v>WB zsZJwKfOHr~uqIWY#eJbdYr`_$R&XZP-=agY-G9uMtZUa={B zA2bjz717Ce^XuE0P!uZNO+sD`{`DlA+qh?iLN0GAIZxfBlKQOmiFmfnlXjul2l%TJ zDxof^V}ueH=G0U2^{~VB4`>^XrZ;bc)$Nj#A5a=d6i@{0J;S)x&8F)7Wg;xY5uzYg z={YY7mBwJm(4KL4Bj4?OvWJ>u+Gq{aU;>eo^N=^t!bFDOAUgw;8&L@wY_?E-!+SDx&KpUcT7G&d2= z{}dI{m=)q$3Pa=YHC(xsM^w0 zQ)3t{_0$)#|@K+u=Lb*d+_Qe}6X$buwL#lCJy$!Au? z1j#VItKsi}^C3mq=qSjXxpF=UZLa~J)u}7>z@D6qEly14;@1kRLvm615fa|(l*@v4 zB~0sV?>3Ol(p*M1Fw|cNc`fe=)}xgXlazIg#Z3i$TCbDe2eHm}DgAjvRXzd|@H)QH zcuWu2@lSNm`u)hR0{_Mw2S3}%PdyXYx@67NNw2K)MJ5iWlmqU(=)D5>vM3$Qs*IO9y%vf1n&J$iSrWlZ90y7HB!#Zgp?RA;iIrv7%eDhuucwEPxLBmMXUJ`MTCx1E>YDHMp{nxma2W_)MoW~5+@k$pKfIj3N{BEH;%JU)NV<# zCB|_9?Y%QsFAP#tqm^F=j6}uzU4*cq%jNXMx(y@?^C^4ky z$}bhX7R;-Fqpm9*UADN9cJvkF<0p#i9p)WQq28uxJ=77hfPKc;c}~Aw z23n`rsi62wqoSc~s4zbzQe*vVsLTcRR)g>@Czj)W0*Jqk;SGpOk)P~PRset0g~O=$ zsl|jbzmHzzXUf;Q4x=~ZMZCggdN8d1)>MgD6X?Kaip_t4YW|sdj0cyoj^fh59H5%O zQs)dh!>hn!J(MpRmD!}DA_}=0osXjKbv4ji=3@VJ=`FyIxm6MoouriZUzQ^r^QaAl zhJHw_k&q|l^!PJ7n7RfE*EfN0W1V2TTa-Wanq2gAFm8#GHeRP=BavyoaYmbVHgk-DIGY)bARftOkE!l2s3gMiwH)<2UShn<*~5U3YJ66X zT06qebh1T6+V@@R#QR+QEj^I7PgKqP0v@A|CK4bOBT#65`=&d7S@SjfJx79q`sVU`P&T-Gsdrr}h8d&1MV=mDna3K>v7&&cD zXGsy8tK%5*-|6bx$O#7(fcjSll`Hur$W|%H6zzXJ>~+n&%J^XtMKo=S)4bK_nn{r0 zLW2!BYA6WwVhLBf9LB?{My?4|Jdc^@It^tr!voI3kjrl@6%*`Qv$Goi#(2LdYq!tI zG5ZD24^JmOXf{vK1r8B@%UtT>bA@uKKsHjOtuaLrW>7fHH#m6X`@!)-9`jMH(&3JC zcn`XAw61VoGiyuVg zv+cg2?;)J92atc+t(;2DU!Cgc0wI4M0zjy*Nem$qf=S6iMjxrkrdZT=4e z+rL4H8IC`fa7|%<*7#8u9yCd0q1=~sUhi<$O%kMX-h|422d)H?2vN9oo`%M`z&*v3 zGn-!)fj)$NZ4%^XzRoSS6PgScbi~8aIs)GzUTMc4ov)mSrm1=OKMUXQOQnWnuS2gS zxqGQX`8u$NOG-0Sz36yBRmTdMK8Knq zQ(mQPr4hmpk|fOpB*p@iPAHc_QdDF9e~TsnjPz#xxUKv{r&Y z8yhN(*EB(l>MzMG-Re!$W)fppfH!m(C%#De*s~=`bsnl+z0X()OuQj~+geqaInP%| z&3A{V{F6~z3~fW$+#=UDx-?iUk2RA-Z<{uK z*Qhu#ppW0-qa z%q;=D^i=Owui$6E7E)I)r()vadB3)p+5%uqz&5J2cx6WGFdoX zrF}#yWdi+anWFZO4!yJid-*Yqf+VSChN)|edIG37mU>&LSh}+V=c}Hkk0B83r~qGE z1$0vaZV%l)ksGw9PTEkG9sIBa|CHnW_>kjfYJDouJfOW$q2+^C=ch-tXX<|1^>%8o zj)#A5Di>+&6y3GUr+x3w&s!W$%Ex*f@)1^wee&&F)v*C4}$w zDOUx&nO_w6B1-_DLH|aoV*r*9jm>6!0r=Jzf?0^b|2IJVROQgvyFT_SFi5em8vH>@ z6vEsS`B@x4S6e&dA!DESAYCMN$WWKk34{|1aa8jWA!!}}Q2IgXtWq|xX_>If9BBKs z=fPYL466*~17g@-M8`4|&zA6;>Ub-fyU;DMR&$L;^1mTUa?_u(f?x@_zdT1IRa^|z zs9vgZ|d>}1V3>8Y$0V2Kc|i}BbS+1 zF@wfhqqem_DUM!PJt3d^UNGL?eXwx0xePpbnDMyV@we)UiCN?3x4I@m@7)f(7WqDhd9h>JtN*tnNA}Wm z>FXxeP;q`itXJ1kXpCIf7qQk*gg&vRMT&`y&}u0;4mVoI7*Q>WSRy+#Mm1{++g9IV ziN%)%_DtwNmySlqY|Upl zZwFc=qqLA|h_^mtnOEv7@cfJFNXZDxyD68*ruG^6_CDonfebIcPqMK9z{y`EF0*2F<+PpefSW0W@zjxTondT{-G7PlOON zF7>VW)uD4IdPzy3^%xQ=&NnJ^qlEh@cr7vbX@&eK$*3cdqn79s;uyv>n=a{{2cB6F6p(fdw@t|XMf-->V@f`hsMU|eo zL?dCSHy&20D|4uyf`+b<0p^497nR>bB0IB1a!XM&DxV_B~h>wMoqc+iuW$t z>S*hhtwe_2nCH!;c~2@vQoSqa-LAH^-eY~lY@DwlMl?G`8)}XKCB}d%4Db;P7PmI=*j*xua261*hiJ-x@&3kCKDblIimxa(^xXNUtWT{rPb8exEn1}{xeQRBKT%|mp!0J}IwE@Kq zI*Czf*85dnkh_J9HbbRBXKp#I0rJH?cyOw|goz%7sl#Hu>e>x-o}R(2Dtp4-YxPnW zGUfXh=RW$M9zcuAftg<`{H}xEDDk!DTo)S6m}-Tg?*@mZY2TM0M`A^&lQ=q*CE2;` z!ntLU<3`1|WD0mzOd+9knzhU3Dr(|htn6Ast(wHhg;z0lvga>^PRX+5>OPeOK(Ya2 zw6C_D?(;w=O|XoVHPq!x=0Te5-kneDO1Dx_=K@dtO_Pb*iH-M09#Z@}XH)y;??W1u z_wvGEPJyYLXfceB|F5En!aVg^6|Z2f<#~a%|~#GbsggM3Ju;_+Mk$2c`?5SSuIO3=iL(*0zTOy&Pl zU;44ZlbDwN#8CDDah%!1G}+szQI0;N8(-Mlw19euiYnHaahND`(s;|3Obr|R?euTU z)Sn<;tW^z1zu1i<$TpwCx7Ye(H#zzH#pdHh&aR1jd^}x5oeO722gr3$Z|^}p{-dPo ztFZf8y^YjWwOU@hyR?UF>YX^4sbB2_xGh&paENg{8qQ z{zBzLuAQMOOUR%D?2wKZDf|-M6LEtm2L2*aPNUAFB#2}tSPE5%t;alF zkyCpSs3a}S0w8%Sn6G&qtj|lOJ~V#i)c+>5)V@Qb~1K(a{! z&4-3~>)3TgY#2Q89w3SSR_JP?84b3>D!M61YIr8!AW8L2zeBbLh^2^1mun|q&+)7~ zX0A+7ULg6XMifadoA)c|90U2<0`!{FUxwUz0jX7Y`Kce(;Q)U&x*? zwJ`X`E}~Y#0Q-oJ2F@coZriDFgOlp!q5?dD3+-0T>tx%ZAd%5U2I|P!KAEpvQmQxs z8=-vTdh+{gtC$NGOPEZ`+nFxZfW<2Wu_&h@7DD4#XS^H)b&&-@+W&)2XTLeyJQ+AvIS8i!$_z86XxiBKw?u@`)}t4KSH#5K7~K>Zfv zQ8`O>-OmX!_?D-Vgc27`9By^MSLT@aPqT# z*bHi+S~r@Ey`SGDFQaL3E-ubtwADdi-9n5rK{qa|B2->R`R}YeY6NBbx%g*MMZyj# z)OyoEEzv1WA?jlTabE#kn#b=2B13%Pe@)@*bDSsU@E%7%UuZ305AZuFTkRt4Oo9U3 z0v!)0X`+S%&P4GqH&3-poC)K5iq~-;|4>g zyJBp_iUIqXdAv&Gd{@bOLCu}Ps8;@_K%uZ)aJ!M3Bux!PQOA}dZ|@q1@>X?e=r~wu z$!n0}=L2wk2vRQd9CZycdAt%;{FIehr(Zu=JV7ESFrF+c8xOyN{7?D(FR*vTkG>_d_(%@^z#;t$N+ljH7dkxo4PCo|n|+wul3mm|LaOYNWJi+sa=O2Yg$LC{S>;*QGwz5v2b43|vk+ev{g;T&I|hd-)Lv59heFNFSKm_EPn9Y9 zySls75bfYN;#c#WEwt!1{*ms@at;0t*uB(NW-v;$aw*MIO6v!~;a*_S>!}oel+uPh zlu9=)qz+b8_K;jsXE|W3a3Q(e{W>+=^3wWJaF#a6hsHbTwn=&JmYy_ z7wb61bQYUDb-Vht;Me4O^aJE5xZ~Vf;{wFT<@$3yjKaT9qg;FT6e?%4Pnhz6P&|tJ z<2tmJ#{S5rR#CaW?#qRYK+t$4vKu+y2U=Qfk;3B-3&jP7Hg^fT$H{&nlqvp#Of7Yu zJ?LQlin5Y=H>G`#j`V|QXt_b&)`_G&Sv&L#fsbhNrc1?Oyx~{oX=6R99(lu-8gNnZ zjc!ELU)s29;lw@L=&mjQhp6`ekFrYN zhtG4)dE2x%Gm}a}GBZhF5|Sa65($umB=nF%4G<>D1V%yzG6_W%l_Dw@?25gwtGg^w1lH0n7>mUL)DEmm=XJEdAH0DGVEuMxCa|Bu|D%ifI#OKq zx9a2@;8_*_pas-eG^KS{g<*MAIoRA+EIKl@`?)G3Q?47M$=1%o_<$oobq?`Xz}I*E z{B|{*?tbrKUbj~cPduWi+=VG8n3Bk8iaK6Eqm$_4t7VOkdxLZ|m-h#D!C=onKi0S44qa^TmIT=!~0KlCeDsP8Fu3{K`9S&B zDmqHkoz{-_t}j;Nu0(nKCim9;M)R-XQ^dTnzSziQ`k9#zU;WqP4m#m@Bo@KoyuO{6 zvP}cZw6LV?-7K2Y;h8?g7n_pB|HV2mE$CZ^N%2lriQ4;{khqO@N0WgNPHw25=z(#y z!~Mw)E{<~ZnM(5+-l(b+>`hHkTJ0zTEa{;87YB!jiu0%7L{C7^Ooc;b+gLjote~jm ze279ED{LRkd7w3len}0ALUg;bvukAhp@l_}!0IaJZA8y`_-*$<%f^ou`H~OgI@ajl zMEkO|hrvq0lxt+4pr=D%QFzPzwwS_r)Gf};=WjFA?1PX@Z^@x+z1J&+ED#15Y}JHl zO>qm($_U;J>ZZiTjm&=Y2KnBX9$4#$pnMR+w`6u_*6c!fVY)^1l zFcJ7CWhebhv>PPmi^I@7Ht^ZPG$sxdl*R(%;DD1d{KK^s4U7T9gUg7wF)*?vk$i3^SPpA&+}E5( z--uY$kW)r|haP=CP$ z#C(}m;Rg?scu?ne5(gz68pD}PtGLd+%%B%Z59_>D&}IBv4-}aoOKnzHGXMu%zzz$7 zT6BQ0bR0ZZKnx&i+lpE|#$t4QiIKrjlXCAVG4y(!pDcYzH%QGx zYnR9>TuMI*?L(r^`1Nr-C3-GA^~$^3`eALj3S{gNUHi!5Piy>kp3}cGfEA)%19}5L z#d=|B&sz=H&lWUJGVl~PWdQGac!D=_ls6~~xK%`-fZ7pmg!CtNWqU?wArpUD!%KKO zX5nRleUs=1%;Sfi$r0aKj;%foQ`5gYw0=vjw6qYTAIE+8KN~XDP{)S8&!7P(EhA&O z&&Ut)Tv&@_n098!R08n%OiUuj(PE>@;O!{-=T_6@b>bmBjW=4S2FfLGIcm`92kGRL z^G-O_41T?_b0nqU9@^GZ$S1UU1OJ#d1b>6!WN7s)t={FyUXnh0*k#|yn0>tq@7}Am zV>(hRA7T6^O}k9X2=|wX&t~x@TIQT9IS*Bjg`+}dNiXLg=0aOiZdqk4y}O9Xhn<;@ zSE|zkc?`r`IgstT(o4sjbPmY_S@uDfEm2~g=;3?KP=DtKhCLI+)`mP<>=HKll341l z8tZwR=|9w%0JEQUi35H!n`yst5#wU-6mu(Ao#H9Fd~UpbKl5v$g?g5s_FGjLXLRnT zcANan-NG|V7TL1FrDJ;Tv-RsqdmJt3WDX6CHj&TpOANkukm&OppA-Lxo&P`Y#K3D( zRrx26QcMbndoLoojO2~X7dTaTd#*F((?a`u8h>TcF5F5f6Obo_J&Eg3sA{LC@nQis zG7ZCPXmH^&SckmM9EFUBNxZ1Zj)U?8wH{hC!Iw)6F7WZM*=}51fG2j8d~Z80>J|@r z?O%xNcVO6b%$C<>fQ;TpO~Cdr!kXA0ILl3hkmEZ z85d7Ja|_+*;=eVjlhG}Ym9?vFsq7O|oOp$jSg&AaD+_IjReCy9NfxQ)A}(sd_}NR@ zRyuPnCYoYtCyTBW3FvF0uZsO!TDX|H-!YE?Uo#(0qm{s@=e5H$CpMhjq+iHwyGc^5 z3(f6KO&u3StT-E{XFVA;Gyc+1M1`_R2B zZ<@Yllh#Uq-FFiRMbw#EN?X3&@HU*v|Bu7*CM7`WjSj<#6Gy6u`5KGZ^}W%PP|t)m zjczSswGOMKp@<&Q?L)1W#1J}Tc|L!P+1DAw!F$+c(&`-B8pC-p8x#6Sq}Sc&v_GF0 zzyw@8qD2+$Yos~uqV2Qg7oq`c)WSFq4#3rmPRS??)Q`E@sv83iIuA-kI5X1%4rbpknZ2ql7pzO8$0G)*R9<&9L17ZWReBcLx82%~4-{6_LBE|Jtp}$5jE? zXDZ^vrffRw_a(nf({?%iiCO8i&lwFTK?_PcCjzaXoJBA9(3`gS-tXOJPAME5`0TE# z@$g-&#;h(HP*0Ve$?G!c>WnH}Ud338icqhdkS02^=$_dWu`nEd9AH!wx6|5`ky_>? z6n0-`2K!<&vuRsKa|sH@%ZGV=5jBm;8v2(zG>~{F(-;3+m%nR*znCNx#ruQk^IiO0C4bd}K`pY)uw4->5^)KK%Vfc*_4b~D__Jf?u zYslALQtJzUX88-zYF%aLByS4x-MV~L@L3ui&GvOh7=U)i)_k!Cv!&Jx+^d;!j)q2) zci1`z^S$+|+}x6{9TGaOWRGJi7;P7U-1%*xtFUOI1y#DP^}tcZOQ$>NbEj`OhuJeC zOhRRP8lynE-Gd$TF0cH-%CP=}M!`7^HQPh3P<&_+qY=IdXdbrZ(3Eufl5M}t3~%>U z%(5fCSq}_>O6uH^E=#?gDYg8FLtG2he#xIS>0=!!Ddg2mSR89$3wIv~fd4^bbVyTp zmZ)*uG2`u<&`e^oyyRu)J2w%8ipPfA)RhCXm6ZE%3Gf~Zwk7ho+V!nX*Z)uL+PwK4 z!`_n=_C+_D+H~z()*p*)1ilS-if|OewIR`~ipy~H-Gdj5D1vtglmASX)0@deDHr9` zA5|u}y=RIgICz2e*BhZ|3D&1;#l_LhSfZod>sR?$Z?F2g7r@}$r-RYWRRO?|s+2j6 zIkFY$UR!U00A;?*+Bf4hHy_|4%b~DiXwJ6ytkrG$uM_v+=}yLTYn#hqo-EWCCJ_MlhdcZHglEoyG+YMxaW0)UXscDA;4)q&9(z2D8PtllnlB$C~o zas}H}j)`J)FjRue*hM1O1@4G9{IcD5To$|_Yka8e(o9F=HU8dCV;cSp{wSY8kM33S z^ih%O>zkMu%d#7K(#;%}+nB@jYE~R;=yuaOs}hop!g|rcGGiyXc>x>KSitfc0<66u zj3uO)>HW;x_udKo^(Nj|;p^Ygq*8^%b~a6bi{5)5FbgU^9~+yxD5m?ma28%>CK z6mu@-kKHD;oAtiYq4h(FEk8m9fTI-Owm{5+O&W;;q+9zegO+Zs#rm2vV+QW$?^(9~ zF4@;g-Q}SusIPEXI7=FtWOixVTSQ)NfY5Y!Ax$K69u^T z;Ap8P)MK2dtzVRyWaKmbFuAUVrf-lHg-f>Ahr%WB-5O8kIn8_m^Ow}+LR)9N2tg=g z{8nYM=jg$RLwMsyvgv0ZY%J|F*nE`JntPk{Ro=|_cJgjKKsA_J#)uc-KQBFu_X?;F zdpqtX%3k~2az2-t&k1yL{0B4v+)>FW<=}^tVJ;CI(0uJ!v51+slVb@mh%~6B$-XqBmlJ0RtWDaTrig={fu`qn47B~9v8)-riC#EVg{DqD3cmWeq&R#D)--_e ze2N1n3CY)aQ?&zozv6I_&4`e{lvtj8Wpm}ZXA^!m**>~2N`4# zOC#!-v@7orbhq%vk+);Qt!m;$we1%CBeg6CEy3j!dU7G%Fh%U49{x6FDtR z(}o~dVQ>l*?-GB3ng1uv*H~|l>ePE~WcKUOCe%TT6*J`P9)W3O2e&R{#zhR*259*O zl^ZoFVv05pDyN?@beKnf$gIZa+S3N2V*`atoabuh3z%+NPCi$kf{uKI^Ec1*);$BF ziNn0V&F~XwExb>2OlAU?(;pUF7n^*$O1~f)S7$%%uzBxn`b&!m8%(gIq9B%GW_q*R zoB?&ATlW}^n=!SS$c@Fq{iVUbkg^+|pB!Ft_IfTgP_vIP`z;VgJ)TNjD~l_{6(4DP zb$cam;KS0nifd8b+qSOA@w7{yOzvA;G?5(&Z$}LYGGzcXLFZ09>cyifKf?um2Ojm| zQFUO4&PhhriX`Pno_|0*2bWJmH_f}*@|@#{r;7&Z-tOTK<Xfof+}P>}qo0QDN9Gj!^Qk1=zUTA&GGU-~|uzr^Sx zDfUruxS_#tRb|Yv@cUU7)MpfB@>8=}?XR~mvolX`3^_*7pCo;Q@wc>3WKy3=b{wzqFOq3wTxHV%o1g5Y&1NM;?SJ#3Vb%h=T_r=? zn}DQ2vE}w;gt8d_(Bg|lbUtV|gKc9&X1bmkLh*L?T^y(XRNHgV#+pBQ|h?IorK zbB%>!tRK8}@q`}_xP2;}VP^>5g?g`V{d?8+PdsEtjC}1T>H07igX5g0F~an59^;`{ ze|=>}a-AVwxd`ifhb?*x7zm0tC!)}DE_)r2deGXjU1}!gR#;pZSbF#ZgDc29uQNLE z(?qLJ;)9&Zg0c%L33$D-uw9PP+8-d=JROxZNr<(}AwFaB59%*12@>X9hVOE~o!^+; z3do<=!}$O*BmMc>i&Laq+(S+8={A9ev5AWcqZ5;YKz(O`wwm_arp~{1+pn9fJU(zb z)9wzSk~g28KMh?vD&Cnvvu+Asna_V*fhjE=5n$zLS(eXw05gDLIPrnsb%Q3b2%eb5 zf7LB}jI&c@bvLlaRk#LE7O;J~E5L8GCYA*-d=6Euijjt5>b0OZnpNT(x)M_+WrRsT zF}NuB!Uog671lM(Sfgv_TXM43KQuI!2l<8ID5OYqLl5f$zTZEa&LHQ9__|~&=i|mr zp)Do1GuJ#ucQKx;sGIbfMTJgG^Ef0Qe`9n|@-`=R1>}CRPH@`2Tz5}#`mH5^4`;J^ zf$~5lL{UKpzlLD~btRhu;@wr8_fu~89cHX1?GlIfOaXlw0APimRe{qm3U3!?0CRoF zXM~u1h80w-gB}671!Zsw#l7cy*U^j38e}*oYq5eyS+*DdhkZ`#=U{_^ZzKw;Igvbt z@ekn1!14+z&S0kefVuie9whju@LxzvFLHiL;4T7`T8F@>n;Zh(Ru&VFjbJf~>8|nh zfmxW8XNepa|B~A~Tz>60nhuS?9j<9vW!Dn_V!4n~t^>Qp<+HvMvX04Z&+`Gj88=m6 zw~(1$zATr1L-GTc{V#MLcjfq+1{v>S&L%Pw%oWqAR;OJ~>uLuS(eh%rE}J&=h~oE# zS)OChVea(CWn+v7ChI1kGf|t)7k)jQoagSfvqRrOq*j5&=bldazJDrpeV| zK8FXbrQd9)q4B&^*N#ZnU#u)sF|K8#Yd{$yl8k?p6L%2c^sNyV1=M#({ zBC*lkp|;-bQEne$EP|X13MU;pI5myJ54kf*taqtzbjKdKR|~w z3!;BVu@7s;Y_c!Q|Mh-)Q)pHJd2sojk)En78G?S`9?7-!T_v z5lAx9vH5S*_zf_&=h{;mw@r@1XjPrInQ2RzI6Rg&GSFNwRpNJZdnOY=P>yh|P0$MN zoUTfdWRzPq_UJoEJRxC->764bG3EG>HkPX3b?{I~Uh7Yh|hCrP=D2-)Gp8DnV=-8^egf z`YT};X2z9(w4f2otc_QPp~Vcmq0^Z3R!;TXuLumWt2@=13oJ>4AI0*Sj5^o5{Z$CdbV#Ez3fpTiVCzi zJ4UGBz~aUySb}alFP*cK!mH31IgfL?1>q!ZsFI2l()?Cads~{h2(kqb$ z@uhAqWE~1Wupp+9HjjY#h_I(b;?~yP_@+Vwn`SWkHD=7hjS=|8#y+TF*E)uiM~x@N zh7;3Tv#nd|#*omiN|6=i{nN+Ji@_ky0fR?+$HWn&3?unlSUT2PDCFO^r` z$0k<;J=6@iO7dGZp20n5n%#Kyk>q8X_A&8ixmL@J%QX=LL&}(~esYcpgGK!gPCtuH zZ3EC%3`hH?P~`&cCc|}U&^!&h1&+ImkLqHx?LtF-Tdm`IgU*)@A9kX53@q{L(Y^Rgb`D59xb74Ogf5a)8C&|khP10Y*@)hk(*=>Wbw}f05jr07DfltnV zuipT#{-_eY2kK$;8TkN+56*)_V1B+=(ULJmMlvAJmqsBIg^d^odXx3KoC-#=^jXQ7 z=oN*VvMem}xeT(dnm}owkfG~Mx3ET6GGjmUTg}Yg#~c!B#PPZOLJc-bj>7<7h&D5x zE|5rJo=l13DR*yad=zgWvNbT_QFd8mD|&o9+Lq4dZYv4w$a9E30|kF0Zt^e+(!k5K zv_!q(8ez`Y8-NJZV#SIXtOjlS=RUKt(LN!Zqg9h*uiMCBj!o+S?O*aKrQ`2e=PatM zxSDgQn&S@ri8^s4OV5#ceLE|8T!6aBAC~%+#aOrK&tqsyrRzoVH~s7YS|^?hE>@cY z9J%zD{D9z%om5;|fl0ttO1X|aVctEXeI(Ht8+mk$efJD%P{C|h$e;>VlZ)%=Xhr&* zn-cs?alVqdEK$vNGSgpi_6&dDwFj5R2Qk9mcL6wA+Gh<0UT!Lrm!{XCuLgTLyoG6# zW@8tj4NN$|#^|GAid#Kk9qT=wHQ&#T^*v|Ruvip z69GeL=Un4r?x=;ko6yc_x+LY%uD*XC>CZFyWP69=j6}yPF>Rp3X@IW3487h z%sbL`5~Nky^FsRrtHFx3n?;k*u8f4s!JK7c2W|voQg0ODOWN4D2xxjI3zh{8oBxZb zVIKXXO}j(}1NFIH4RVV{F{XPPi>jR_R37m{DI>eHh+i~I`+_OXtQ*GbKI;*DcsRs`E$| z0epKIYRlJ~35)^=QL?ZQ1;2w()5izL8zfw3sP>Uo2-}E_DX)w!3&HilWTTxqM9HDoz2WgHqbZ3kg{h?%g?!kCU|l ztQd}%6&g9d9v$>`YCWm!&M_V@m0(?2I&j=Y=ZBz5;cTb`2~PsrOR;nm3I+~XJk~en zp;RTcW`NdBqVKx_DmCYhi#Q(c_9o%#@yoEce@+$r$Y`zEuBKq!I}6QmSJ3+dzB&0w zj7sz+%hq$MCL~-(f?|oZk1~D%UPpL3FJvndSwvjDOLo?P1#Eh5Y)WE$XvF z*rKn~-5`*#@e#+LNy8>^l)mMqU;?1t!9+cvD$CNk50))XDTBJ^+=YA#j0zCOmJA$Q zQn@bCKlw~==g}Txh>K?k{*~H`X-|QZhOz_5@W$A1{{nLd4-XFBtx~$a0~N6&KiBP%@n+1(0NO-b8EH`c8EYb<~i*2GX^L%a`~)J9%~nBrF@ zcXY}9J#zAIwAy?yIcD~9EW`5O)UGTP91qgiHF+$!CQ!rl$GM%u3*Ffqcfa=h3OMKMgShXTD*L?sM{<}a1?OPnn%tFB_@xbpJT zczWG5_qYYys#vH#&@J>NVj@BdzA)*2NlEv_^71Us{u^eh?Mh{t4*xgGv~gCaI-a3p zy?ntUYw}nSU>V(BVYIPMB@mf@dWH7o)iuDI=SgwKJP@;kfi2q{9av|Q@n^mIpV;{E z$-fBw(X;7&9t@2BRsM+UUB#lLGIy1|P830Jf7TxF>r36zigp>;m&v~wGU0-iNx_KY zv3=6q_==;(ghLGEwnlCPRGAB{DT3gPgex5%vIsH4=tT_#>3)?!o z<}~#*wJliGwh%^EJqsE-<~4UT1^M4Bd4GwuXH9=t-9}{>PNjqS>5lK3cct)l3?b)v zjsT=cgsZT@Bts>8JS%6aR%0yl4o>3ykCnw?$Ak%dM*;uBBkQ`1Tv$JmH&i9)i*j$M zxdORv5+G&1R)Y(6aKZJd*jp8{mBjR8jtGVv_v9(k2Ri*je(W(u%N{KqEEEUSs4%lg zP4pJEH^3Vhsuf@T84R&tgVJj~`}YR@dmOc#qisv-8S%}%~I zmtTCbb^+5j`t5v8*EK1x8pF4yxi1_;SNa;2@&Y&tDyq#jJP&Q|R0y{|vH8nc{A->W zUICZ4ae608{qpknS5UyDHM-^1>5&}qg&&9j?@q5;7~5hq;7oi^j&@C&7#O^yApl!^ zRS-R{7mg?Lci@{@!9`jJ3gV%NaE0zyd9_ZQo7L|9mMA@R)L~{SCB+vbSMgKtX25>|LL;R zNNd4864}|SeWuIZ+Kp=d(trWt#%)K)^F0IEhO)TWwP?(mYmhwQ;>!1vIOYurtdr0r zG)odw2`njUmu5r1pbUyZ&;t`KiMc$+vD!f$oDZcZy;3D?;+wQB-cOwqtJKw3X#@e@dbSQa=>zW~K>yuS-W{Uy2d%h+g2 z*jsXPPHN7d7|6o_WChV}6>@5(7#htG7ME=mH*fLQy(oOqR+$xAuYBdJk7)d6 z6)5HU7z0h@YdvtjQOtOYA=c^k#Vl_C#5J%t|83fr!`SAlbbb|jY9g&$AjaA_ij~u_ zPbAa+k&Usw=b9k8ij*yJlLm#mw41oos;5TPb6(9QY^ZP2?RQZf5$w`d$^K$xnSo9T z(x{YGd|c`kY}0YwWbk9N$g)o5-m*hFaBQ=6*sY*V;>HD$;vnSC=Ay5LZF z4x}99bo`pbui*A-DiAvjv(;%VB3BmMq*}BS0-;81QK^6g1m&c1tkHR|J;~h!*lzHu zp^RvqnLA?-+3U#=y>bfbuBlFXJu73&JCsxQA4Nuzdk9Gwd!)O7!Gc`ch_qO&s;;9i z1+)bqBKdSJPm^Z$3|zvW`g~35N{+d-S!`UO-f(9}08o}NW4)#!;z|b)dE7k^)`2Fh z>ug*E+Ch>JO2@j@+D<%Ow#gHIz06}rGa|;>nbw_B|El7)j9dR78*FG?V}AG1q|b>#D-WHOo;C<>_Ry@6M5My3upO$I3B2 zr-KSwgDU%Kka+Gfji5aV*XZxaz6#U$hMpXw0YE{+s}3u;iIQ3a^z_N24v{cvYaM2F zomKd0^j*ul{Zwhp*yLzr4eFz-c|IDEoTXLDg8JGqboC~X6m?X#aA+)o24m6MXjMe9 z09l)20e(o%CdF#rxlSDLP`%TJ-L$vm!I0N${yjX4Zy9it`sg}*b z_ru2+QY{mnc{#P;EvN>~5Yyu}imG3%sanN0S!xJYTjOGmSu_$419hAiLobW7n0{1DW-Gzjqry3RA6mGa(4LpI%zy3Ag{;?*48GO>#{n0 zU4)K6WQEbq%%FQ*v`x~TB-U${aEjkdG?VBV!bMLL1MWGVjc1qAN>_;bN%g_cFZ@5U+ucYT%*94~BcOgw#AKnrf17GECk_rm{y4E0Im;6_q-3}glf!)z=c^yx zRn!@QPr+!mCHDz0R4rn|cyXq|@74>q@A#UvLLj;Mf+Cv0_&5G86kXk$7uwFlYzDqO z%t|X~#rwWL*+1OxFGxOu24FJPCd2*ZOnVB^z05cNV3=ND%Bf*etuN5}BK@Jvc%J5{ z9&Q1QYM2fw2Lv+aouGO!I@B};vc>p|;t|5|m3s*D;%eOf7jf~HK`11?%1|C4mlBT6 zHB^N!X9MwPBT04dZ$nM>3oU3^*3;RrVA1^MPFz2eDo(Pm>;GRgOwxei*AJ{3wS*Ty zfQd=pv#b>*PUWY}F_x`Jxx4#u^KX&RPt0Q3#h0-gQ(pP0q}U4{Mv%~U}bPFZ^omE@T)qDv;qL6^%H0k-uFcy z40y$LEY&Nn_V1t`MAY=HAD4s87&4>2^({&a5*`BF;2)QAs2SQ@R*8r2TKt8aXsjPK zM6#VV;;E1}=Pai!is=Tm(!6V;$CHfW3r(X3(@rvqE@K<0;LdNufM2RE% zGcf+`ji^_Nbk6matRT-ZxN0$ktob8336xfH(mbn^nQ!25orcjD*lJhg1AB)%DyS-& z81l-^mcUHE2`hImR{#CX2?E29b@S9D7fkcZHMxlKfYsa3tDKm+sVq>BH9^_b8}xiw z<%DU!WiIamF;pSm(J10GW)Q|&zWTppN&s0v%>MxHJl--)m3dcLU|^_ZYPmSpj(Mv% z>mA1_TvdhxlQV>mGQ!}PzmWx+8$W^bRt%VRKDXey|KHj%y-K_#`4rZpsBabbqvSwS z7w2k@}TgG=}h~F@?Wivn{vfnRT9 ze1tj2m>@vWe9D+_ zNm2ZiXKPGltfL5;%6cGa4~*Qs-;5+9-h66f;v|*Iy`5p~M8 zAGz#S;6ngidrK97QQE{HEm0R({~hP@6O$w}Q5E6Yw`|$!Exm{24YZ_GSptkZU!D+j zy~DgE4`no}-M(k%w-R!mz~Z4eMqoo|W>DSE8E8kc0T`HZ#QYTQtAP}(c8qq#S&ysd zR4l4t7<7DlC*>bbseCRemZ#eCFNM>2JUUH1lPHSeWVm$|Fe$>Y-`FhzRZJZ{bekp4m`m= z=m3+K=3bBW7|*B77B2aKFL4GVfnw2wl<=WvcGTdMFPy&;Cj*(iNO9=pX(SMU@C3Us z6;i82b89(G19zs3U+M$965I2KAj(<6K=}IlBXCfS1r+@cLijoSm_Q;f9L*rlO3{cD z!=VyLQEwsnJ>oWX(0kW{xBJ-G>(CDaLqweN#+Mv)$qD?4GxbWEg0^B=CZ8Xg5Km6u zekhff2dD)3M$0AeUMyjuqTy&Sm3IDxt|RqX0sN}m zU*zY#BlJUJQ%Rr?4#TlV9_)?%NW1ZeDeHuvo}^o~XdsLu6ob$KM7pvrA+Ml0>S<<@ zzhrZM`f%K!ZbmN|8F4)aMV|PC@w=!~nb?f~2Duv-nqM>j@;#i+cU0nR7tRDo{NaCd zfUx2zP1o71JrxX?(3JJ=Q2Mu1S7 zMQ+8#yAawQ@5d7z6?6ydj|9RkNYXyH5zt$-CqoMbI+X`<)WNH9G5cY}3utG~z+7s; zr;oo$O~WIx{tA-!k%lN1B!I23pK|%05Ia8@Ul;g7rwQSrc+x2c)Dnc4c7uO7U6coQDiOd4Y^0+? zn_vKYHzPs&8>bZS$r>!U`Dsu7e?fc*0A0gJB4EPChqwLHbk1i6#DS|81(RhSp}mv( zeMg$uF2eNzzsw0gBht$rBg%E^r=97I+porgnqD@!lOfVGGfO|SQH?SNB!j(vk= z)uRq@<D>z|8(>SFwfIo zcKO2}0qsJj-;PwU(tqXdGr$b1;;EGu9H=)nbAsj__)s<9i>{;-5v!ulcc6uR)oCB4 zG`Zd#EUC+0Cc1bvOuRnTvg@B7e;Ve5p(Gg2VK!z6D~f_u20j@N4wSgUL-%`V&QuKM z{~Ag~c-6u76cuA4k1a*UUI`*>bvs~7+og{^fp)oUEyy>88b;u9wsCzCKi6Z!ic(xn zn^aa6lm#yqgUhX$>*X7rp+H}bI~)n!7PQCOS)$crz5<6cThO>P{*=1Z`C+Ctr=j_= zwL0cq58$h?f$^y!>uUiu|1BPQt5}as9q6WC^0Q0$H(GkA6wPKU%dCHutQ^SKZWa7| zW`QvOC5&L1T$zzd_BqJhd(wJ+Htp% zaO=&?x||ROKysB=|1%Tyj2~kH;Xvt(zDO6JOYxZmSDL@$5f}-hJZwp$%>v{qWlm#C z=l2>s3lLU74?$lKM+0>oS(nj(q%shlE>42I_#z&{UY0xISA!tZ7O@J zfGq82M!2^+NL)$wuVg6$&az)DoP!fk(Ot;IXM(!Y`O}6xhiQArtY0N^x%l1`$#h{V zw`8qaB7N81$fFq3CvilPVYPmLi9Sjmw~G5?mcgTxf7qO6Pnt1VaaZl318P58}YGm z;ZQy+43+%LMfYYlG!Do6uhAw0%Z5?n8`ZE#lsj|Jhr|q5MPIBRaAUIDh|U>;Aw$!F z6aD?qOE@8(7{Kx2w>iZwrhP#dV&P6@;l;j=fU-w#Fo0jS(z9J zdl_Hr)c#dS_h(QJi`LCaHiSc$vC+0v7;EU{YLSUik2yRmHmG#=TygpbQMGoG;99dv;vStNzEnr#+o1?y(?iBa2D!iSi z;QmcXWPM{7X?n&VOblH>j+cSiVeGV9!+!ZOPk}W7 zldMBFnMMKLq{G{7W_VY<9N^jIFw6AUAM1{7#Yegvp>bHtrUB{G`SJZw*!H1 zws{V~b6ptI(ugz9cqZD^38X_D%xD^~OCKadcEv!x#_PB>jpt>X zMY5o$SnBZ3khOwfCH0G#*QN@#?}RB4aB?%ZCosFXZq^TspXIXAm1(~td0T+~smqY` z28s;6);L|Esfa!tW`$Pyqy{X^R*wp$rT=I$>q43@RJUsV{WN+~^X>Tg9Wcf@WY{~v z*{%CV)2<=yTfP%H1^t6C3A@s7{Z@LLmh1X&B_HHkP)8V#mZ|A;7-50_yPrPR+HsW+ z^({y%ZX4JQ!kq$88XR8fR+0NDC_*-iD3&H%xxV5ED*6PNN683M4Ml zMQyfOEucL=OSf-kPSC^MCEUnE_s6|f)`j*-&PRpd|&{Ig0DWXh>Hh`vwObTiBlk*f=*|B>NV?nekAXrVnI2=7skteujv;9 zDC6RF-lC53&%*px5;(o^UHlt!(iBbpew;j?djorc$VmHIh7M+6z350qI?mf(i8X6i zL)OsCWQN2=P0%$b7h)Ad4^l?no=jc3$R7$*K`4tDWs>~}6Z)^<&66LL!rA&+4D^It zs01Jj7J1xX(pf@B>rsk~P5ztnIJdx%Za2j)0WF-%jIjO?(?LhY&-}~AcV=nh|`pI2Nk>C1JWRrWAqWu z2%Esq9C>a&ZND-X=hZ?Kdi5bqze4gaEc;TKX+O@!YRAI%8G<*cyGzm>wbp!1ZV!Pj zfpzO0qfxB^_Ywx>ihp)s2+TlE`t)07UmK%SjKMgTc&n zu~@d@5={PwF?$b#Fw;Z7gSrN8a>Oh38pnDWxI)WFvp?~gt|F#=>1}L6y1;b`>^9xS z3?5(FUd}zvi2QleG%Km}QN!S!#p&llRpI6-9bH zygMBFO{srYlI`k*gL2=xuEY(FA>~~aDtR6&8}S=$@qxBK(x4JXq9fB`JHN1T72JgD zk^`XrI>hO22+|5lK4G@&0C^(?KKn_B-Vn!jw$;lUjR_k^(|s2!oTV9j#>g`EirOzC z3leBX79=*$m>{1Q_SX)qN%~T_{3T=0`2gIDBHp0hYA34%G5+yFaci#lQJ%7PG_G)z zBg6;0qHEX2`VY~bF4#}O&M*|7xXi-OBRg5`toVk*{npj&s>rgygg_O@a5>Do97B#( z4tqbPfriZOQJw}}v*$!!i9gy0$6T5KgZx?@8p$qc91zx^bi4u)Xl+G8>ZZp&M`Lzd zCf##9&IKOSU=8K6qEN{<>5nR-pr!GCbeTyRVIILp2{`-J($##(gyb>)*y?v?d`B98ins7y>>?|?cPlb_x`>_~TYIYBd znPY9L&O@W%|L)2wyEahGQENBYu~^EGuud4aZwVuOuca(~>Dd#IKqN11Tb|e$8!9^o45y_qzJg`p@EuF| zcmXvCpX-v?!LP_iu#Nr|wQL+tY&^r`4?N)s)dkYh%~@r-YpLGCLKZabkXH7pUx1(N z|G}~1bsPpb3&i>q^cTHg3}0D67f%FAHFF}L+rCo~E{1`nt*K6rzPgQ97Pez{?XXsJ zUdF_=mOPVtldH$Jl%s!cq>e~n&QM@#p08|6g?@r{6JueDDYD zr%o+c%RRNAIY$+ykQnluz@v=M&jZklG7N&T0K*V$5WLk$R#v2-Q+`Sr5iQMZ%-~zn zeI=oM?q9OX0p2#F$2mq1Bf%Pg8r)t5&$W6d zDv|7)eQ|8^H=@EGh7rqs`b9ANEa2Y??}n3(`0J;#O}LblW6upb&0gM{#OmK4Gw&9U z2YD5%LZSDsfBGV6iqFwJjmmi?0GxkJbcDI~S<`pa5ySKfb~lv%y81`me# z_%F0hcthhg-^iicilfP`%dJ^|tirH8BMTW79QO098r=9E_5`pYHJpIlH4ysMh=g|B zK>S{v@}kADEx*_0i%aA?AKrC5@)nG(K=&r|;HvQdCV?CNX@1MfV|~fUe`kX6#Kgy~ z;)x~in}=|_oTz9=I19H;ZB$kF8}kd}BRe0_{#4Cpg4r<0+SL`;b-z@@m!ZDja`1>% z6jpyfYgIk(_9gNKMvn>Yv~-!tI#RX&S~dgV-~oqAtIRaYoOGhjFV^)7onl39HqeWV z`a3ilVdFv_8=cQC(YYX%0lo2 zWm;DjC0GMi>7nUNe5%2HVkYx=Ru)@CQI*^s>nyM`!dJ3MScJf;|H;RH()bp}SLqZv zoL@g2x_|$@4T|6Np@btAhB*ol)`z1UZW65Sr{CE~yQYtuk{tup%FoyPBy)_95xE=Lov64Mf6+!^&>r`bEvP$m7&8R|sA&4G)n+~Vatxyi}H`c7=eo!I4) zKijm6HK;ehGwlxyp)E@jZ7%oDG_k<9OYMUI`-lG8sPDG5###JS<_}$;MjJA#(C9s+ z6@-5e^5JaI|9;Q1m#o9TVdl5h_te+FXMt+0IDdfZm&oK*J@MpNxmXqM@WsMGvSFdC zEeW^}(4nrx+O@FNK#7ukdqkWAk}mnYBT$w6%F=iV!vt)M^htgPrS}8W`;-~k$j_15 zmM1ypuHS3&d!`)}@@3r|r&LQ4W?ej?U#HfDwpnd;ftKpq@4?22qdu(Cb`IF3qsd~Q za>n|T#~bNq+M_VWCYt4vFEg=ttbWToccuDgwcJK}Zkta#irfoVg7vi=p|xmz84p3w zhlm74$^@tHI=MV$hPfpyKVh_$@tGu_sT3KE&SAVSiV}4#?zjoenW=V=^Z00;2DtpV^%f^ikodL?B@I`*HXzoek zM4bRgWPuqFop1QqY{mCKJK_rrIkXkRgFMtPHL3qu3TL)BnF)L`3GTp_T_GHNLb`rb zise9Em~2u`zI?)o;s6A4O#euyFBwc(Uvcr(0_GbM8J2P4dmVh(A>S!=J*!?-JbSC7 z*)iI!b`#8}I&C+X_TO|vybqgi()6j!K7j&2=}m{F1CaiRh4{E}3jqHVcH{k8ARed# zSpoUi7I-)a>J0OzgyVbk$mFSlxgzQ?CRzQ+AJdL8Q;V%=|f?N;@F|gGH%O8F4#1K%uT&9ASJQDX-`G#Qc{7{OuJE3 zVSBu<-dN9qa|V8#O$e2SnO}U2xIInbDr5Ng+TM-Mp!>(PS341@8XIaIjl=fWDq{9mX)+@OpO;ye_~kvG zo2mk<1J%gI2vmmH)9}wjTI*2CcH&D-^uWdcjqE-6TeL>8kz3%G#|}22SWfA`<0sWm zuEw$bPow)3OW;oUIEc;0I|hsaEHUp1XR8-d67dfvJhiSCX5RyN_7y39n#r4WTB(b2 z<=DdS_kkuUZzkM9Vgl3M%)E;h*G2pL7u+ZGckMo<+-VtlsI(F_&R6~X0aL#$i*ELb zX&QX!jA$Bnmv|Shw?(PW&yqHpTTxtJ4P4SuCO37`_zIe-(J^vDEPb{mo-{Rw#rt*b z1|1$8FM;6$zcgFkVv1=DT7Fo1h^0_j=-BSdyDOh!vUHwr_U-ASh9P*=8_AM%(KeOaL zmit|}n-fg4Wg$jL*p|5iAZL*Anqhy#*4pm^nT-bQFDS|A(!n)#<0eEsD4yeqjlTea zWWJ?muO_g%{_5vvm-|ZY(mk-^t9BgNzEP2l6Mc@?mh*wZf-K(K5#Js*aVE6{oOnAyuqs%ZW#j^esHAwcOR&EX^jp8|7#uNA146N&ss1Ww3cdWr4E}OTA z>1+?|3;HJA5BDYk#G~7EOs!Trm-5Id?xZ}%NB*wndBls%H*bkug)`=@Lb_+17WpYR zkFH4RnAEslpFyiE-fFu?E#R4ZUYRmneE%y{&LG`4f=TZ&`FfJ^V-np?BE=FF1p>*M z|LbO1!bla~5fDnCdT*zs+V~%|2^ldSOR2<{H(!OU}%Y&w}6$ zv~C|Mcwv^O^J_Hw5iO0D3ca|HeoldvOnx{tMzoo^Mp#PA-%~XtmKsPe!&{%tt^ZQ_ z$mX9(^Nz(=K92ndXY{Y(u|@d>2}rUH1jBiBA$!4LX^)rdrb6%==l4xB}T@<35t-~nPu+!o5 z+zRRRiq?zXQy)rPMu1r|Rpzx9yO=8&>R^H?BqW`->^Wt~?_ z)vUNI_z8)g06mB~AiPLZc@>(1BQxZC1++}c8;TT+^H=A{Jsxl3pUJd*N_zvIQrC%^ zME1xZbNr3no8#p}#r}eyi@wrI98faYeFB2OohlRq19Z{*-E>CYOw2FdDCon14WKj2 zO}}T{lj(21ddjc0dLPrCm?BPayZ26^r+q19W69>tx>{o1c@Q7juW2E{u+Yz<*TS*LKm-(^@v*Uy;XJ6$Qi*O)IKWMv z1ACZB^rO3c%#H0Ye`5?Zs1#O-o7}>{_*MVJR|$}>W1}1Df)!RvUT8@!8_%J3G8}uk$F}XI@ z;e568h{VL3E;wHzk>pldIq6t;v{pOPenxi$w+7L}$Gkn5B|XoyunhESc>#;3H3G&P ztv*nV;p^*t93rqgfW$$!f#0|}4-;R6$b+ua3~lI-N2ij<29IzX`*==vfG!-+9oO| zJK98Oe=fiy;unJ9%NYpre2+u>8V-i)Ll^Nja3O=C3C7El=+v1|a8oAN-psmkbWlrfOa3>ms|x$ z*krK~)}y>Q61jU7U&M@BZqJh*hk4ggqH6;BKCrH@xgJ*Xk4huvnQRy9e=)5a7*)Z6ff_&GF%3)EU2p#(PP zSSkt+RSoX)&5R0Md_zFo#sJFl;u(JPXR9<=e@iN)Y@z<{6NPyGB($Y=gr&6aEWi6N|Wt*N_N z`AFg`mPxw_><(Ts6HTo@xP<7b0_3fWw4IMbB5uXKZiozxPlz^GtKR7O0Z<0Vj@^1kHX>zs!z=${N zvWxVfo@N#?3yunv>HI}q&Ea|_JebgD`JvLM4_P~!mdN@Jp40S`B{vG*CLLps4?Kvc zOWM1fP9W{@YM!3Or+T75L_Tr*LJ0=XI0V);Ilozo@U+2%^g3O9WNogFqWn@;39V0m zrg{n;4);YKkiPIDrl0F3lcPGr$3rXDt#<}L;a8Vwe74S?Wx$YxJDISb-Yh@^<*>*3 z8yKqf$wD2)U8FId2)R?4@s0#UIX0M)HeBoBvzf|bX6Z`xGc!@t*1N@1!jKps(>|CSaN$1*O#=l5{Den5B5Y5K99hZevfU z?Lu?}#F(9GeqtTK^4fuYEE3##itRJ=4o?l%f5@PJpfC)>p7-z%gm@NPGp6L<5dA>> zn@>~@<&O`4MC+V~3#XB}oZ%?&?f3^LxJRQRceClZ%G%j<+;G$41mTuaPETo9UEJWR?jlLo84&*5u6KaYbcf%iTB@1M`5G9 zC^pfIW<5MPhIOw!J+e5~Kh`ifb_WA3LA_d?S3iVqsmxMSm}ueRXY$5#BfRM*%uyuF zK#!Kd^Ayl#c_B>67w}6lZW@iF%8LzSh+c+aMPD%F&3vzAa+25D=h)c>Lo*#k6$%zQ zD%0Yp=d0<^5Fs8KL;8R#i z6!sMHWVv2>z5|tz2wVMU*+TlufhpcvmulwN`hQyacrh;qi@P3uuY-tzJm-eDwdmsN zV(eT;Nd8B?>u_rZ2Qfj%ldBIJ>`Sd`GRmv{n8t8rbVgaQ79`Bt%G0X&CkmPM_LnRL zd3UR|MP-_qyt0>296oPZBL7X5LB^zMOm1AcHf1sJmnM$@IDZ`>` zHTy&6b-l}sQ`Xu~G3+w&!B0&?BZ0E_`dAow^jE+;f`#S} zzv+U5GTK_QP4Pt?1w7(~kS~H}^5j%Bzw#=@;i+Z0J0gy>cZenC52FSf4xO#g*0f0qi(ziHanqd1cWf$!TLQjl`9nx6-kq33wF`NhmcmByz z6U1K)T-WT(Q2-i+`%I`Vscjr{#o!Yz9@J1LGz4b{qj`i)sdjd-;A94MI}V&M%b!~R zmJZb`{Cd5miy1x1MX@IUn6RiU*v!^+Id}37(}t&|)xX2-JLZQKr=s!r__S3q&Z1;I8~f;w3Y{H1)0bGqF3l5@%WyyEB4Iu48OsFc#h+%NvHeM<-5F)*0&M z1+i`6P)}b?w5B&yl5!E8o9-Ezq~BqJ10q?S)@&5**m)15TV1=H{bd1%zZM%6jaH*`D#H1ptV0Y@6VYhZxDqSb~@=gNjD zvC+rFf_WsEIo-Mj7i$r;wQCs|Y?%2kMVNKG|0#`+F;p$-=2r{RxL*%o#~j2~5Uf9F zctfALx8Qq+A`OGABZf+6RS3k*WNJSLA+d6-d*DupXfb$@uHGWMxg8E4$>N7ud^cb` zG-x*kcvcCb*=djF8K0u3>1tlPw%2gKe0>UN@~5h87b$q(*WY5(VJ3dG^s58ma6zv?yHxRxqZ#=o{4B9zo3k+%5i*% z&dC8SO004$2YGj`(5#F8ma`_^MyO^v4W6y)F z?o8=a4!yWEpx^tHg-YrGiuVeiwTELV!3PLlI%XzT20-o)1YJp$`24p{R}+e!Um#(s z{I;aa9Vyy7jL=?3Jfw!09+ zlCaKl^DXoOLP6mqF`mE(Eiv|ZfE%0x0Dlamz;f|9IT+kR!Ec6>sHOeAoQI0_CdLmU zrA^w8Fq(_)6(bnElk~;VsYUNd@Xavc3t1~!;~2ooKCZpRtq?1S_wIaV)H#7Rlr7a_gZgGbqug``<{+2Zzoh?f`LLU>{lg*2i>^6}nsD|WelS$}_vF5K$ z@#dNQPL;O4F6y|y+|Rx1f2wGAGUZ0+XGmzu%EIE~sy&hQh*yO8sqebLnTf`UNkP@e zQLptK6PLhglxYXZ{zN&t6}d;Fb6JbC|K23G^ur|^M@*IX&iVK%ats{6x`)Pz2c`9( z=C4`E`9s|GAo!St05+j-y4f872Tc54H9|(Ud42X*Y;rt;=`2nx?DSnT8NW@Xnz#N& zM=93GgC`!Dk}MNntPJpM2!g?BP7<_aV>G8EuTU345m49pMcN+25>BWN~}@|{wK zDadlp&*AV9bL^j<)=`Io_@u*eU{c8#o75Y2VsAcc3gtM{2_LK_Fe4&|?D`tx;bIv0 zTmncaxN&@_d-xuAepzs>JFot)ZiD{5C+T7)#yybM-5w4VMJoD>u(5++D#r*1X)*0W z6Ic#o4p{l{+WnQ`p48T~Gj{}!|g zQycDEzlM0qbSUGsm6}|w|IG1Q{kZi^+VN~OSWPGi8hjJc}2r+XC4EnkUr@!7%h;O(5h zN?!AihT|;{Ytt1D`(1(3TEpde%2q}e-7Dm$O#2P-AFFw=Lc(3qA_>4U&zwp^ds??| z(KWe()H$%YOXiArVIjvS0Kr!={K_?N#S4Oi^7WgnG^@klk7wh+w=eQYBge1*+mi$# zSCXL9QK^xt5AvNJZ{#OStX-{2T{6pUys5*JYBtjv$aA5ge{6tKs#4tgodNgnm~{s6 zYm>C!JJpuBt<7~aQwza~AYZ}DI^D<27fWy%(f%SipP{MJlWqP06K%}8iiz(~;t=m3 z@qCtg*G_E%ZR_tg0-#halHg`DKgvxov>5-?L3NgqMK4ffmz3=9B~;PRYBv;-Dx zYx7ibveOe~<@-X{f{waXwwX{%Cmt;4w^fTYmc4U%o_3nC3k5Gq-0*PKEPVFz8eSQBpvzOpbY{J7TOoVE|$_oNd$5-#ckz7Z_=hN*RxsU=I2H6(NXpv$TNr_Wf4dc&a3(*L+Sj04o?1CL(fri> zl)f9SMkj{(DbRW`vv1?7*{2SYxE(G&X2d_G1HfiDbQ}|-d`hTng*)GL@&QiLTp&S# zd;D*2EzaTV2=s2PQ^0E%IZh8U4;tZFx$gfm(auDbA`=`2%t25+@6G0Wq&_V5GVY29 z@tGt&Uw$U#RxaP*_6v&KTe$wbQqPlu(bE}n{p45+f=nGJBwDrCbKF-l^9%)pBl;E^ z8=iEHaDB&XP}eV2`Y3Z{69@Hc4NF=i?P@9YKH1`Yls(d3EJ-xF^#|NE57c^BVX%IK zpv7ZF)1nW!?IKJ0gYbHK)v)hUdJ}}u!MG`A6P~)7L$u5ChW9eV5S{}bf7AG-sH{~R zqsIk+6Zgf??p{KB!n#iwA!)a8j|UbHhdD1Xc#_ojnF0*4v*0I2Dwm7bvY^Z)u1E1VI};!{%dqZK%m%J%S-B>qBD4cda5{NG0w zIDB3w;9#(;;cqQ)KC- z;B#j40S_(Dv6rj@iDa9VDV%utN} z!T6Fnmi9sCw2TDXfiC44q7R<#=~|bUQl|5IscvBq$qs}AQ!p3gRcXbDm!|BOd>-9}0vANgb0X)-(PY zGuAN+EW4M|_B+W7BYmj324I!czLc=CLFpEB z(i!^l!9P!ok0PncaaaPvFpnY75aYIB@p^i^*num$hl9mX?H|LM;6()kaMvSc+U3jq z;TfV5CHBYjAs}=md%2I?F|MK0?}M{eKARRUna?~3OPDQf*hgj99qxD@6W2S2acPX+tJZc4F-6*ia6>p&XzQ74 zgFvzhCIdBm28nTjvCny2UrR0*hY*5Uxt#7uDhP+}=jOfG7+ED=(XvbQ%lCQ99^?F9 z25)Sv#Y;S@_`Be(6@RC+41}JQej)BKr>?i{;5v;0FS&3Jf>tQ&Nd$x%)$9;*vVG7pR5{1g+mly#CHze0cTW>LSW$)W4ISV6c1 z(LyJB_CUh@tHD1cEAJifg$bRYVp2l;BT(Xv$c+0F~<9t%bv?gz_u;u_L=2$JbF>s0R`1lA3X!9lg)2Or|w znK$;;uU3d7zYo%@`Qvlr z^M0Djn;c7qci43JAueS_Tf0XNw!x5Ws0?g=+yn#BbJdQGDZz)!+e*Ns?mwIN|7ufE z$S?`puvZptC_AuV*Q9w8F6eScgDAf=yzzgR@~s~JC#G&!`e|TY&SZrngZXB0JTasM z2v&EowjdZ4!Apk%iDtz&!Lb$=n)>pin)B*92WRz-)=d(WCKD5f7hab-!47@R@d5Qs z>%|+54Z!hUAAA6A|08SImlhs95^Ns9UIM<*1hB5SGya|k_I`dI-Q(nfjZsw~z*>@q`n)6MII$GYc9F@MWL$id{FKhQoO>-0E~89u*Dvu%jBw zsC-PwdF(VSj?R(s-i;552B#;w!Pns^0XFrU!gPs;Txj>T{$t$R8SgQRr z;2xjaK+lJ~@x9Ex3JboO$;()5dBf7yru_YEf>N zZh-@@1pyZ0W#k(_Kk`21l}v#yQ`5w! zTf9xrb6=lspAZf?t#atH#rgvIo5c8L2W3xOj)3{WmaPaPdT(hZxRKwm7Eo8a|6zzs z%fI0mqX{{?8%)Eyp~31M4sIA3-f%>B0<1eX#$Zy>q0B2)vqMnxo8>*R%dJ4`2Arf@ zSwV2a=Vm2%1k=q*WdAg4`j2c6?mh?$>DcIhEqac44yyF!FTouQmuST7h`|)}1Ez*7 z@4C|N(Md)(23)DT@a{P8#=O-^drt&fO*a2TOg|o9tY2X+2x8YejHp|oKf!5>%7|T0 z!nCN=4G7P_F~(6P)WDJ9yDpJSHCSiY;M`^W#pD`nmLIu!CATkNVhffWl7joUGY@#U zNxD6bPvyF61$FECXU=C-SW*{r@dJKxr=mQcNFPE23tfJ?8{ToDZfe8g8IIp{?H1>h z8yyRGpjKow$_U)UDHygL&6Jnw9y_hU(PJ+uRpZQ8HjVR~>rj3^uct)fjH9hM)$7S; z;78iblO*37kO9f}8sY|RVISh!Cg4^C+uu2%MoO{vWbx0Zf|@ADO|^WnT<5t6h0u7+ zp@F9+iwI^NxtMx_RLc0fa8zI#N{3$VoT!D(Gmt-gNQ)vlE{D-ZH{vl(a}_&K=j2W| zI#}sbT-(zizb*dN)J`h#Q2P1U#qP3LcGZD3Z%wjG`-=T@tOtQ#d8-js{a zuEakcUyaqRW?O(mP9qr7Lm2{j#PHbB+$r(?0ZVQQAhL{Bvngf4zcifs>;7IAT&#mb z1bwX}*Uet4_40ZZ2^pu=w(u z=*Jt~txS;g4NtU{?6A7Syl@z)DyIkdbS{5}ttp9F0=!Rg(rcQR9Rl4FrBD+t(;wDL zw1ruWo@=fJ{WF8L^dkg?JamPim4GPO)b7EYBG;+Qed(GeU< z!W*dsie}MY^F!MsI344{-fnbL{oNkc+v9xE<6%9$ z&KJENxG``@t%r1fMldgBU6<JxWpY; z9v?7oMU2@Ml#WYA0HonhaCx8y@r<@P9i0Es@!0vS0UK;nD+jUeU+4oIcTKQZD^NQg zlO(UYr50QLE5hE&FlXTqN;_EF?f^tE+=d~sSgz+KQe|acGz~ehIG+ z&D)^U#nRn6rldBT_>@3E*S&gNB`1{8ot1Ie7c} z8(hDf(Pzv)C^CJ~>ja+(Vk3aF<$Q+1@j4eF5I^9fl%+7Gm&Xau0F&M1t-OHikE84& z5RZL<%7Y1a<`=07ykFZZCgnYQU(xI^e!tf`UcgWb z6+q}hxG)Nz8E0xxXhVXJ3ww$1L`TPmF=0(;6h-EMF77VpH&0Te0~|>`)ks+U@FwxL zFjjB^x@QKYIYV^|%vt$9!&t@me=YkOU9)dz9xpt=#JS9Xy6s%##;|Ow9E)ePxz+w7KAw;Q!a~=8(JD@w*X= zY`gG6?~3d0_fGVA8=%~q&Y*%)1ee&Z*iL07Iz&wr}iWoO=y}VB9Cr#q|Q= z&%lWg8jG(Vrn6IA3*gw6_81tbiSa=^80xm4)9mlk#JiCos?bSuQs-npRJ#R)0LO0Y zg1{8Wk|4*j%spw|U_~)z*ps%-CMK`$Av|OaKLVB(i@1el|*_sT=+LF+7&YD3z^emFp4`)!CSm894qw|nH za=&-(K+bAB(}rM}+t1#u^JW1x1T@L80K+Rd%A?qLT3pSe6Txk# zFW~GUkRw=@m1*2|FVg133toGl-!#GVzsj%p#R>^#D>;At`WtQc zvF;+vI#F5`VCf}`pOfvSr>I@)9fwG~mn*k$x3_Bj;K+%U^Z;86CdSUPE_P}O7O$h+ zSmwCq+}N*u#TuP&zhsUPt-r26Ef7ADWtW^@N|%qor=uFmQx{7uiLps<0)M)v&*JO2 zj3VbTFe&s$W)7jSKup+^DI(kZ7GYBeoEGfD zCou6`L3dO4Fe>!1Twk!gwIA&pme{A#tjAEuB}BHYA?(^0lip7JwJ0y7hLKJ1i({8M z*z;zmpuY@uQE4)Hd7o3~k8G+IJ*Ff156=Lk>gL~bp1~#bD$QWrk@$t1i+G1sAs9oG z8$pETfOwGerMnK|?v_ugfuUd2KvYg+<69>=rs4UF&TJ5m{jnqAbZ;&UdFoRbR?+Ch zeC{I18T%6htQt#xEL~f{qWJzZ5_iE>gS{UPLCrkYI7y)ml`+qktP4K|voIKPw1b18 zeeN#}(J5gUw|91O2>stJwas(7zo9*kU8l%^vW(pW!Yu2#* z`6Is(4u$8Ld5ulzf3jGU^I-E>add{Cb-}!1Q|wGuTM?-VvC-8H5T6eZ^`&BX3A@0R zlbn%qmba#yaV?e2j{sJUuobIDyEkwC$)lySJ(JuD$HBs6k54KAv9~+_I}ZXf*0uu5 z{K`Dm4n_WqilNJ+6(wbc9WXDa$`!54@2pgpkspU%ZzDd~k)c+m0D<)(5)OY>*#*p$ zueJgiG5Nz)HlRk|&Mk~83+Naq;M1f1iAXK?N0NEFgMnH>w%*yuv#Q1X>i*%f9AQ@q zpKtRz(mF&S;$tBE25V+!+G|BIr8cUENUabwwMI-4HnJb1%SRGjF0h1D4cDr`-{kNo z3Nc@F1()ZG_DQYAc|zo3f+Fk@ZYP1WbOna7M{1&YN8XF`WNz=`riEkvAlGNI+?`w; zay(WScccCvTR=@F?A``-@)0u6Mk6ax#K(IR zwHO14Gf6%G{Q`$qjBlsG%wz#?hS3BZwPWFamsgyks1)QWuIGuu!>ZHdcjoQl!65LtNI-TRH7cZ)e-ZbxCsDHv5*W=7N4FV!O>0?}~&~3Yy@f7pK z8R*JHRM0t0-dndW>?94$*yJP@VMQL{;(c0=H-Ay=Dmz7HOTED{?cHs-H`XT=*Ha7G*9k>G6JnPpFOKJ)1xxBaX6q`|U$!Viv(*A;=!_M% z>=b#T;4ht@6V7P$1&im|rP&x^xvQ{Q?KSxgj1Q_F9Gc-!=&u}&#d~l#n{;3p3p*EI zCUgCk)4S<(yRXUV&Cc}rn=j(x+jjfmc{UxYlE<=(Ciwz@Z9q8nng={>&f5gh-q*6u ziL$|9EF76u;v|EILM49ojaihK+f}r+D6+1!C=xE(T3Y7}??OemD80wdpHUPH%^eKY zR=mQz<{IwpvTQwj*_UqIY0N?DCpliA8f|mF0Vp%HIeadoz3giS|I6W$y3UbSfC9Mj z=7=B8+u}G=dkU~-{VhpN&hxs9^Rd~q(5<0%v>0Z<^-JmjwbBsDMu9dCnVfJCBOM(F zNIA>M2Gv2lvoOEAXP2%ofxEOY)gM)$ZWB;xXcEC(WBSEN$KnA@C{4i=zf{of zWDr)8K340S9JvTIXNa9m)nd9c&C;3pugzmK_)DVERE!-~+FK$1ppC$3=K_7h&>k0d zl{;M^`cIewY?FOL;fqDdwA5XV9r+i%+1Zi%+|)!k08%6EZhl6R`i-97GjMG#h<5gB zx5$sw%y46I>z%9{o=Olw6!%_?U;Z`S>>M|hB44Ct8uNAZy3G%gd^Nvw7gtw=S>wuh zBEF5&`EKnhWDe|>vW(7K(b_aJIvg9t3?Bw8p%{acLt|t)BG;1XM@FqWA1AfJwihx6 zUa4FJQ#Y_s3}&BVb6@mr=9z{S-)J=2@SIZig*FndnM%6U12s6ae*~Yj(QE&J0W{9! z?-`nQy9tIu2Ru#|hnB-6JU4tyyVqks<*_`|od9C(B21ti`FP*xhl{)gpV6crP6`u; zr05lDZh0F7U9m7K5Wfa$uSA+;@bwev77kGj3`)Z5uvnuQl>w@srR)(fT!V8!Y)eQ2 z?`RI_?`*{+#Je)pJ&g6i^axwu?u@M}m<5)*+t>fmCI@bRRf2oLpU*;p3;kD(!rFE2~nuTz*AD_p+#9O4l!c5UaIU1E6c<9&{NAav|7 z1QOOom1@3AkME}q=mx5^F*M&<>e}u0IW9+s=M5gmE(QAsSn0O0a4+k07Iw*sOmQUw z9a&pwM{s9o4;R}N$X#jSPsm!zJny-vO!HOtcHt4;6Z}|R{MLXW;GbbPIBB^P2DAQn zy?mc>yl(K-#IN*euccxw+H=z8ZTVllPsrtKax)D~?DO>oXUn)rOS&&vv`ci%>K+}j zPqt~~cK14z%mpvz%KP*NXA5}-am13ZhCa1>bkj@){RQyc_TW@lVASBxipUJ7JE*KE zDq&i^P8Cc$m@54PyqNjR$|ra9_qaI8;Abv#$nQuHn4a$9Shb;tEbSu0bw^s#jC4#- z0J^AFUHj2*pRAjKiY-Y<(6YDcENeyqvqy9Sw>W7)mvLs_7HG3C4=^ifk$om(?M}(G z_`VGBTt>)T>egyPt-X86el7zJbbmG}vub6k-&~p`?Nt5kmDWgxd|yZTq@SKa3o-C# zp=j+)4&6BY$aAYaLKHC1?97XQNHKm_ITU?zmJ z)U4*-Gh$(`aHEK0G_cFre>%Qjh zZSll+WKGRUU%eTGB@;t-^J%lZKaojX3~+UA7O#WIq!r`fI?~fJLh)W-MtLYSjgj_+ zRf(EmWuE?{P#ch=g1zDzA<_DGCaaMQ?KTH9QCazZ^L#z&5YerEI;>mkljSb14&cpj zQ5)1OAr5&?gYE~5jLN__G5I~EX*6lWEO~fQIt<}oUu^ouOZlAT<+DqQP{kr!U4NQoNn4TV6q!cIEf^c zF0h~OVqYcUS_+N!Y;%z1v3MUc2BHu>*00Fa^=K@yH(fiAYYaiEq+$>CvO zmNJ?KhIB4p%PJeYM}Nn~|CqRzGyv0YGHh1%T$bT&OlyA3*}0d;x5WnWBu`mUWSYxZ z8*FWQH$7NdSRXvo^gJTkoflpGqF>z;vhNLFva`b5g1ydNPXs?`p+-nV0hTs&u#OgS%YH>!1%6C{R!!u!B1XHGRkXEPW z$A@2-hkqx8XU>0D;kYmPgP|kf`Y|SwDGEhu;oL}X_fqC83Phn`qG#6U)wDK;?wg~~ z$DIwJV1C}IPLLS1s|9sz#cU_^<0lfICHwlm_4{{x%eMRqX4Uoh110C>(FK;T03M%L za(E6+8bsVo+Nm4|5e;VR3(kkuMxBg`W4Sk+AEihA?lv4zY^>w+7TASp`%FGHa+O zFIcB#xKqLf{}63UsxZoQPqa-s3WVF&TiPp{sxn%)DRz3DmN|wDaCB4(xihB#P z`L1bZPIjSX{tKQG9v7MyJAWGV_qKT3v{NmQ9X4b$wuj2YF45+XhC*eB4c!A3&5JBM z^qkM0VhZ=SVS7QKT9x=wt@e+7!{`Qo3!eO7+q9%k55LzZYSV}>_Ne#$b`&M% zA1vMc!HaoYVPp)Vgrg?#<P~)HuUkiFzIao@A+~rU@oL8Gpd_bde6ucI9 zE%8LL~~Hu^j?qom3qT$RKorjcs+_Y09d-Hp zKT}8+@*8s*5T8o?@C$!If8qwEtq&R(I$BvO<6y{IfpwGy zVvL7hfb4an{G;TBg$BQc^De0!pN)MaVC6Ai>;4$eH$3Nis4iVyJ(6D*+zFe>7dK-> zjp&x{&Azf5inSa=M94l(&!QBr-OB8nrjdPvp3XV(dAhzYU=Qi3o(`q{;J26PY4SrT zy4LBN+96jQPkc}=z1g`tx9cY??*?`|JaCLN5wi0Nx%xpv>Ev77YZ_^VaucjNnZNyb z(?M%y0TH=_p@#C@!K|-RG{oCK6w$8t*%v8Io~hXgWfz+(sY>bh8}?J)a?hWl{8`=j z&~Q!JsLv-?9tEO7vJ entity ID --> entity data storage = {}, @@ -119,6 +150,9 @@ function World.new() -- Storage for `queryChanged` _changedStorage = {}, }, World) + + self.rootArchetype = createArchetype(self, {}) + return self end export type World = typeof(World.new()) @@ -157,6 +191,34 @@ function World:__iter() return World._next, self end +local function ensureArchetype(world: World, componentIds: { number }) + local archetypeId = archetypeOf(componentIds) + local archetype = world.archetypes[archetypeId] + if archetype then + return archetype + end + + return createArchetype(world, componentIds) +end + +local function ensureRecord(world: World, entityId: number): EntityRecord + local entityRecord = world.entities[entityId] + if entityRecord == nil then + local rootArchetype = world.rootArchetype + entityRecord = { + archetype = rootArchetype, + + -- O(N) for length + indexInArchetype = #rootArchetype.ownedEntities + 1, + } + + table.insert(rootArchetype.ownedEntities, entityId) + world.entities[entityId] = entityRecord + end + + return entityRecord :: EntityRecord +end + local function executeDespawn(world: World, despawnCommand: DespawnCommand) local id = despawnCommand.entityId local entity = world:_getEntity(id) @@ -175,25 +237,42 @@ end local function executeInsert(world: World, insertCommand: InsertCommand) debug.profilebegin("World:insert") - local id = insertCommand.entityId - local entity = world:_getEntity(id) - local wasNew = false + local entityId = insertCommand.entityId + local idToInstance = {} for _, componentInstance in insertCommand.componentInstances do - local metatable = getmetatable(componentInstance) - local oldComponent = entity[metatable] - - if not oldComponent then - wasNew = true - table.insert(world._entityMetatablesCache[id], metatable) - end - - world:_trackChanged(metatable, id, oldComponent, componentInstance) - entity[metatable] = componentInstance - end - - if wasNew then - world:_transitionArchetype(id, entity) - end + idToInstance[#getmetatable(componentInstance)] = componentInstance + end + + world:_transitionArchetype(entityId, idToInstance) + -- local componentInstances = insertCommand.componentInstances + -- for _, componentInstance in componentInstances do + -- local component = getmetatable(componentInstance) + -- local componentId = #component + + -- -- First, get the entities current archetype + -- local record = ensureRecord(world, entityId) + -- local oldArchetype = record.archetype + -- local newComponentIds = table.clone(oldArchetype.componentToStorageIndex) + -- table.insert(newComponentIds, componentId) + + -- --local newArchetype = ensureArchetype(world, newComponentIds) + -- -- local componentStorage = newArchetype.storage[newArchetype.componentToStorageIndex[componentId]] + -- -- if oldArchetype == newArchetype then + -- -- -- Set the data + -- -- --oldArchetype.storage[oldArchetype.componentToStorageIndex[componentId]] = componentInstance + -- -- else + -- -- -- Remove from old archetype + -- -- for _, storageIndex in oldArchetype.componentToStorageIndex do + -- -- -- TODO: + -- -- -- swap remove + -- -- table.remove(archetype.storage[storageIndex], entityRecord.indexInArchetype) + -- -- end + + -- -- print("New archetype.") + -- -- end + + -- print("oldArchetype", oldArchetype, "newArchetype", newArchetype) + -- end debug.profileend() end @@ -332,7 +411,7 @@ end @param ... ComponentInstance -- The component values to spawn the entity with. @return number -- The same entity ID that was passed in ]=] -function World:spawnAt(id, ...) +function World:spawnAt(id: number, ...) if id >= self._nextId then self._nextId = id + 1 end @@ -351,8 +430,8 @@ function World:spawnAt(id, ...) self.markedForDeletion[id] = nil self._entityMetatablesCache[id] = {} - self:_transitionArchetype(id, {}) + ensureRecord(self, id) bufferCommand(self, { type = "insert", entityId = id, componentInstances = componentInstances }) return id end @@ -379,92 +458,131 @@ function World:_updateQueryCache(entityArchetype) end end -local function createArchetype(world: World, components: { ComponentId }): Archetype - local archetypeId = archetypeOf(components) - local componentToStorageIndex = {} - local length = #components - local storage = table.create(length) - local archetype: Archetype = { - id = archetypeId, - componentToStorageIndex = componentToStorageIndex, - storage = storage, - - -- Keep track of all entity ids for fast iteration in queries - ownedEntities = {}, - } - - for index, componentId in components do - local associatedArchetypes = world.componentIndex[componentId] or ({} :: any) - associatedArchetypes[archetypeId] = index - world.componentIndex[componentId] = associatedArchetypes - - componentToStorageIndex[componentId] = index - storage[index] = {} - end - - world.archetypes[archetypeId] = archetype - return archetype -end - function World._transitionArchetype( self: typeof(World.new()), - id: EntityId, - myComponents: { [Component]: ComponentInstance }? + entityId: EntityId, + toAdd: { [ComponentId]: ComponentInstance }?, + toRemove: { Component }? ) debug.profilebegin("transitionArchetype") - if myComponents == nil then - -- Remove all components - local entityRecord = self.entityIndex[id] - if entityRecord == nil then - warn("Tried to transitionArchetype a dead entity") - return - end - - local archetype = entityRecord.archetype - if archetype == nil then - warn("Already have no archetype") - return - end - - for _, storageIndex in archetype.componentToStorageIndex do - -- TODO: - -- swap remove - table.remove(archetype.storage[storageIndex], entityRecord.indexInArchetype) - end - - entityRecord.archetype = nil - entityRecord.indexInArchetype = nil - - -- TODO: - -- This is slow (and unsafe) - table.remove(archetype.ownedEntities, table.find(archetype.ownedEntities, id)) - else - local componentIds: { ComponentId } = {} - for component in myComponents do - table.insert(componentIds, #component) + if toAdd then + local entityRecord = ensureRecord(self, entityId) + local oldArchetype = entityRecord.archetype + local oldComponentIds = oldArchetype.componentToStorageIndex + local newComponentIds = table.clone(oldComponentIds) + local hasNew = false + + for componentId, componentInstance in toAdd do + -- table.insert(newComponentIds, componentId) + if not table.find(newComponentIds, componentId) then + table.insert(newComponentIds, componentId) + hasNew = true + end end - local entityRecord = self.entityIndex[id] - if entityRecord == nil then - entityRecord = {} - self.entityIndex[id] = entityRecord - end - assert(entityRecord, "Make typechecker happy") + if hasNew then + -- Move from old to new + -- todo: remove from old ownedEntities + local oldIndexInArchetype = entityRecord.indexInArchetype + local newArchetype = ensureArchetype(self, newComponentIds) + local indexInArchetype = #newArchetype.ownedEntities + 1 + newArchetype.ownedEntities[indexInArchetype] = entityId + + for _, componentId in newComponentIds do + local oldStorageIndex = oldArchetype.componentToStorageIndex[componentId] + if oldStorageIndex then + --warn("TODO: exist") + else + -- It doesn't exist in the old archetype, so just add. + newArchetype.storage[newArchetype.componentToStorageIndex[componentId]][indexInArchetype] = + toAdd[componentId] + end + end - -- Find the archetype that matches these components - local newArchetypeId = archetypeOf(componentIds) - local newArchetype = self.archetypes[newArchetypeId] or createArchetype(self, componentIds) + -- todo: swap to avoid cascading moves + table.remove(oldArchetype.ownedEntities, oldIndexInArchetype) - -- Add entity to archetype - local indexInArchetype = #newArchetype.storage[newArchetype.componentToStorageIndex[componentIds[1]]] + 1 - for component, componentInstance in myComponents do - newArchetype.storage[newArchetype.componentToStorageIndex[#component]][indexInArchetype] = componentInstance + entityRecord.indexInArchetype = indexInArchetype + entityRecord.archetype = newArchetype + --print("oldArchetype", oldArchetype, "newArchetype", newArchetype) + else + --warn("TODO: none new") end - - entityRecord.indexInArchetype = indexInArchetype - entityRecord.archetype = newArchetype - table.insert(newArchetype.ownedEntities, id) - end + elseif toRemove then + end + + -- local entityRecord = ensureRecord(self, entityId) + -- local oldArchetype = entityRecord.archetype + -- local oldComponentIds = oldArchetype.componentToStorageIndex + -- local newComponentIds = table.clone(oldComponentIds) + -- local storage + + -- if toAdd then + -- for _, componentInstance in toAdd do + -- table.insert(newComponentIds, #getmetatable(componentInstance)) + -- end + -- end + + -- if toRemove then + + -- end + + -- local newArchetype = entityRecord. + -- if myComponents == nil then + -- -- Remove all components + -- local entityRecord = self.entities[id] + -- if entityRecord == nil then + -- warn("Tried to transitionArchetype a dead entity") + -- return + -- end + + -- local archetype = entityRecord.archetype + -- if archetype == nil then + -- warn("Already have no archetype") + -- return + -- end + + -- for _, storageIndex in archetype.componentToStorageIndex do + -- -- TODO: + -- -- swap remove + -- table.remove(archetype.storage[storageIndex], entityRecord.indexInArchetype) + -- end + + -- -- entityRecord.archetype = nil + -- -- entityRecord.indexInArchetype = nil + -- self.entities[id] = nil + + -- -- TODO: + -- -- This is slow (and unsafe) + -- table.remove(archetype.ownedEntities, table.find(archetype.ownedEntities, id)) + -- else + -- local componentIds: { ComponentId } = {} + -- for component in myComponents do + -- table.insert(componentIds, #component) + -- end + + -- local entityRecord = self.entities[id] + -- if entityRecord == nil then + -- entityRecord = {} + -- self.entities[id] = entityRecord + -- end + -- assert(entityRecord, "Make typechecker happy") + + -- -- Find the archetype that matches these components + -- local newArchetypeId = archetypeOf(componentIds) + -- local newArchetype = self.archetypes[newArchetypeId] or createArchetype(self, componentIds) + + -- print("archetype", newArchetype) + -- -- Add entity to archetype + -- local indexInArchetype = #newArchetype.ownedEntities + 1 + -- for component, componentInstance in myComponents do + -- newArchetype.storage[newArchetype.componentToStorageIndex[#component]][indexInArchetype] = componentInstance + -- end + + -- entityRecord.indexInArchetype = indexInArchetype + -- entityRecord.archetype = newArchetype + -- table.insert(newArchetype.ownedEntities, id) + -- end debug.profileend() end @@ -521,7 +639,7 @@ end @return bool -- `true` if the entity exists ]=] function World.contains(self: typeof(World.new()), id) - return self.entityIndex[id] ~= nil + return self.entities[id] ~= nil end --[=[ @@ -532,7 +650,7 @@ end @return ... -- Returns the component values in the same order they were passed in ]=] function World.get(self: typeof(World.new()), id, ...: Component) - local entityRecord = self.entityIndex[id] + local entityRecord = self.entities[id] if entityRecord == nil then error(ERROR_NO_ENTITY, 2) end @@ -942,11 +1060,8 @@ end function World.query(self: World, ...) -- TODO: -- cache queries - - --assertValidComponent((...), 1) - --print("Rebuilding query") - --print(self) local components = { ... } + local A, B = ... local a, b = ... local queryLength = select("#", ...) @@ -961,7 +1076,7 @@ function World.query(self: World, ...) local possibleArchetypes local compatibleArchetypes = {} for _, componentId in components do - local associatedArchetypes = self.componentIndex[componentId] + local associatedArchetypes = self.componentToArchetypes[componentId] if associatedArchetypes == nil then error("No-op query unimplemented") end @@ -1042,66 +1157,6 @@ function World.query(self: World, ...) }) end --- function World:query(...) --- debug.profilebegin("World:query") --- assertValidComponent((...), 1) - --- local metatables = { ... } --- local queryLength = select("#", ...) - --- local archetype = archetypeOf(...) - --- if self._queryCache[archetype] == nil then --- self:_newQueryArchetype(archetype) --- end - --- local compatibleArchetypes = self._queryCache[archetype] - --- debug.profileend() - --- if next(compatibleArchetypes) == nil then --- -- If there are no compatible storages avoid creating our complicated iterator --- return noopQuery --- end - --- local queryOutput = table.create(queryLength) - --- local function expand(entityId, entityData) --- if not entityId then --- return --- end - --- if queryLength == 1 then --- return entityId, entityData[metatables[1]] --- elseif queryLength == 2 then --- return entityId, entityData[metatables[1]], entityData[metatables[2]] --- elseif queryLength == 3 then --- return entityId, entityData[metatables[1]], entityData[metatables[2]], entityData[metatables[3]] --- elseif queryLength == 4 then --- return entityId, --- entityData[metatables[1]], --- entityData[metatables[2]], --- entityData[metatables[3]], --- entityData[metatables[4]] --- elseif queryLength == 5 then --- return entityId, --- entityData[metatables[1]], --- entityData[metatables[2]], --- entityData[metatables[3]], --- entityData[metatables[4]], --- entityData[metatables[5]] --- end - --- for i, metatable in ipairs(metatables) do --- queryOutput[i] = entityData[metatable] --- end - - return entityId, unpack(queryOutput, 1, queryLength) - end - --- return QueryResult.new(self, expand, archetype, compatibleArchetypes, metatables) --- end - local function cleanupQueryChanged(hookState) local world = hookState.world local componentToTrack = hookState.componentToTrack @@ -1297,7 +1352,7 @@ end function World.remove(self: typeof(World.new()), id, ...: ComponentMetatable) error("unimplemented") - local entityRecord = self.entityIndex[id] + local entityRecord = self.entities[id] if entityRecord == nil then error(ERROR_NO_ENTITY, 2) end diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 70ce066e..50bc87c7 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -143,6 +143,14 @@ return function() end) describe("immediate", function() + itFOCUS("should work", function() + local world = World.new() + local A, B = component(), component() + + world:spawnAt(10, A({ a = true }), B({ b = true })) + --print(world:get(10, A)) + end) + it("should be iterable", function() local world = World.new() local A = component() diff --git a/lib/archetype.luau b/lib/archetype.luau index 61cd9c03..e51bb6f5 100644 --- a/lib/archetype.luau +++ b/lib/archetype.luau @@ -17,6 +17,7 @@ local function getValueId(value) end function archetypeOf(componentIds: { number }) + table.sort(componentIds) return table.concat(componentIds, "_") -- local archetype = "" -- for _, component in componentIds do From 4a7740d7ac38b6eb49ba322aba3396e894a6c70b Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 10 Aug 2024 21:25:09 -0400 Subject: [PATCH 46/87] try to handle only single insertions --- benchmarks/insert.bench.luau | 2 ++ lib/World.luau | 63 +++++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/benchmarks/insert.bench.luau b/benchmarks/insert.bench.luau index 93543a0d..4ef8bf06 100644 --- a/benchmarks/insert.bench.luau +++ b/benchmarks/insert.bench.luau @@ -26,6 +26,8 @@ return { for i = 1, N do world:insert(i, A({ i }), B({ i })) end + + --print(world) end, ["Old"] = function(_, _, world) for i = 1, N do diff --git a/lib/World.luau b/lib/World.luau index df38fd86..da232a8c 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -1,6 +1,7 @@ --!strict local Component = require(script.Parent.component) local archetypeModule = require(script.Parent.archetype) +local component = require(script.Parent.component) local topoRuntime = require(script.Parent.topoRuntime) local assertValidComponentInstances = Component.assertValidComponentInstances @@ -22,9 +23,12 @@ type ComponentToId = { [Component]: number } type ArchetypeId = string type Archetype = { id: string, - --indexInArchetypes: number, ownedEntities: { EntityId }, + + --- Maps a component ID to its index in the storage + componentIds: { ComponentId }, + componentToStorageIndex: { [ComponentId]: number }, storage: { { [DenseEntityId]: ComponentInstance } }, } @@ -91,6 +95,7 @@ local function createArchetype(world: World, componentIds: { ComponentId }): Arc id = archetypeId, indexInArchetypes = indexInArchetypes, + componentIds = componentIds, componentToStorageIndex = componentToStorageIndex, storage = storage, @@ -193,9 +198,11 @@ end local function ensureArchetype(world: World, componentIds: { number }) local archetypeId = archetypeOf(componentIds) - local archetype = world.archetypes[archetypeId] - if archetype then - return archetype + local archetypes = world.archetypes + for _, archetype in archetypes do + if archetype.id == archetypeId then + return archetype + end end return createArchetype(world, componentIds) @@ -238,12 +245,52 @@ local function executeInsert(world: World, insertCommand: InsertCommand) debug.profilebegin("World:insert") local entityId = insertCommand.entityId - local idToInstance = {} - for _, componentInstance in insertCommand.componentInstances do - idToInstance[#getmetatable(componentInstance)] = componentInstance + local entityRecord = ensureRecord(world, entityId) + local componentInstances = insertCommand.componentInstances + for _, componentInstance in componentInstances do + local componentId = #getmetatable(componentInstance) + + local startArchetype = entityRecord.archetype + local startComponentIds = startArchetype.componentIds + + local finishComponentIds = table.clone(startComponentIds) + table.insert(finishComponentIds, componentId) + + local finishArchetype = ensureArchetype(world, finishComponentIds) + if startArchetype == finishArchetype then + print("Todo: existing, do not move") + continue + end + + local ownedEntities = finishArchetype.ownedEntities + local finishEntityIndex = #ownedEntities + 1 + ownedEntities[finishEntityIndex] = entityId + + -- Move entity to new archetype + if #startArchetype.componentIds > 0 then + -- Has previous components, need to move them over! + for index, componentStorage in startArchetype.storage do + local finishIndex = finishArchetype.componentToStorageIndex[startArchetype.componentIds[index]] + finishArchetype.storage[finishIndex][finishEntityIndex] = + componentStorage[entityRecord.indexInArchetype :: number] + + componentStorage[entityRecord.indexInArchetype :: number] = nil + table.remove(startArchetype.ownedEntities, entityRecord.indexInArchetype) + end + end + + finishArchetype.storage[finishArchetype.componentToStorageIndex[componentId]][finishEntityIndex] = + componentInstance + + entityRecord.archetype = finishArchetype + entityRecord.indexInArchetype = finishEntityIndex + --print("Finish archetype", finishArchetype) + --print("Archetypes", world.archetypes) end - world:_transitionArchetype(entityId, idToInstance) + -- fo + + -- world:_transitio nArchetype(entityId, componentInstances) -- local componentInstances = insertCommand.componentInstances -- for _, componentInstance in componentInstances do -- local component = getmetatable(componentInstance) From f8a613c735f5dfb8c1f474c9bbfcce265a58cbb5 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 11 Aug 2024 17:22:47 -0400 Subject: [PATCH 47/87] move entities from old archetype to new archetype --- lib/World.luau | 77 ++++++++++++++++++++++++++++----------------- lib/World.spec.luau | 10 ++++-- 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index da232a8c..7b7b812e 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -34,7 +34,7 @@ type Archetype = { } type EntityRecord = { - indexInArchetype: DenseEntityId?, + indexInArchetype: number, archetype: Archetype, } @@ -226,6 +226,48 @@ local function ensureRecord(world: World, entityId: number): EntityRecord return entityRecord :: EntityRecord end +local function transitionArchetype(world: World, entityId: number, entityRecord: EntityRecord, archetype: Archetype) + local oldArchetype = entityRecord.archetype + local oldEntityIndex = entityRecord.indexInArchetype + + -- Add entity to archetype's ownedEntities + local ownedEntities = archetype.ownedEntities + local entityIndex = #ownedEntities + 1 + ownedEntities[entityIndex] = entityId + + -- Move old storage to new storage if needed + local oldNumEntities = #oldArchetype.ownedEntities + local wasLastEntity = oldNumEntities == oldEntityIndex + for index, oldComponentStorage in oldArchetype.storage do + local componentStorage = archetype.storage[archetype.componentToStorageIndex[oldArchetype.componentIds[index]]] + + -- Does the new storage contain this component? + if componentStorage then + componentStorage[entityIndex] = oldComponentStorage[oldEntityIndex] + end + + -- Swap entity component storage + if not wasLastEntity then + oldComponentStorage[oldEntityIndex] = oldComponentStorage[oldNumEntities] + end + + oldComponentStorage[oldNumEntities] = nil + end + + -- Swap entity location marker + if not wasLastEntity then + oldArchetype.ownedEntities[oldEntityIndex] = oldArchetype.ownedEntities[oldNumEntities]; + (world.entities[oldArchetype.ownedEntities[oldEntityIndex]] :: EntityRecord).indexInArchetype = oldEntityIndex + end + + -- Remove from old archetype + oldArchetype.ownedEntities[oldNumEntities] = nil + + -- Mark entity as being in new archetype + entityRecord.indexInArchetype = entityIndex + entityRecord.archetype = archetype +end + local function executeDespawn(world: World, despawnCommand: DespawnCommand) local id = despawnCommand.entityId local entity = world:_getEntity(id) @@ -262,30 +304,11 @@ local function executeInsert(world: World, insertCommand: InsertCommand) continue end - local ownedEntities = finishArchetype.ownedEntities - local finishEntityIndex = #ownedEntities + 1 - ownedEntities[finishEntityIndex] = entityId - - -- Move entity to new archetype - if #startArchetype.componentIds > 0 then - -- Has previous components, need to move them over! - for index, componentStorage in startArchetype.storage do - local finishIndex = finishArchetype.componentToStorageIndex[startArchetype.componentIds[index]] - finishArchetype.storage[finishIndex][finishEntityIndex] = - componentStorage[entityRecord.indexInArchetype :: number] - - componentStorage[entityRecord.indexInArchetype :: number] = nil - table.remove(startArchetype.ownedEntities, entityRecord.indexInArchetype) - end - end - - finishArchetype.storage[finishArchetype.componentToStorageIndex[componentId]][finishEntityIndex] = + transitionArchetype(world, entityId, entityRecord, finishArchetype) + finishArchetype.storage[finishArchetype.componentToStorageIndex[componentId]][entityRecord.indexInArchetype] = componentInstance - entityRecord.archetype = finishArchetype - entityRecord.indexInArchetype = finishEntityIndex - --print("Finish archetype", finishArchetype) - --print("Archetypes", world.archetypes) + print(finishArchetype) end -- fo @@ -696,8 +719,8 @@ end @param ... Component -- The components to fetch @return ... -- Returns the component values in the same order they were passed in ]=] -function World.get(self: typeof(World.new()), id, ...: Component) - local entityRecord = self.entities[id] +function World.get(self: World, entityId, ...: Component) + local entityRecord = self.entities[entityId] if entityRecord == nil then error(ERROR_NO_ENTITY, 2) end @@ -706,10 +729,6 @@ function World.get(self: typeof(World.new()), id, ...: Component) local componentInstances = table.create(length, nil) local archetype = entityRecord.archetype - if archetype == nil then - return componentInstances - end - local componentToStorageIndex = archetype.componentToStorageIndex for i = 1, length do local component = select(i, ...) diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 50bc87c7..63043d38 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -143,12 +143,16 @@ return function() end) describe("immediate", function() - itFOCUS("should work", function() + it("should work", function() local world = World.new() local A, B = component(), component() - world:spawnAt(10, A({ a = true }), B({ b = true })) - --print(world:get(10, A)) + world:spawnAt(10, A({ a = true })) + world:spawnAt(11, A({ a = true, two = true })) + world:spawnAt(12, A({ a = true, three = true })) + + world:insert(10, B({ b = true })) + print(world) end) it("should be iterable", function() From 241b0dc4937c7787ea525c21b75cb079c09e1c4e Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 11 Aug 2024 17:31:45 -0400 Subject: [PATCH 48/87] add component removal --- lib/World.luau | 45 +++++++++++---------------------------------- lib/World.spec.luau | 12 +++++++----- 2 files changed, 18 insertions(+), 39 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 7b7b812e..080db050 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -376,26 +376,17 @@ local function executeReplace(world: World, replaceCommand: ReplaceCommand) end local function executeRemove(world: World, removeCommand: RemoveCommand) - local id = removeCommand.entityId - local entity = world:_getEntity(id) - - local removed = {} - for index, metatable in removeCommand.components do - local oldComponent = entity[metatable] - removed[index] = oldComponent - - world:_trackChanged(metatable, id, oldComponent, nil) - entity[metatable] = nil - end - - -- Rebuild entity metatable cache - local metatables = {} - for metatable in pairs(entity) do - table.insert(metatables, metatable) + local entityId = removeCommand.entityId + local entityRecord = ensureRecord(world, entityId) + local componentIds = table.clone(entityRecord.archetype.componentIds) + for _, component in removeCommand.components do + local index = table.find(componentIds, #component) + if index then + table.remove(componentIds, index) + end end - world._entityMetatablesCache[id] = metatables - world:_transitionArchetype(id, entity) + transitionArchetype(world, entityId, entityRecord, ensureArchetype(world, componentIds)) end local function processCommand(world: World, command: Command) @@ -1415,28 +1406,14 @@ end @param id number -- The entity ID @param ... Component -- The components to remove ]=] -function World.remove(self: typeof(World.new()), id, ...: ComponentMetatable) - error("unimplemented") - +function World.remove(self: World, id, ...: ComponentMetatable) local entityRecord = self.entities[id] if entityRecord == nil then error(ERROR_NO_ENTITY, 2) end local components = { ... } - local length = #components - - local entity = self:_getEntity(id) - local removed = table.create(length, nil) - for index, component in components do - assertValidComponent(component, index) - - local oldComponent = entity[component] - removed[index] = oldComponent - end - - bufferCommand(self, { type = "remove", entityId = id, components = components }) - return unpack(removed, 1, length) + bufferCommand(self :: any, { type = "remove", entityId = id, components = components }) end --[=[ diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 63043d38..f827cd70 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -142,16 +142,18 @@ return function() end) end) - describe("immediate", function() + describeFOCUS("immediate", function() it("should work", function() local world = World.new() local A, B = component(), component() - world:spawnAt(10, A({ a = true })) - world:spawnAt(11, A({ a = true, two = true })) - world:spawnAt(12, A({ a = true, three = true })) + -- world:spawnAt(10, A({ a = true })) + -- world:spawnAt(11, A({ a = true, two = true })) + -- world:spawnAt(12, A({ a = true, three = true })) - world:insert(10, B({ b = true })) + -- world:insert(10, B({ b = true })) + world:spawnAt(10, A({ a = true }), B({ b = true })) + world:remove(10, B) print(world) end) From f5d80d06ad4d9e54d0c02d95aa0766b7ddd08b54 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 11 Aug 2024 18:02:40 -0400 Subject: [PATCH 49/87] smarter insertions --- benchmarks/insert.bench.luau | 6 +-- lib/World.luau | 89 +++++++++++------------------------- lib/World.spec.luau | 18 ++++++-- 3 files changed, 41 insertions(+), 72 deletions(-) diff --git a/benchmarks/insert.bench.luau b/benchmarks/insert.bench.luau index 4ef8bf06..9e7d45fd 100644 --- a/benchmarks/insert.bench.luau +++ b/benchmarks/insert.bench.luau @@ -24,14 +24,12 @@ return { Functions = { ["New"] = function(_, world) for i = 1, N do - world:insert(i, A({ i }), B({ i })) + world:insert(i, A({ i })) end - - --print(world) end, ["Old"] = function(_, _, world) for i = 1, N do - world:insert(i, A({ i }), B({ i })) + world:insert(i, A({ i })) end end, }, diff --git a/lib/World.luau b/lib/World.luau index 080db050..1dd8431c 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -30,7 +30,7 @@ type Archetype = { componentIds: { ComponentId }, componentToStorageIndex: { [ComponentId]: number }, - storage: { { [DenseEntityId]: ComponentInstance } }, + storage: { { ComponentInstance } }, } type EntityRecord = { @@ -42,10 +42,10 @@ type EntityRecord = { type Entities = { [EntityId]: EntityRecord? } -- Find archetypes containing component -type ComponentToArchetypes = { [ComponentId]: { [ArchetypeId]: number } } +type ComponentToArchetypes = { [ComponentId]: { number } } -- Find archetype from all components -type Archetypes = { [ArchetypeId]: Archetype } +type Archetypes = { Archetype } local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" local ERROR_EXISTING_ENTITY = @@ -105,7 +105,7 @@ local function createArchetype(world: World, componentIds: { ComponentId }): Arc for index, componentId in componentIds do local associatedArchetypes = world.componentToArchetypes[componentId] or ({} :: any) - associatedArchetypes[archetypeId] = index + associatedArchetypes[indexInArchetypes] = index world.componentToArchetypes[componentId] = associatedArchetypes componentToStorageIndex[componentId] = index @@ -124,6 +124,8 @@ function World.new() entities = {} :: Entities, componentToArchetypes = {} :: ComponentToArchetypes, archetypes = {} :: Archetypes, + archetypeHashToIndex = {} :: { [string]: number }, + -- Map from archetype string --> entity ID --> entity data storage = {}, @@ -226,7 +228,12 @@ local function ensureRecord(world: World, entityId: number): EntityRecord return entityRecord :: EntityRecord end -local function transitionArchetype(world: World, entityId: number, entityRecord: EntityRecord, archetype: Archetype) +local function transitionArchetype( + world: World, + entityId: number, + entityRecord: EntityRecord, + archetype: Archetype +): number local oldArchetype = entityRecord.archetype local oldEntityIndex = entityRecord.indexInArchetype @@ -266,6 +273,8 @@ local function transitionArchetype(world: World, entityId: number, entityRecord: -- Mark entity as being in new archetype entityRecord.indexInArchetype = entityIndex entityRecord.archetype = archetype + + return entityIndex end local function executeDespawn(world: World, despawnCommand: DespawnCommand) @@ -289,61 +298,20 @@ local function executeInsert(world: World, insertCommand: InsertCommand) local entityId = insertCommand.entityId local entityRecord = ensureRecord(world, entityId) local componentInstances = insertCommand.componentInstances + + local oldArchetype = entityRecord.archetype for _, componentInstance in componentInstances do local componentId = #getmetatable(componentInstance) + local componentIds = table.clone(oldArchetype.componentIds) + table.insert(componentIds, componentId) - local startArchetype = entityRecord.archetype - local startComponentIds = startArchetype.componentIds - - local finishComponentIds = table.clone(startComponentIds) - table.insert(finishComponentIds, componentId) + local archetype = ensureArchetype(world, componentIds) + local entityIndex = transitionArchetype(world, entityId, entityRecord, archetype) + archetype.storage[archetype.componentToStorageIndex[componentId]][entityIndex] = componentInstance - local finishArchetype = ensureArchetype(world, finishComponentIds) - if startArchetype == finishArchetype then - print("Todo: existing, do not move") - continue - end - - transitionArchetype(world, entityId, entityRecord, finishArchetype) - finishArchetype.storage[finishArchetype.componentToStorageIndex[componentId]][entityRecord.indexInArchetype] = - componentInstance - - print(finishArchetype) + oldArchetype = archetype end - -- fo - - -- world:_transitio nArchetype(entityId, componentInstances) - -- local componentInstances = insertCommand.componentInstances - -- for _, componentInstance in componentInstances do - -- local component = getmetatable(componentInstance) - -- local componentId = #component - - -- -- First, get the entities current archetype - -- local record = ensureRecord(world, entityId) - -- local oldArchetype = record.archetype - -- local newComponentIds = table.clone(oldArchetype.componentToStorageIndex) - -- table.insert(newComponentIds, componentId) - - -- --local newArchetype = ensureArchetype(world, newComponentIds) - -- -- local componentStorage = newArchetype.storage[newArchetype.componentToStorageIndex[componentId]] - -- -- if oldArchetype == newArchetype then - -- -- -- Set the data - -- -- --oldArchetype.storage[oldArchetype.componentToStorageIndex[componentId]] = componentInstance - -- -- else - -- -- -- Remove from old archetype - -- -- for _, storageIndex in oldArchetype.componentToStorageIndex do - -- -- -- TODO: - -- -- -- swap remove - -- -- table.remove(archetype.storage[storageIndex], entityRecord.indexInArchetype) - -- -- end - - -- -- print("New archetype.") - -- -- end - - -- print("oldArchetype", oldArchetype, "newArchetype", newArchetype) - -- end - debug.profileend() end @@ -1118,7 +1086,6 @@ function World.query(self: World, ...) -- TODO: -- cache queries local components = { ... } - local A, B = ... local a, b = ... local queryLength = select("#", ...) @@ -1143,9 +1110,9 @@ function World.query(self: World, ...) end end - -- Narrow the archetypes so only ones that contain all components are searched. - for archetypeId in possibleArchetypes do - local archetype = self.archetypes[archetypeId] + -- Narrow the archetypes so only ones that contain all components are searched + for archetypeIndex in possibleArchetypes do + local archetype = self.archetypes[archetypeIndex] local incompatible = false for _, componentId in components do -- Does this archetype have this component? @@ -1167,7 +1134,7 @@ function World.query(self: World, ...) local currentArchetype = compatibleArchetypes[currentArchetypeIndex] local currentArchetypeNumEntities = #currentArchetype.ownedEntities local currentEntityIndex = 0 - local function nextEntity() + local function nextEntity(): any currentEntityIndex += 1 if currentEntityIndex > currentArchetypeNumEntities then currentEntityIndex = 0 @@ -1201,10 +1168,6 @@ function World.query(self: World, ...) end local function iter() - -- currentArchetype = self.archetypes[next(compatibleArchetypes)] - -- currentArchetypeNumEntities = #currentArchetype.ownedEntities - -- currentEntityIndex = 0 - return nextEntity end diff --git a/lib/World.spec.luau b/lib/World.spec.luau index f827cd70..742d69b9 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -142,19 +142,27 @@ return function() end) end) - describeFOCUS("immediate", function() - it("should work", function() + describe("immediate", function() + itFOCUS("should work", function() local world = World.new() local A, B = component(), component() + world:spawnAt(10) + world:insert(10, A({ a = true }), B({ b = true })) + -- world:spawnAt(11) + -- world:insert(11, A({ a = true, two = true }), B({ b = true, two = true })) + print(world.archetypes) -- world:spawnAt(10, A({ a = true })) -- world:spawnAt(11, A({ a = true, two = true })) -- world:spawnAt(12, A({ a = true, three = true })) -- world:insert(10, B({ b = true })) - world:spawnAt(10, A({ a = true }), B({ b = true })) - world:remove(10, B) - print(world) + --world:spawnAt(10, A({ a = true }), B({ b = true })) + --world:remove(10, B) + -- for id, a, b in world:query(A, B) do + -- print(id, a, b) + -- end + --print(world) end) it("should be iterable", function() From 70a4d841104c6984f645b5be7e91e5af09e30f2d Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 12 Aug 2024 17:39:24 -0400 Subject: [PATCH 50/87] cache references to component storage in queries --- lib/World.luau | 54 ++++++++++++++++++++++++++------------------- lib/World.spec.luau | 7 +++--- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 1dd8431c..53eb0326 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -1086,13 +1086,14 @@ function World.query(self: World, ...) -- TODO: -- cache queries local components = { ... } - local a, b = ... + local A, B = ... + local a, b = nil, nil local queryLength = select("#", ...) if queryLength == 1 then - components = { #a } + components = { #A } elseif queryLength == 2 then - components = { #a, #b } + components = { #A, #B } else error("Unimplemented query length") end @@ -1130,38 +1131,44 @@ function World.query(self: World, ...) table.insert(compatibleArchetypes, archetype) end + local currentEntityIndex = 1 local currentArchetypeIndex = 1 - local currentArchetype = compatibleArchetypes[currentArchetypeIndex] - local currentArchetypeNumEntities = #currentArchetype.ownedEntities - local currentEntityIndex = 0 + local currentArchetype = compatibleArchetypes[1] + + local function cacheComponentStorages() + local storage, componentToStorageIndex = currentArchetype.storage, currentArchetype.componentToStorageIndex + if queryLength == 1 then + a = storage[componentToStorageIndex[components[1]]] + elseif queryLength == 2 then + a = storage[componentToStorageIndex[components[1]]] + b = storage[componentToStorageIndex[components[2]]] + end + end + + local entityId: number local function nextEntity(): any - currentEntityIndex += 1 - if currentEntityIndex > currentArchetypeNumEntities then - currentEntityIndex = 0 + entityId = currentArchetype.ownedEntities[currentEntityIndex] + --print("currentEntityIndex", currentEntityIndex, "entityId", entityId) + while entityId == nil do + currentEntityIndex = 1 currentArchetypeIndex += 1 currentArchetype = compatibleArchetypes[currentArchetypeIndex] if currentArchetype == nil then - -- Out of entities. return nil end - currentArchetypeNumEntities = #currentArchetype.ownedEntities - return nextEntity() + cacheComponentStorages() + entityId = currentArchetype.ownedEntities[currentEntityIndex] end - local entityId = currentArchetype.ownedEntities[currentEntityIndex] - local storage, componentToStorageIndex = currentArchetype.storage, currentArchetype.componentToStorageIndex + local entityIndex = currentEntityIndex + currentEntityIndex += 1 + + local entityId = currentArchetype.ownedEntities[entityIndex] if queryLength == 1 then - return entityId, storage[componentToStorageIndex[components[1]]][currentEntityIndex] + return entityId, a[entityIndex] elseif queryLength == 2 then - return entityId, - storage[componentToStorageIndex[components[1]]][currentEntityIndex], - storage[componentToStorageIndex[components[2]]][currentEntityIndex] - elseif queryLength == 3 then - return entityId, - storage[componentToStorageIndex[components[1]]][currentEntityIndex], - storage[componentToStorageIndex[components[2]]][currentEntityIndex], - storage[componentToStorageIndex[components[3]]][currentEntityIndex] + return entityId, a[entityIndex], b[entityIndex] else error("Unimplemented Query Length") end @@ -1171,6 +1178,7 @@ function World.query(self: World, ...) return nextEntity end + cacheComponentStorages() return setmetatable({}, { __iter = iter, next = nextEntity, diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 742d69b9..f62a85a6 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -149,6 +149,7 @@ return function() world:spawnAt(10) world:insert(10, A({ a = true }), B({ b = true })) + world:spawnAt(50, A({ a = true, two = true }), B({ b = true, two = true })) -- world:spawnAt(11) -- world:insert(11, A({ a = true, two = true }), B({ b = true, two = true })) print(world.archetypes) @@ -159,9 +160,9 @@ return function() -- world:insert(10, B({ b = true })) --world:spawnAt(10, A({ a = true }), B({ b = true })) --world:remove(10, B) - -- for id, a, b in world:query(A, B) do - -- print(id, a, b) - -- end + for id, a, b in world:query(A, B) do + print(id, a, b) + end --print(world) end) From 55cc4e7a2a3abe782898b6737826103524545f21 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 12 Aug 2024 17:46:16 -0400 Subject: [PATCH 51/87] add despawning --- lib/World.luau | 16 ++++++---------- lib/World.spec.luau | 4 ++-- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 53eb0326..3e5df4e9 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -278,16 +278,13 @@ local function transitionArchetype( end local function executeDespawn(world: World, despawnCommand: DespawnCommand) - local id = despawnCommand.entityId - local entity = world:_getEntity(id) - - for metatable, component in pairs(entity) do - world:_trackChanged(metatable, id, component, nil) - end + local entityId = despawnCommand.entityId + local entityRecord = ensureRecord(world, entityId) - local shouldOnlyClear = world.deferring and world.markedForDeletion[id] ~= true - world._entityMetatablesCache[id] = if shouldOnlyClear then {} else nil - world:_transitionArchetype(id, if shouldOnlyClear then {} else nil) + -- TODO: + -- Optimize remove so no cascades + transitionArchetype(world, entityId, entityRecord, world.rootArchetype) + table.remove(world.rootArchetype.ownedEntities, entityRecord.indexInArchetype) world._size -= 1 end @@ -1148,7 +1145,6 @@ function World.query(self: World, ...) local entityId: number local function nextEntity(): any entityId = currentArchetype.ownedEntities[currentEntityIndex] - --print("currentEntityIndex", currentEntityIndex, "entityId", entityId) while entityId == nil do currentEntityIndex = 1 currentArchetypeIndex += 1 diff --git a/lib/World.spec.luau b/lib/World.spec.luau index f62a85a6..754fff0b 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -142,8 +142,8 @@ return function() end) end) - describe("immediate", function() - itFOCUS("should work", function() + describeFOCUS("immediate", function() + it("should work", function() local world = World.new() local A, B = component(), component() From 30fbfea654c692f2f42f30ce9aa090ef72e5ee27 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 12 Aug 2024 17:58:23 -0400 Subject: [PATCH 52/87] fix get not using component id --- lib/World.luau | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 3e5df4e9..2fcb90ea 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -691,7 +691,7 @@ function World.get(self: World, entityId, ...: Component) assertValidComponent(component, i) -- Does this component belong to the archetype that this entity is in? - local storageIndex = componentToStorageIndex[component] + local storageIndex = componentToStorageIndex[#component] if storageIndex == nil then continue end @@ -702,9 +702,7 @@ function World.get(self: World, entityId, ...: Component) return unpack(componentInstances, 1, length) end - local function noop() end - local noopQuery = setmetatable({ next = noop, snapshot = function() @@ -1100,7 +1098,7 @@ function World.query(self: World, ...) for _, componentId in components do local associatedArchetypes = self.componentToArchetypes[componentId] if associatedArchetypes == nil then - error("No-op query unimplemented") + return noopQuery end if possibleArchetypes == nil or #possibleArchetypes > #associatedArchetypes then @@ -1175,9 +1173,8 @@ function World.query(self: World, ...) end cacheComponentStorages() - return setmetatable({}, { + return setmetatable({ next = nextEntity }, { __iter = iter, - next = nextEntity, }) end @@ -1380,7 +1377,16 @@ function World.remove(self: World, id, ...: ComponentMetatable) end local components = { ... } + local componentInstances = {} + local archetype = entityRecord.archetype + for _, component in components do + local componentId = #component + local storage = archetype.storage[archetype.componentToStorageIndex[componentId]] + table.insert(componentInstances, if storage then storage[entityRecord.indexInArchetype] else nil) + end + bufferCommand(self :: any, { type = "remove", entityId = id, components = components }) + return unpack(componentInstances) end --[=[ From 6698ebc81ab2a1919334e097b7f39ee8a92f1ccc Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 12 Aug 2024 19:10:47 -0400 Subject: [PATCH 53/87] add :without --- lib/World.luau | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/World.luau b/lib/World.luau index 2fcb90ea..7df4b552 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -702,6 +702,7 @@ function World.get(self: World, entityId, ...: Component) return unpack(componentInstances, 1, length) end + local function noop() end local noopQuery = setmetatable({ next = noop, @@ -1172,8 +1173,42 @@ function World.query(self: World, ...) return nextEntity end + local function without(query, ...: Component) + local numComponents = select("#", ...) + local numCompatibleArchetypes = #compatibleArchetypes + for archetypeIndex = numCompatibleArchetypes, 1, -1 do + local archetype = compatibleArchetypes[archetypeIndex] + local shouldRemove = false + for componentIndex = 1, numComponents do + local component = select(componentIndex, ...) + if archetype.componentToStorageIndex[#component] then + shouldRemove = true + break + end + end + + if shouldRemove then + -- Swap remove + if archetypeIndex ~= numCompatibleArchetypes then + compatibleArchetypes[archetypeIndex] = compatibleArchetypes[numCompatibleArchetypes] + end + + compatibleArchetypes[numCompatibleArchetypes] = nil + numCompatibleArchetypes -= 1 + end + end + + if numCompatibleArchetypes == 0 then + return noopQuery + end + + currentArchetype = compatibleArchetypes[1] + cacheComponentStorages() + return query + end + cacheComponentStorages() - return setmetatable({ next = nextEntity }, { + return setmetatable({ next = nextEntity, without = without }, { __iter = iter, }) end From 7b389998eceab7b8a252d1cc4b50e13186335bd7 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 12 Aug 2024 19:32:37 -0400 Subject: [PATCH 54/87] add world iterator --- lib/World.luau | 87 +++++++++++++++++++++++++++++++-------------- lib/World.spec.luau | 5 +-- 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 7df4b552..2e2b6845 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -26,10 +26,15 @@ type Archetype = { ownedEntities: { EntityId }, - --- Maps a component ID to its index in the storage + --- The component IDs that are part of this archetype, in no particular order componentIds: { ComponentId }, - componentToStorageIndex: { [ComponentId]: number }, + --- Maps a component ID to its index in the storage + componentIdToStorageIndex: { [ComponentId]: number }, + + --- Maps a storage index to its component ID, useful for iterating the world + storageIndexToComponentId: { [number]: ComponentId }, + storage: { { ComponentInstance } }, } @@ -86,7 +91,7 @@ World.__index = World local function createArchetype(world: World, componentIds: { ComponentId }): Archetype local archetypeId = archetypeOf(componentIds) - local componentToStorageIndex = {} + local componentIdToStorageIndex, storageIndexToComponentId = {}, {} local length = #componentIds local storage = table.create(length) @@ -96,7 +101,10 @@ local function createArchetype(world: World, componentIds: { ComponentId }): Arc indexInArchetypes = indexInArchetypes, componentIds = componentIds, - componentToStorageIndex = componentToStorageIndex, + + componentIdToStorageIndex = componentIdToStorageIndex, + storageIndexToComponentId = storageIndexToComponentId, + storage = storage, -- Keep track of all entity ids for fast iteration in queries @@ -108,7 +116,9 @@ local function createArchetype(world: World, componentIds: { ComponentId }): Arc associatedArchetypes[indexInArchetypes] = index world.componentToArchetypes[componentId] = associatedArchetypes - componentToStorageIndex[componentId] = index + componentIdToStorageIndex[componentId] = index + storageIndexToComponentId[index] = componentId + storage[index] = {} end @@ -122,6 +132,7 @@ end function World.new() local self = setmetatable({ entities = {} :: Entities, + componentIdToComponent = {}, componentToArchetypes = {} :: ComponentToArchetypes, archetypes = {} :: Archetypes, archetypeHashToIndex = {} :: { [string]: number }, @@ -194,8 +205,26 @@ end @return number @return {[Component]: ComponentInstance} ]=] -function World:__iter() - return World._next, self +function World.__iter(world: World) + local lastEntityId = nil + return function(): (number?, ...any) + local entityId, entityRecord = next(world.entities, lastEntityId) + if entityId == nil or entityRecord == nil then + return nil + end + + lastEntityId = entityId + + local componentIdToComponent = world.componentIdToComponent + local archetype = entityRecord.archetype + local componentInstances = {} + for index, componentStorage in archetype.storage do + componentInstances[componentIdToComponent[archetype.storageIndexToComponentId[index]]] = + componentStorage[entityRecord.indexInArchetype] + end + + return entityId, componentInstances + end end local function ensureArchetype(world: World, componentIds: { number }) @@ -246,7 +275,8 @@ local function transitionArchetype( local oldNumEntities = #oldArchetype.ownedEntities local wasLastEntity = oldNumEntities == oldEntityIndex for index, oldComponentStorage in oldArchetype.storage do - local componentStorage = archetype.storage[archetype.componentToStorageIndex[oldArchetype.componentIds[index]]] + local componentStorage = + archetype.storage[archetype.componentIdToStorageIndex[oldArchetype.componentIds[index]]] -- Does the new storage contain this component? if componentStorage then @@ -298,13 +328,18 @@ local function executeInsert(world: World, insertCommand: InsertCommand) local oldArchetype = entityRecord.archetype for _, componentInstance in componentInstances do - local componentId = #getmetatable(componentInstance) + local component = getmetatable(componentInstance) + local componentId = #component local componentIds = table.clone(oldArchetype.componentIds) table.insert(componentIds, componentId) local archetype = ensureArchetype(world, componentIds) local entityIndex = transitionArchetype(world, entityId, entityRecord, archetype) - archetype.storage[archetype.componentToStorageIndex[componentId]][entityIndex] = componentInstance + archetype.storage[archetype.componentIdToStorageIndex[componentId]][entityIndex] = componentInstance + + -- FIXME: + -- This is slow + world.componentIdToComponent[componentId] = component oldArchetype = archetype end @@ -494,12 +529,11 @@ function World._transitionArchetype( if toAdd then local entityRecord = ensureRecord(self, entityId) local oldArchetype = entityRecord.archetype - local oldComponentIds = oldArchetype.componentToStorageIndex + local oldComponentIds = oldArchetype.componentIdToStorageIndex local newComponentIds = table.clone(oldComponentIds) local hasNew = false for componentId, componentInstance in toAdd do - -- table.insert(newComponentIds, componentId) if not table.find(newComponentIds, componentId) then table.insert(newComponentIds, componentId) hasNew = true @@ -515,12 +549,12 @@ function World._transitionArchetype( newArchetype.ownedEntities[indexInArchetype] = entityId for _, componentId in newComponentIds do - local oldStorageIndex = oldArchetype.componentToStorageIndex[componentId] + local oldStorageIndex = oldArchetype.componentIdToStorageIndex[componentId] if oldStorageIndex then --warn("TODO: exist") else -- It doesn't exist in the old archetype, so just add. - newArchetype.storage[newArchetype.componentToStorageIndex[componentId]][indexInArchetype] = + newArchetype.storage[newArchetype.componentIdToStorageIndex[componentId]][indexInArchetype] = toAdd[componentId] end end @@ -539,7 +573,7 @@ function World._transitionArchetype( -- local entityRecord = ensureRecord(self, entityId) -- local oldArchetype = entityRecord.archetype - -- local oldComponentIds = oldArchetype.componentToStorageIndex + -- local oldComponentIds = oldArchetype.componentIdToStorageIndex -- local newComponentIds = table.clone(oldComponentIds) -- local storage @@ -568,7 +602,7 @@ function World._transitionArchetype( -- return -- end - -- for _, storageIndex in archetype.componentToStorageIndex do + -- for _, storageIndex in archetype.componentIdToStorageIndex do -- -- TODO: -- -- swap remove -- table.remove(archetype.storage[storageIndex], entityRecord.indexInArchetype) @@ -602,7 +636,7 @@ function World._transitionArchetype( -- -- Add entity to archetype -- local indexInArchetype = #newArchetype.ownedEntities + 1 -- for component, componentInstance in myComponents do - -- newArchetype.storage[newArchetype.componentToStorageIndex[#component]][indexInArchetype] = componentInstance + -- newArchetype.storage[newArchetype.componentIdToStorageIndex[#component]][indexInArchetype] = componentInstance -- end -- entityRecord.indexInArchetype = indexInArchetype @@ -685,13 +719,13 @@ function World.get(self: World, entityId, ...: Component) local componentInstances = table.create(length, nil) local archetype = entityRecord.archetype - local componentToStorageIndex = archetype.componentToStorageIndex + local componentIdToStorageIndex = archetype.componentIdToStorageIndex for i = 1, length do local component = select(i, ...) assertValidComponent(component, i) -- Does this component belong to the archetype that this entity is in? - local storageIndex = componentToStorageIndex[#component] + local storageIndex = componentIdToStorageIndex[#component] if storageIndex == nil then continue end @@ -1113,7 +1147,7 @@ function World.query(self: World, ...) local incompatible = false for _, componentId in components do -- Does this archetype have this component? - if archetype.componentToStorageIndex[componentId] == nil then + if archetype.componentIdToStorageIndex[componentId] == nil then -- Nope, so we can't use this one. incompatible = true break @@ -1132,12 +1166,12 @@ function World.query(self: World, ...) local currentArchetype = compatibleArchetypes[1] local function cacheComponentStorages() - local storage, componentToStorageIndex = currentArchetype.storage, currentArchetype.componentToStorageIndex + local storage, componentIdToStorageIndex = currentArchetype.storage, currentArchetype.componentIdToStorageIndex if queryLength == 1 then - a = storage[componentToStorageIndex[components[1]]] + a = storage[componentIdToStorageIndex[components[1]]] elseif queryLength == 2 then - a = storage[componentToStorageIndex[components[1]]] - b = storage[componentToStorageIndex[components[2]]] + a = storage[componentIdToStorageIndex[components[1]]] + b = storage[componentIdToStorageIndex[components[2]]] end end @@ -1181,14 +1215,13 @@ function World.query(self: World, ...) local shouldRemove = false for componentIndex = 1, numComponents do local component = select(componentIndex, ...) - if archetype.componentToStorageIndex[#component] then + if archetype.componentIdToStorageIndex[#component] then shouldRemove = true break end end if shouldRemove then - -- Swap remove if archetypeIndex ~= numCompatibleArchetypes then compatibleArchetypes[archetypeIndex] = compatibleArchetypes[numCompatibleArchetypes] end @@ -1416,7 +1449,7 @@ function World.remove(self: World, id, ...: ComponentMetatable) local archetype = entityRecord.archetype for _, component in components do local componentId = #component - local storage = archetype.storage[archetype.componentToStorageIndex[componentId]] + local storage = archetype.storage[archetype.componentIdToStorageIndex[componentId]] table.insert(componentInstances, if storage then storage[entityRecord.indexInArchetype] else nil) end diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 754fff0b..d3c6dad2 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -142,7 +142,7 @@ return function() end) end) - describeFOCUS("immediate", function() + describe("immediate", function() it("should work", function() local world = World.new() local A, B = component(), component() @@ -166,7 +166,7 @@ return function() --print(world) end) - it("should be iterable", function() + itFOCUS("should be iterable", function() local world = World.new() local A = component() local B = component() @@ -177,6 +177,7 @@ return function() local count = 0 for id, data in world do + print(id, data) count += 1 if id == eA then expect(data[A]).to.be.ok() From c7b4c5103ac3533607e419ee3017fd174d6f860c Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 12 Aug 2024 19:58:37 -0400 Subject: [PATCH 55/87] track changed and replace --- lib/World.luau | 62 ++++++++++++++++++++++++++++++++------------- lib/World.spec.luau | 4 +-- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 2e2b6845..62ae2123 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -245,8 +245,6 @@ local function ensureRecord(world: World, entityId: number): EntityRecord local rootArchetype = world.rootArchetype entityRecord = { archetype = rootArchetype, - - -- O(N) for length indexInArchetype = #rootArchetype.ownedEntities + 1, } @@ -310,6 +308,14 @@ end local function executeDespawn(world: World, despawnCommand: DespawnCommand) local entityId = despawnCommand.entityId local entityRecord = ensureRecord(world, entityId) + local archetype = entityRecord.archetype + + -- Track changes + for _, componentStorage in archetype.storage do + local componentInstance = componentStorage[entityRecord.indexInArchetype] + local component = getmetatable(componentInstance :: any) + world:_trackChanged(component, entityId, componentInstance, nil) + end -- TODO: -- Optimize remove so no cascades @@ -335,10 +341,12 @@ local function executeInsert(world: World, insertCommand: InsertCommand) local archetype = ensureArchetype(world, componentIds) local entityIndex = transitionArchetype(world, entityId, entityRecord, archetype) + local oldComponentInstance = archetype.storage[archetype.componentIdToStorageIndex[componentId]][entityIndex] archetype.storage[archetype.componentIdToStorageIndex[componentId]][entityIndex] = componentInstance + world:_trackChanged(component, entityId, oldComponentInstance, componentInstance) -- FIXME: - -- This is slow + -- This shouldn't be in a hotpath, probably better in createArchetype world.componentIdToComponent[componentId] = component oldArchetype = archetype @@ -348,38 +356,56 @@ local function executeInsert(world: World, insertCommand: InsertCommand) end local function executeReplace(world: World, replaceCommand: ReplaceCommand) - local id = replaceCommand.entityId - if not world:contains(id) then + local entityId = replaceCommand.entityId + if not world:contains(entityId) then error(ERROR_NO_ENTITY, 2) end - local components = {} - local metatables = {} - local entity = world:_getEntity(id) + local entityRecord = ensureRecord(world, entityId) + local oldArchetype = entityRecord.archetype + local componentIds = {} + local componentIdMap = {} + -- Track new for _, componentInstance in replaceCommand.componentInstances do - local metatable = getmetatable(componentInstance) - world:_trackChanged(metatable, id, entity[metatable], componentInstance) + local component = getmetatable(componentInstance) + local componentId = #component + table.insert(componentIds, componentId) - components[metatable] = componentInstance - table.insert(metatables, metatable) + local storageIndex = oldArchetype.componentIdToStorageIndex[componentId] + world:_trackChanged( + component, + id, + if storageIndex then oldArchetype.storage[storageIndex][entityRecord.indexInArchetype] else nil, + componentInstance + ) + + componentIdMap[componentId] = true end - for metatable, component in pairs(entity) do - if not components[metatable] then - world:_trackChanged(metatable, id, component, nil) + -- Track removed + for index, componentStorage in oldArchetype.storage do + local componentId = oldArchetype.storageIndexToComponentId[index] + if componentIdMap[componentId] == nil then + local component = world.componentIdToComponent[componentId] + world:_trackChanged(component, entityId, componentStorage[oldArchetypeIndex], nil) end end - world._entityMetatablesCache[id] = metatables - world:_transitionArchetype(id, components) + transitionArchetype(world, entityId, entityRecord, world.rootArchetype) + transitionArchetype(world, entityId, entityRecord, ensureArchetype(world, componentIds)) end local function executeRemove(world: World, removeCommand: RemoveCommand) local entityId = removeCommand.entityId local entityRecord = ensureRecord(world, entityId) + local archetype = entityRecord.archetype local componentIds = table.clone(entityRecord.archetype.componentIds) for _, component in removeCommand.components do + local componentInstance = + archetype.storage[archetype.componentIdToStorageIndex[#component]][entityRecord.indexInArchetype] + world:_trackChanged(component, entityId, componentInstance, nil) + local index = table.find(componentIds, #component) if index then table.remove(componentIds, index) @@ -1371,7 +1397,7 @@ function World:queryChanged(componentToTrack, ...: nil) end end -function World:_trackChanged(metatable, id, old, new) +function World._trackChanged(self: World, metatable, id, old, new) if not self._changedStorage[metatable] then return end diff --git a/lib/World.spec.luau b/lib/World.spec.luau index d3c6dad2..d965a2f5 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -166,7 +166,7 @@ return function() --print(world) end) - itFOCUS("should be iterable", function() + it("should be iterable", function() local world = World.new() local A = component() local B = component() @@ -483,7 +483,7 @@ return function() expect(withoutCount).to.equal(0) end) - it("should track changes", function() + itFOCUS("should track changes", function() local world = World.new() local loop = Loop.new(world) From 7517122c447eb50072938d1d950e64ac3eaa2277 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 12 Aug 2024 20:09:16 -0400 Subject: [PATCH 56/87] fix empty query edge cases --- lib/World.luau | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 62ae2123..e4c86c64 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -1142,16 +1142,20 @@ function World.query(self: World, ...) -- TODO: -- cache queries local components = { ... } - local A, B = ... - local a, b = nil, nil + local A, B, C, D = ... + local a, b, c, d = nil, nil, nil, nil local queryLength = select("#", ...) if queryLength == 1 then components = { #A } elseif queryLength == 2 then components = { #A, #B } + elseif queryLength == 3 then + components = { #A, #B, #C } + elseif queryLength == 4 then + components = { #A, #B, #C, #D } else - error("Unimplemented query length") + error("Unimplemented query length " .. queryLength) end local possibleArchetypes @@ -1187,17 +1191,34 @@ function World.query(self: World, ...) table.insert(compatibleArchetypes, archetype) end + if #compatibleArchetypes == 0 then + return noopQuery + end + local currentEntityIndex = 1 local currentArchetypeIndex = 1 local currentArchetype = compatibleArchetypes[1] local function cacheComponentStorages() + if currentArchetype == nil then + return + end + local storage, componentIdToStorageIndex = currentArchetype.storage, currentArchetype.componentIdToStorageIndex if queryLength == 1 then a = storage[componentIdToStorageIndex[components[1]]] elseif queryLength == 2 then a = storage[componentIdToStorageIndex[components[1]]] b = storage[componentIdToStorageIndex[components[2]]] + elseif queryLength == 3 then + a = storage[componentIdToStorageIndex[components[1]]] + b = storage[componentIdToStorageIndex[components[2]]] + c = storage[componentIdToStorageIndex[components[3]]] + elseif queryLength == 4 then + a = storage[componentIdToStorageIndex[components[1]]] + b = storage[componentIdToStorageIndex[components[2]]] + c = storage[componentIdToStorageIndex[components[3]]] + d = storage[componentIdToStorageIndex[components[4]]] end end @@ -1224,6 +1245,10 @@ function World.query(self: World, ...) return entityId, a[entityIndex] elseif queryLength == 2 then return entityId, a[entityIndex], b[entityIndex] + elseif queryLength == 3 then + return entityId, a[entityIndex], b[entityIndex], c[entityIndex] + elseif queryLength == 4 then + return entityId, a[entityIndex], b[entityIndex], c[entityIndex], d[entityIndex] else error("Unimplemented Query Length") end From 6fb750d13927c0eb3cbefcf5fbd678af978c09b0 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 12 Aug 2024 20:13:45 -0400 Subject: [PATCH 57/87] remove unused archetype transition fn --- lib/World.luau | 128 ------------------------------------------------- 1 file changed, 128 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index e4c86c64..90dc9db8 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -545,134 +545,6 @@ function World:_updateQueryCache(entityArchetype) end end -function World._transitionArchetype( - self: typeof(World.new()), - entityId: EntityId, - toAdd: { [ComponentId]: ComponentInstance }?, - toRemove: { Component }? -) - debug.profilebegin("transitionArchetype") - if toAdd then - local entityRecord = ensureRecord(self, entityId) - local oldArchetype = entityRecord.archetype - local oldComponentIds = oldArchetype.componentIdToStorageIndex - local newComponentIds = table.clone(oldComponentIds) - local hasNew = false - - for componentId, componentInstance in toAdd do - if not table.find(newComponentIds, componentId) then - table.insert(newComponentIds, componentId) - hasNew = true - end - end - - if hasNew then - -- Move from old to new - -- todo: remove from old ownedEntities - local oldIndexInArchetype = entityRecord.indexInArchetype - local newArchetype = ensureArchetype(self, newComponentIds) - local indexInArchetype = #newArchetype.ownedEntities + 1 - newArchetype.ownedEntities[indexInArchetype] = entityId - - for _, componentId in newComponentIds do - local oldStorageIndex = oldArchetype.componentIdToStorageIndex[componentId] - if oldStorageIndex then - --warn("TODO: exist") - else - -- It doesn't exist in the old archetype, so just add. - newArchetype.storage[newArchetype.componentIdToStorageIndex[componentId]][indexInArchetype] = - toAdd[componentId] - end - end - - -- todo: swap to avoid cascading moves - table.remove(oldArchetype.ownedEntities, oldIndexInArchetype) - - entityRecord.indexInArchetype = indexInArchetype - entityRecord.archetype = newArchetype - --print("oldArchetype", oldArchetype, "newArchetype", newArchetype) - else - --warn("TODO: none new") - end - elseif toRemove then - end - - -- local entityRecord = ensureRecord(self, entityId) - -- local oldArchetype = entityRecord.archetype - -- local oldComponentIds = oldArchetype.componentIdToStorageIndex - -- local newComponentIds = table.clone(oldComponentIds) - -- local storage - - -- if toAdd then - -- for _, componentInstance in toAdd do - -- table.insert(newComponentIds, #getmetatable(componentInstance)) - -- end - -- end - - -- if toRemove then - - -- end - - -- local newArchetype = entityRecord. - -- if myComponents == nil then - -- -- Remove all components - -- local entityRecord = self.entities[id] - -- if entityRecord == nil then - -- warn("Tried to transitionArchetype a dead entity") - -- return - -- end - - -- local archetype = entityRecord.archetype - -- if archetype == nil then - -- warn("Already have no archetype") - -- return - -- end - - -- for _, storageIndex in archetype.componentIdToStorageIndex do - -- -- TODO: - -- -- swap remove - -- table.remove(archetype.storage[storageIndex], entityRecord.indexInArchetype) - -- end - - -- -- entityRecord.archetype = nil - -- -- entityRecord.indexInArchetype = nil - -- self.entities[id] = nil - - -- -- TODO: - -- -- This is slow (and unsafe) - -- table.remove(archetype.ownedEntities, table.find(archetype.ownedEntities, id)) - -- else - -- local componentIds: { ComponentId } = {} - -- for component in myComponents do - -- table.insert(componentIds, #component) - -- end - - -- local entityRecord = self.entities[id] - -- if entityRecord == nil then - -- entityRecord = {} - -- self.entities[id] = entityRecord - -- end - -- assert(entityRecord, "Make typechecker happy") - - -- -- Find the archetype that matches these components - -- local newArchetypeId = archetypeOf(componentIds) - -- local newArchetype = self.archetypes[newArchetypeId] or createArchetype(self, componentIds) - - -- print("archetype", newArchetype) - -- -- Add entity to archetype - -- local indexInArchetype = #newArchetype.ownedEntities + 1 - -- for component, componentInstance in myComponents do - -- newArchetype.storage[newArchetype.componentIdToStorageIndex[#component]][indexInArchetype] = componentInstance - -- end - - -- entityRecord.indexInArchetype = indexInArchetype - -- entityRecord.archetype = newArchetype - -- table.insert(newArchetype.ownedEntities, id) - -- end - - debug.profileend() -end - --[=[ Replaces a given entity by ID with an entirely new set of components. Equivalent to removing all components from an entity, and then adding these ones. From aa9d869401b15539d1ebfdeaefa1581973e66c5d Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 12 Aug 2024 20:37:58 -0400 Subject: [PATCH 58/87] fix overwriting on insert creating invalid archetype --- benchmarks/query.bench.luau | 10 +++++----- example/src/shared/start.luau | 8 ++++++++ lib/World.luau | 27 +++++++++++++++++++-------- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/benchmarks/query.bench.luau b/benchmarks/query.bench.luau index d1ae6d86..a448d4fc 100644 --- a/benchmarks/query.bench.luau +++ b/benchmarks/query.bench.luau @@ -11,7 +11,7 @@ local pinnedWorld = PinnedMatter.World.new() local A, B = Matter.component(), Matter.component() local pinnedA, pinnedB = PinnedMatter.component(), PinnedMatter.component() -for i = 1, 10_000 do +for i = 1, 100_000 do world:spawnAt(i, A({}), B({})) pinnedWorld:spawnAt(i, pinnedA({}), pinnedB({})) end @@ -23,15 +23,15 @@ return { Functions = { ["New Matter"] = function() - local count = 0 + --local count = 0 for _ in world:query(A, B) do - count += 1 + --count += 1 end end, ["Old Matter"] = function() - local count = 0 + --local count = 0 for _ in pinnedWorld:query(pinnedA, pinnedB) do - count += 1 + --count += 1 end end, }, diff --git a/example/src/shared/start.luau b/example/src/shared/start.luau index 5e89e2f2..c3b19762 100644 --- a/example/src/shared/start.luau +++ b/example/src/shared/start.luau @@ -92,6 +92,14 @@ local function start(containers) end) end + if RunService:IsServer() then + task.spawn(function() + while task.wait(1) do + print(world.archetypes) + end + end) + end + return world, state end diff --git a/lib/World.luau b/lib/World.luau index 90dc9db8..5e8b0430 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -337,18 +337,29 @@ local function executeInsert(world: World, insertCommand: InsertCommand) local component = getmetatable(componentInstance) local componentId = #component local componentIds = table.clone(oldArchetype.componentIds) - table.insert(componentIds, componentId) - local archetype = ensureArchetype(world, componentIds) - local entityIndex = transitionArchetype(world, entityId, entityRecord, archetype) - local oldComponentInstance = archetype.storage[archetype.componentIdToStorageIndex[componentId]][entityIndex] + local archetype: Archetype + local entityIndex: number + local oldComponentInstance: ComponentInstance? + if oldArchetype.componentIdToStorageIndex[componentId] == nil then + table.insert(componentIds, componentId) + archetype = ensureArchetype(world, componentIds) + entityIndex = transitionArchetype(world, entityId, entityRecord, archetype) + oldComponentInstance = archetype.storage[archetype.componentIdToStorageIndex[componentId]][entityIndex] + + -- FIXME: + -- This shouldn't be in a hotpath, probably better in createArchetype + world.componentIdToComponent[componentId] = component + else + archetype = oldArchetype + entityIndex = entityRecord.indexInArchetype + oldComponentInstance = + oldArchetype.storage[oldArchetype.componentIdToStorageIndex[componentId]][entityIndex] + end + archetype.storage[archetype.componentIdToStorageIndex[componentId]][entityIndex] = componentInstance world:_trackChanged(component, entityId, oldComponentInstance, componentInstance) - -- FIXME: - -- This shouldn't be in a hotpath, probably better in createArchetype - world.componentIdToComponent[componentId] = component - oldArchetype = archetype end From d4980d71ab269eb73dbd3e853a6defa0ac48c194 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 12 Aug 2024 21:35:43 -0400 Subject: [PATCH 59/87] map archetype ids to archetypes --- benchmarks/query.bench.luau | 10 +++--- benchmarks/stress.bench.luau | 61 ++++++++++++++++++++++++++++++++++++ lib/World.luau | 12 ++----- 3 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 benchmarks/stress.bench.luau diff --git a/benchmarks/query.bench.luau b/benchmarks/query.bench.luau index a448d4fc..d1ae6d86 100644 --- a/benchmarks/query.bench.luau +++ b/benchmarks/query.bench.luau @@ -11,7 +11,7 @@ local pinnedWorld = PinnedMatter.World.new() local A, B = Matter.component(), Matter.component() local pinnedA, pinnedB = PinnedMatter.component(), PinnedMatter.component() -for i = 1, 100_000 do +for i = 1, 10_000 do world:spawnAt(i, A({}), B({})) pinnedWorld:spawnAt(i, pinnedA({}), pinnedB({})) end @@ -23,15 +23,15 @@ return { Functions = { ["New Matter"] = function() - --local count = 0 + local count = 0 for _ in world:query(A, B) do - --count += 1 + count += 1 end end, ["Old Matter"] = function() - --local count = 0 + local count = 0 for _ in pinnedWorld:query(pinnedA, pinnedB) do - --count += 1 + count += 1 end end, }, diff --git a/benchmarks/stress.bench.luau b/benchmarks/stress.bench.luau new file mode 100644 index 00000000..df7f8c41 --- /dev/null +++ b/benchmarks/stress.bench.luau @@ -0,0 +1,61 @@ +--!optimize 2 +--!native +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Matter = require(ReplicatedStorage.Matter) +local PinnedMatter = require(ReplicatedStorage.PinnedMatter) + +local world = Matter.World.new() +local pinnedWorld = PinnedMatter.World.new() + +local A, B, C, D = Matter.component(), Matter.component(), Matter.component(), Matter.component() +local pinnedA, pinnedB, pinnedC, pinnedD = + PinnedMatter.component(), PinnedMatter.component(), PinnedMatter.component(), PinnedMatter.component() + +local function flip() + return math.random() > 0.5 +end + +for i = 1, 200_000 do + local id = i + world:spawnAt(id) + pinnedWorld:spawnAt(id) + + if flip() then + world:insert(id, A()) + pinnedWorld:insert(id, pinnedA()) + end + if flip() then + world:insert(id, B()) + pinnedWorld:insert(id, pinnedB()) + end + if flip() then + world:insert(id, C()) + pinnedWorld:insert(id, pinnedC()) + end + if flip() then + world:insert(id, D()) + pinnedWorld:insert(id, pinnedD()) + end +end + +return { + ParameterGenerator = function() + return + end, + + Functions = { + ["Old Matter"] = function() + for _ in pinnedWorld:query(pinnedA, pinnedB) do + end + end, + ["New Matter"] = function() + --local count = 0 + for _ in world:query(A, B) do + --count += 1 + end + + --print(count) + end, + }, +} diff --git a/lib/World.luau b/lib/World.luau index 5e8b0430..7308c591 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -123,6 +123,7 @@ local function createArchetype(world: World, componentIds: { ComponentId }): Arc end table.insert(world.archetypes, archetype) + world.archetypeIdToArchetype[archetypeId] = archetype return archetype end @@ -135,7 +136,7 @@ function World.new() componentIdToComponent = {}, componentToArchetypes = {} :: ComponentToArchetypes, archetypes = {} :: Archetypes, - archetypeHashToIndex = {} :: { [string]: number }, + archetypeIdToArchetype = {} :: { [string]: Archetype? }, -- Map from archetype string --> entity ID --> entity data storage = {}, @@ -229,14 +230,7 @@ end local function ensureArchetype(world: World, componentIds: { number }) local archetypeId = archetypeOf(componentIds) - local archetypes = world.archetypes - for _, archetype in archetypes do - if archetype.id == archetypeId then - return archetype - end - end - - return createArchetype(world, componentIds) + return world.archetypeIdToArchetype[archetypeId] or createArchetype(world, componentIds) end local function ensureRecord(world: World, entityId: number): EntityRecord From 0c3b507a7398be3b501e410df5181fb3b13f200d Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 13 Aug 2024 11:05:39 -0400 Subject: [PATCH 60/87] do not transition archetype on removal if same --- lib/World.luau | 19 +++++++++++++------ lib/World.spec.luau | 20 ++++++++++++++++---- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 7308c591..4c73c361 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -315,6 +315,7 @@ local function executeDespawn(world: World, despawnCommand: DespawnCommand) -- Optimize remove so no cascades transitionArchetype(world, entityId, entityRecord, world.rootArchetype) table.remove(world.rootArchetype.ownedEntities, entityRecord.indexInArchetype) + world.entities[entityId] = nil world._size -= 1 end @@ -406,18 +407,24 @@ local function executeRemove(world: World, removeCommand: RemoveCommand) local entityRecord = ensureRecord(world, entityId) local archetype = entityRecord.archetype local componentIds = table.clone(entityRecord.archetype.componentIds) - for _, component in removeCommand.components do - local componentInstance = - archetype.storage[archetype.componentIdToStorageIndex[#component]][entityRecord.indexInArchetype] - world:_trackChanged(component, entityId, componentInstance, nil) - local index = table.find(componentIds, #component) + local didRemove = false + for _, component in removeCommand.components do + local componentId = #component + local index = table.find(componentIds, componentId) if index then + local componentInstance = + archetype.storage[archetype.componentIdToStorageIndex[componentId]][entityRecord.indexInArchetype] + world:_trackChanged(component, entityId, componentInstance, nil) + table.remove(componentIds, index) + didRemove = true end end - transitionArchetype(world, entityId, entityRecord, ensureArchetype(world, componentIds)) + if didRemove then + transitionArchetype(world, entityId, entityRecord, ensureArchetype(world, componentIds)) + end end local function processCommand(world: World, command: Command) diff --git a/lib/World.spec.luau b/lib/World.spec.luau index d965a2f5..0c1d48f0 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -142,14 +142,18 @@ return function() end) end) - describe("immediate", function() + describeFOCUS("immediate", function() it("should work", function() local world = World.new() local A, B = component(), component() world:spawnAt(10) world:insert(10, A({ a = true }), B({ b = true })) - world:spawnAt(50, A({ a = true, two = true }), B({ b = true, two = true })) + world:remove(10, A, B) + world:insert(10, A({ a = true, second = true })) + world:insert(10, B({ b = true, second = true })) + + --world:spawnAt(50, A({ a = true, two = true }), B({ b = true, two = true })) -- world:spawnAt(11) -- world:insert(11, A({ a = true, two = true }), B({ b = true, two = true })) print(world.archetypes) @@ -160,8 +164,8 @@ return function() -- world:insert(10, B({ b = true })) --world:spawnAt(10, A({ a = true }), B({ b = true })) --world:remove(10, B) - for id, a, b in world:query(A, B) do - print(id, a, b) + for id, a in world:query(A) do + print(id, a) end --print(world) end) @@ -264,6 +268,14 @@ return function() expect(world:size()).to.equal(1) end) + it("should allow removing missing components", function() + local world = World.new() + local A = component() + + local entityId = world:spawn() + expect(world:remove(entityId, A)).to.equal(nil) + end) + it("should not find any entities", function() local world = World.new() From 1e87a7a69be3d8ff432294f6e71c8f41918a2960 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 13 Aug 2024 11:12:04 -0400 Subject: [PATCH 61/87] allow query length up to 6 --- lib/World.luau | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 4c73c361..4744677c 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -1026,8 +1026,8 @@ function World.query(self: World, ...) -- TODO: -- cache queries local components = { ... } - local A, B, C, D = ... - local a, b, c, d = nil, nil, nil, nil + local A, B, C, D, E, F = ... + local a, b, c, d, e, f = nil, nil, nil, nil, nil, nil local queryLength = select("#", ...) if queryLength == 1 then @@ -1038,6 +1038,10 @@ function World.query(self: World, ...) components = { #A, #B, #C } elseif queryLength == 4 then components = { #A, #B, #C, #D } + elseif queryLength == 5 then + components = { #A, #B, #C, #D, #E } + elseif queryLength == 6 then + components = { #A, #B, #C, #D, #E, #F } else error("Unimplemented query length " .. queryLength) end @@ -1103,6 +1107,19 @@ function World.query(self: World, ...) b = storage[componentIdToStorageIndex[components[2]]] c = storage[componentIdToStorageIndex[components[3]]] d = storage[componentIdToStorageIndex[components[4]]] + elseif queryLength == 5 then + a = storage[componentIdToStorageIndex[components[1]]] + b = storage[componentIdToStorageIndex[components[2]]] + c = storage[componentIdToStorageIndex[components[3]]] + d = storage[componentIdToStorageIndex[components[4]]] + e = storage[componentIdToStorageIndex[components[5]]] + elseif queryLength == 6 then + a = storage[componentIdToStorageIndex[components[1]]] + b = storage[componentIdToStorageIndex[components[2]]] + c = storage[componentIdToStorageIndex[components[3]]] + d = storage[componentIdToStorageIndex[components[4]]] + e = storage[componentIdToStorageIndex[components[5]]] + f = storage[componentIdToStorageIndex[components[6]]] end end @@ -1133,6 +1150,16 @@ function World.query(self: World, ...) return entityId, a[entityIndex], b[entityIndex], c[entityIndex] elseif queryLength == 4 then return entityId, a[entityIndex], b[entityIndex], c[entityIndex], d[entityIndex] + elseif queryLength == 5 then + return entityId, a[entityIndex], b[entityIndex], c[entityIndex], d[entityIndex], e[entityIndex] + elseif queryLength == 6 then + return entityId, + a[entityIndex], + b[entityIndex], + c[entityIndex], + d[entityIndex], + e[entityIndex], + f[entityIndex] else error("Unimplemented Query Length") end From 1893c77a21de6ef822702cb19f2d4468ffd927e2 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 13 Aug 2024 15:00:06 -0400 Subject: [PATCH 62/87] add snapshots and fix replace --- lib/World.luau | 341 +++++++++++++++++++++----------------------- lib/World.spec.luau | 25 ++-- 2 files changed, 181 insertions(+), 185 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 4744677c..19be4292 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -369,6 +369,7 @@ local function executeReplace(world: World, replaceCommand: ReplaceCommand) local entityRecord = ensureRecord(world, entityId) local oldArchetype = entityRecord.archetype + local componentIds = {} local componentIdMap = {} @@ -381,7 +382,7 @@ local function executeReplace(world: World, replaceCommand: ReplaceCommand) local storageIndex = oldArchetype.componentIdToStorageIndex[componentId] world:_trackChanged( component, - id, + entityId, if storageIndex then oldArchetype.storage[storageIndex][entityRecord.indexInArchetype] else nil, componentInstance ) @@ -394,12 +395,16 @@ local function executeReplace(world: World, replaceCommand: ReplaceCommand) local componentId = oldArchetype.storageIndexToComponentId[index] if componentIdMap[componentId] == nil then local component = world.componentIdToComponent[componentId] - world:_trackChanged(component, entityId, componentStorage[oldArchetypeIndex], nil) + world:_trackChanged(component, entityId, componentStorage[entityRecord.indexInArchetype], nil) end end transitionArchetype(world, entityId, entityRecord, world.rootArchetype) transitionArchetype(world, entityId, entityRecord, ensureArchetype(world, componentIds)) + executeInsert( + world, + { type = "insert", componentInstances = replaceCommand.componentInstances, entityId = entityId } + ) end local function executeRemove(world: World, removeCommand: RemoveCommand) @@ -694,58 +699,166 @@ local noopQuery = setmetatable({ local QueryResult = {} QueryResult.__index = QueryResult -function QueryResult.new(world, expand, queryArchetype, compatibleArchetypes, metatables) - return setmetatable({ - world = world, - currentCompatibleArchetype = next(compatibleArchetypes), - compatibleArchetypes = compatibleArchetypes, - storageIndex = 1, - metatables = metatables, - _expand = expand, - _queryArchetype = queryArchetype, - }, QueryResult) -end +function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds: { number }) + local a, b, c, d, e, f = nil, nil, nil, nil, nil, nil + local currentEntityIndex = 1 + local currentArchetypeIndex = 1 + local currentArchetype = compatibleArchetypes[1] + + local function cacheComponentStorages() + if currentArchetype == nil then + return + end + + local storage, componentIdToStorageIndex = currentArchetype.storage, currentArchetype.componentIdToStorageIndex + if queryLength == 1 then + a = storage[componentIdToStorageIndex[componentIds[1]]] + elseif queryLength == 2 then + a = storage[componentIdToStorageIndex[componentIds[1]]] + b = storage[componentIdToStorageIndex[componentIds[2]]] + elseif queryLength == 3 then + a = storage[componentIdToStorageIndex[componentIds[1]]] + b = storage[componentIdToStorageIndex[componentIds[2]]] + c = storage[componentIdToStorageIndex[componentIds[3]]] + elseif queryLength == 4 then + a = storage[componentIdToStorageIndex[componentIds[1]]] + b = storage[componentIdToStorageIndex[componentIds[2]]] + c = storage[componentIdToStorageIndex[componentIds[3]]] + d = storage[componentIdToStorageIndex[componentIds[4]]] + elseif queryLength == 5 then + a = storage[componentIdToStorageIndex[componentIds[1]]] + b = storage[componentIdToStorageIndex[componentIds[2]]] + c = storage[componentIdToStorageIndex[componentIds[3]]] + d = storage[componentIdToStorageIndex[componentIds[4]]] + e = storage[componentIdToStorageIndex[componentIds[5]]] + elseif queryLength == 6 then + a = storage[componentIdToStorageIndex[componentIds[1]]] + b = storage[componentIdToStorageIndex[componentIds[2]]] + c = storage[componentIdToStorageIndex[componentIds[3]]] + d = storage[componentIdToStorageIndex[componentIds[4]]] + e = storage[componentIdToStorageIndex[componentIds[5]]] + f = storage[componentIdToStorageIndex[componentIds[6]]] + end + end + + local entityId: number + local function nextEntity(): any + entityId = currentArchetype.ownedEntities[currentEntityIndex] + while entityId == nil do + currentEntityIndex = 1 + currentArchetypeIndex += 1 + currentArchetype = compatibleArchetypes[currentArchetypeIndex] + if currentArchetype == nil then + return nil + end -local function nextItem(query) - local world = query.world - local currentCompatibleArchetype = query.currentCompatibleArchetype - local compatibleArchetypes = query.compatibleArchetypes + cacheComponentStorages() + entityId = currentArchetype.ownedEntities[currentEntityIndex] + end - local entityId, entityData + local entityIndex = currentEntityIndex + currentEntityIndex += 1 + + local entityId = currentArchetype.ownedEntities[entityIndex] + if queryLength == 1 then + return entityId, a[entityIndex] + elseif queryLength == 2 then + return entityId, a[entityIndex], b[entityIndex] + elseif queryLength == 3 then + return entityId, a[entityIndex], b[entityIndex], c[entityIndex] + elseif queryLength == 4 then + return entityId, a[entityIndex], b[entityIndex], c[entityIndex], d[entityIndex] + elseif queryLength == 5 then + return entityId, a[entityIndex], b[entityIndex], c[entityIndex], d[entityIndex], e[entityIndex] + elseif queryLength == 6 then + return entityId, + a[entityIndex], + b[entityIndex], + c[entityIndex], + d[entityIndex], + e[entityIndex], + f[entityIndex] + else + error("Unimplemented Query Length") + end + end - local storage = world.storage - local currently = storage[currentCompatibleArchetype] - if currently then - entityId, entityData = next(currently, query.lastEntityId) + local function iter() + return nextEntity end - while entityId == nil do - currentCompatibleArchetype = next(compatibleArchetypes, currentCompatibleArchetype) + local function without(query, ...: Component) + local numComponents = select("#", ...) + local numCompatibleArchetypes = #compatibleArchetypes + for archetypeIndex = numCompatibleArchetypes, 1, -1 do + local archetype = compatibleArchetypes[archetypeIndex] + local shouldRemove = false + for componentIndex = 1, numComponents do + local component = select(componentIndex, ...) + if archetype.componentIdToStorageIndex[#component] then + shouldRemove = true + break + end + end - if currentCompatibleArchetype == nil then - return nil - elseif storage[currentCompatibleArchetype] == nil then - continue + if shouldRemove then + if archetypeIndex ~= numCompatibleArchetypes then + compatibleArchetypes[archetypeIndex] = compatibleArchetypes[numCompatibleArchetypes] + end + + compatibleArchetypes[numCompatibleArchetypes] = nil + numCompatibleArchetypes -= 1 + end end - entityId, entityData = next(storage[currentCompatibleArchetype]) + if numCompatibleArchetypes == 0 then + return noopQuery + end + + currentArchetype = compatibleArchetypes[1] + cacheComponentStorages() + return query end - query.lastEntityId = entityId + local Snapshot = { + __iter = function(self) + local i = 0 + return function() + i += 1 - query.currentCompatibleArchetype = currentCompatibleArchetype + local entry = self[i] :: any + if entry then + return unpack(entry, 1, entry.n) + end - return entityId, entityData -end + return + end + end, + } -function QueryResult:__iter() - return function() - return self._expand(nextItem(self)) + local function snapshot() + local entities: { any } = setmetatable({}, Snapshot) :: any + while true do + local entry = table.pack(nextEntity()) + if entry.n == 1 then + break + end + + table.insert(entities, entry) + end + + return entities end -end -function QueryResult:__call() - return self._expand(nextItem(self)) + cacheComponentStorages() + return setmetatable({ + next = nextEntity, + without = without, + snapshot = snapshot, + }, { + __iter = iter, + __call = nextEntity, + }) end --[=[ @@ -1023,32 +1136,32 @@ end ]=] function World.query(self: World, ...) - -- TODO: - -- cache queries - local components = { ... } local A, B, C, D, E, F = ... - local a, b, c, d, e, f = nil, nil, nil, nil, nil, nil + local componentIds: { number } local queryLength = select("#", ...) if queryLength == 1 then - components = { #A } + componentIds = { #A } elseif queryLength == 2 then - components = { #A, #B } + componentIds = { #A, #B } elseif queryLength == 3 then - components = { #A, #B, #C } + componentIds = { #A, #B, #C } elseif queryLength == 4 then - components = { #A, #B, #C, #D } + componentIds = { #A, #B, #C, #D } elseif queryLength == 5 then - components = { #A, #B, #C, #D, #E } + componentIds = { #A, #B, #C, #D, #E } elseif queryLength == 6 then - components = { #A, #B, #C, #D, #E, #F } + componentIds = { #A, #B, #C, #D, #E, #F } else - error("Unimplemented query length " .. queryLength) + componentIds = table.create(queryLength) + for i = 1, queryLength do + componentIds[i] = select(i, ...) + end end local possibleArchetypes local compatibleArchetypes = {} - for _, componentId in components do + for _, componentId in componentIds do local associatedArchetypes = self.componentToArchetypes[componentId] if associatedArchetypes == nil then return noopQuery @@ -1063,7 +1176,7 @@ function World.query(self: World, ...) for archetypeIndex in possibleArchetypes do local archetype = self.archetypes[archetypeIndex] local incompatible = false - for _, componentId in components do + for _, componentId in componentIds do -- Does this archetype have this component? if archetype.componentIdToStorageIndex[componentId] == nil then -- Nope, so we can't use this one. @@ -1083,129 +1196,7 @@ function World.query(self: World, ...) return noopQuery end - local currentEntityIndex = 1 - local currentArchetypeIndex = 1 - local currentArchetype = compatibleArchetypes[1] - - local function cacheComponentStorages() - if currentArchetype == nil then - return - end - - local storage, componentIdToStorageIndex = currentArchetype.storage, currentArchetype.componentIdToStorageIndex - if queryLength == 1 then - a = storage[componentIdToStorageIndex[components[1]]] - elseif queryLength == 2 then - a = storage[componentIdToStorageIndex[components[1]]] - b = storage[componentIdToStorageIndex[components[2]]] - elseif queryLength == 3 then - a = storage[componentIdToStorageIndex[components[1]]] - b = storage[componentIdToStorageIndex[components[2]]] - c = storage[componentIdToStorageIndex[components[3]]] - elseif queryLength == 4 then - a = storage[componentIdToStorageIndex[components[1]]] - b = storage[componentIdToStorageIndex[components[2]]] - c = storage[componentIdToStorageIndex[components[3]]] - d = storage[componentIdToStorageIndex[components[4]]] - elseif queryLength == 5 then - a = storage[componentIdToStorageIndex[components[1]]] - b = storage[componentIdToStorageIndex[components[2]]] - c = storage[componentIdToStorageIndex[components[3]]] - d = storage[componentIdToStorageIndex[components[4]]] - e = storage[componentIdToStorageIndex[components[5]]] - elseif queryLength == 6 then - a = storage[componentIdToStorageIndex[components[1]]] - b = storage[componentIdToStorageIndex[components[2]]] - c = storage[componentIdToStorageIndex[components[3]]] - d = storage[componentIdToStorageIndex[components[4]]] - e = storage[componentIdToStorageIndex[components[5]]] - f = storage[componentIdToStorageIndex[components[6]]] - end - end - - local entityId: number - local function nextEntity(): any - entityId = currentArchetype.ownedEntities[currentEntityIndex] - while entityId == nil do - currentEntityIndex = 1 - currentArchetypeIndex += 1 - currentArchetype = compatibleArchetypes[currentArchetypeIndex] - if currentArchetype == nil then - return nil - end - - cacheComponentStorages() - entityId = currentArchetype.ownedEntities[currentEntityIndex] - end - - local entityIndex = currentEntityIndex - currentEntityIndex += 1 - - local entityId = currentArchetype.ownedEntities[entityIndex] - if queryLength == 1 then - return entityId, a[entityIndex] - elseif queryLength == 2 then - return entityId, a[entityIndex], b[entityIndex] - elseif queryLength == 3 then - return entityId, a[entityIndex], b[entityIndex], c[entityIndex] - elseif queryLength == 4 then - return entityId, a[entityIndex], b[entityIndex], c[entityIndex], d[entityIndex] - elseif queryLength == 5 then - return entityId, a[entityIndex], b[entityIndex], c[entityIndex], d[entityIndex], e[entityIndex] - elseif queryLength == 6 then - return entityId, - a[entityIndex], - b[entityIndex], - c[entityIndex], - d[entityIndex], - e[entityIndex], - f[entityIndex] - else - error("Unimplemented Query Length") - end - end - - local function iter() - return nextEntity - end - - local function without(query, ...: Component) - local numComponents = select("#", ...) - local numCompatibleArchetypes = #compatibleArchetypes - for archetypeIndex = numCompatibleArchetypes, 1, -1 do - local archetype = compatibleArchetypes[archetypeIndex] - local shouldRemove = false - for componentIndex = 1, numComponents do - local component = select(componentIndex, ...) - if archetype.componentIdToStorageIndex[#component] then - shouldRemove = true - break - end - end - - if shouldRemove then - if archetypeIndex ~= numCompatibleArchetypes then - compatibleArchetypes[archetypeIndex] = compatibleArchetypes[numCompatibleArchetypes] - end - - compatibleArchetypes[numCompatibleArchetypes] = nil - numCompatibleArchetypes -= 1 - end - end - - if numCompatibleArchetypes == 0 then - return noopQuery - end - - currentArchetype = compatibleArchetypes[1] - cacheComponentStorages() - return query - end - - cacheComponentStorages() - return setmetatable({ next = nextEntity, without = without }, { - __iter = iter, - }) + return QueryResult.new(compatibleArchetypes, queryLength, componentIds) :: any end local function cleanupQueryChanged(hookState) diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 0c1d48f0..6165fe8a 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -147,16 +147,20 @@ return function() local world = World.new() local A, B = component(), component() - world:spawnAt(10) - world:insert(10, A({ a = true }), B({ b = true })) - world:remove(10, A, B) - world:insert(10, A({ a = true, second = true })) - world:insert(10, B({ b = true, second = true })) + local id = world:spawn(A({ a = true })) + world:replace(id, B({ b = true })) + + print(world.archetypes) + -- world:spawnAt(10) + -- world:insert(10, A({ a = true }), B({ b = true })) + -- world:remove(10, A, B) + -- world:insert(10, A({ a = true, second = true })) + -- world:insert(10, B({ b = true, second = true })) --world:spawnAt(50, A({ a = true, two = true }), B({ b = true, two = true })) -- world:spawnAt(11) -- world:insert(11, A({ a = true, two = true }), B({ b = true, two = true })) - print(world.archetypes) + --print(world.archetypes) -- world:spawnAt(10, A({ a = true })) -- world:spawnAt(11, A({ a = true, two = true })) -- world:spawnAt(12, A({ a = true, three = true })) @@ -164,9 +168,9 @@ return function() -- world:insert(10, B({ b = true })) --world:spawnAt(10, A({ a = true }), B({ b = true })) --world:remove(10, B) - for id, a in world:query(A) do - print(id, a) - end + -- for id, a in world:query(A) do + -- print(id, a) + -- end --print(world) end) @@ -495,7 +499,7 @@ return function() expect(withoutCount).to.equal(0) end) - itFOCUS("should track changes", function() + it("should track changes", function() local world = World.new() local loop = Loop.new(world) @@ -559,6 +563,7 @@ return function() local ran = false for entityId, record in w:queryChanged(A) do + print(entityId, record, tostring(record.new)) if additionalQuery then if w:get(entityId, additionalQuery) == nil then continue From 778fd19496540bb728063f192ba2f260e6663271 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 15 Aug 2024 20:10:33 -0400 Subject: [PATCH 63/87] remove dead code --- lib/World.luau | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 19be4292..5694c7bc 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -1,7 +1,6 @@ --!strict local Component = require(script.Parent.component) local archetypeModule = require(script.Parent.archetype) -local component = require(script.Parent.component) local topoRuntime = require(script.Parent.topoRuntime) local assertValidComponentInstances = Component.assertValidComponentInstances @@ -16,14 +15,9 @@ type ComponentId = number type Component = { [any]: any } type ComponentInstance = { [any]: any } -type ComponentMetatable = { [any]: any } -type ComponentMetatables = { ComponentMetatable } -type ComponentToId = { [Component]: number } type ArchetypeId = string type Archetype = { - id: string, - ownedEntities: { EntityId }, --- The component IDs that are part of this archetype, in no particular order @@ -97,18 +91,13 @@ local function createArchetype(world: World, componentIds: { ComponentId }): Arc local indexInArchetypes = #world.archetypes + 1 local archetype: Archetype = { - id = archetypeId, - indexInArchetypes = indexInArchetypes, + ownedEntities = {}, componentIds = componentIds, componentIdToStorageIndex = componentIdToStorageIndex, storageIndexToComponentId = storageIndexToComponentId, - storage = storage, - - -- Keep track of all entity ids for fast iteration in queries - ownedEntities = {}, } for index, componentId in componentIds do @@ -1391,7 +1380,7 @@ end @param id number -- The entity ID @param ... Component -- The components to remove ]=] -function World.remove(self: World, id, ...: ComponentMetatable) +function World.remove(self: World, id, ...: Component) local entityRecord = self.entities[id] if entityRecord == nil then error(ERROR_NO_ENTITY, 2) From 2c0f9453d4d680f09e6695179c5c2a90dacee8f1 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 27 Aug 2024 21:14:21 -0400 Subject: [PATCH 64/87] Revert "Merge branch 'main' into use-real-archetypes" This reverts commit 343de5e0cd3aaa0a1fec902b5415f1520fd85fb8, reversing changes made to 778fd19496540bb728063f192ba2f260e6663271. --- CHANGELOG.md | 30 +------ README.md | 2 +- docs/Guides/Migration.md | 2 +- docs/Installation.md | 2 +- lib/Loop.luau | 18 +---- lib/component.luau | 7 -- lib/debugger/clientBindings.luau | 39 +++------ lib/debugger/debugger.luau | 2 - lib/debugger/formatTable.luau | 2 +- lib/debugger/hookWorld.luau | 36 +-------- lib/debugger/ui.luau | 97 ++++++----------------- lib/debugger/widgets/hoverInspect.luau | 5 +- lib/debugger/widgets/queryInspect.luau | 100 ++++++++++++++---------- lib/debugger/widgets/selectionList.luau | 78 ++++-------------- lib/debugger/widgets/tooltip.luau | 13 +-- lib/debugger/widgets/worldInspect.luau | 56 ++++--------- wally.toml | 2 +- 17 files changed, 132 insertions(+), 359 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a1f5f04..4679f8ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,33 +22,6 @@ The format is based on [Keep a Changelog][kac], and this project adheres to - Deprecated the return type of `World:remove()` because it can now be inaccurate. - Deprecated `World:optimizeQueries()` because it no longer does anything. -## [0.8.4] - 2024-08-15 - -### Added - -- Better assertions / error messages added to `World` methods that accept - variadic component arguments. At least 1 component must be provided. These - assertions have been added to `get` `insert` `replace` `remove` -- Ability to sort the world inspect table by clicking the table headers (entity - count and component name) -- Ability to disable systems in the debugger list by right clicking them. - -### Changed - -- The alt-hover tooltip's text is smaller and the background is slightly darker - for improved legibility. -- Component data now has syntax highlighting applied. This is present in the - **alt-hover tooltip** and the **entity inspector panel** in the debugger. - -### Fixed - -- The alt-hover tooltip now displays component data properly, with each - component being displayed on a new line. -- Removed extra new-lines in component data strings within the debugger entity - inspect tables. -- Fixed alt-hover erroring when hovered entity is despawned. -- Fixed flashing buttons ("View queries" and "View logs") in system inspect panel - ## [0.8.3] - 2024-07-02 ### Fixed @@ -328,8 +301,7 @@ The format is based on [Keep a Changelog][kac], and this project adheres to - Initial release -[unreleased]: https://github.com/matter-ecs/matter/compare/v0.8.4...HEAD -[0.8.4]: https://github.com/matter-ecs/matter/releases/tag/v0.8.4 +[unreleased]: https://github.com/matter-ecs/matter/compare/v0.8.3...HEAD [0.8.3]: https://github.com/matter-ecs/matter/releases/tag/v0.8.3 [0.8.2]: https://github.com/matter-ecs/matter/releases/tag/v0.8.2 [0.8.1]: https://github.com/matter-ecs/matter/releases/tag/v0.8.1 diff --git a/README.md b/README.md index a4332b2b..b79836b3 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Matter can be installed with [Wally] by including it as a dependency in your `wally.toml` file. ```toml -Matter = "matter-ecs/matter@0.8.4" +Matter = "matter-ecs/matter@0.8.3" ``` ## Migration diff --git a/docs/Guides/Migration.md b/docs/Guides/Migration.md index f8205d8a..2044fb04 100644 --- a/docs/Guides/Migration.md +++ b/docs/Guides/Migration.md @@ -4,5 +4,5 @@ Migrating from `evaera/matter` to `matter-ecs/matter` is easy! The only thing yo ```toml title="wally.toml" [dependencies] - matter = "matter-ecs/matter@0.8.4" + matter = "matter-ecs/matter@0.8.3" ``` diff --git a/docs/Installation.md b/docs/Installation.md index 02eb87c1..3f4c83a4 100644 --- a/docs/Installation.md +++ b/docs/Installation.md @@ -25,7 +25,7 @@ wally = "UpliftGames/wally@x.x.x" ```toml title="wally.toml" [dependencies] -matter = "matter-ecs/matter@0.8.4" +matter = "matter-ecs/matter@0.8.3" ``` 6. Run `wally install`. diff --git a/lib/Loop.luau b/lib/Loop.luau index c7b2a21a..c1ccd5e0 100644 --- a/lib/Loop.luau +++ b/lib/Loop.luau @@ -59,7 +59,6 @@ function Loop.new(...) _middlewares = {}, _systemErrors = {}, _systemLogs = {}, - _debugger = nil, profiling = nil, trackErrors = false, }, Loop) @@ -425,22 +424,7 @@ function Loop:begin(events) profiling[system] = {} end - local debugger = self._debugger - - if debugger and debugger.debugSystem == system and debugger._queries then - local totalQueryTime = 0 - - for _, query in debugger._queries do - totalQueryTime += query.averageDuration - end - - rollingAverage.addSample( - profiling[system], - if debugger.debugSystem then duration - totalQueryTime else duration - ) - else - rollingAverage.addSample(profiling[system], duration) - end + rollingAverage.addSample(profiling[system], duration) end if coroutine.status(thread) ~= "dead" then diff --git a/lib/component.luau b/lib/component.luau index df80820b..874aa80b 100644 --- a/lib/component.luau +++ b/lib/component.luau @@ -171,16 +171,9 @@ local function assertValidComponentInstances(componentInstances) end end -local function assertComponentArgsProvided(...) - if not (...) then - error(`No components passed to world:{debug.info(3, "n")}, at least one component is required`, 2) - end -end - return { newComponent = newComponent, assertValidComponentInstance = assertValidComponentInstance, assertValidComponentInstances = assertValidComponentInstances, assertValidComponent = assertValidComponent, - assertComponentArgsProvided = assertComponentArgsProvided, } diff --git a/lib/debugger/clientBindings.luau b/lib/debugger/clientBindings.luau index fdac1a05..405aed41 100644 --- a/lib/debugger/clientBindings.luau +++ b/lib/debugger/clientBindings.luau @@ -1,21 +1,5 @@ local UserInputService = game:GetService("UserInputService") local CollectionService = game:GetService("CollectionService") - -local tags = { - system = "MatterDebuggerTooltip_System", - altHover = "MatterDebuggerTooltip_AltHover", -} - -local function getOffset(mousePos: Vector2, tag: string): UDim2 - if tag == tags.altHover then - return UDim2.fromOffset(mousePos.X + 20, mousePos.Y) - elseif tag == tags.system then - return UDim2.fromOffset(mousePos.X + 20, mousePos.Y + 10) - end - - return UDim2.fromOffset(mousePos.X, mousePos.Y + 10) -end - local function clientBindings(debugger) local connections = {} @@ -37,23 +21,20 @@ local function clientBindings(debugger) local mousePosition = UserInputService:GetMouseLocation() - for _, tag in tags do - for _, gui in CollectionService:GetTagged(tag) do - gui.Position = getOffset(mousePosition, tag) - end + for _, gui in CollectionService:GetTagged("MatterDebuggerTooltip") do + gui.Position = UDim2.new(0, mousePosition.X + 20, 0, mousePosition.Y) end end) ) - for _, tag in tags do - table.insert( - connections, - CollectionService:GetInstanceAddedSignal(tag):Connect(function(gui) - local mousePosition = UserInputService:GetMouseLocation() - gui.Position = getOffset(mousePosition, tag) - end) - ) - end + table.insert( + connections, + CollectionService:GetInstanceAddedSignal("MatterDebuggerTooltip"):Connect(function(gui) + local mousePosition = UserInputService:GetMouseLocation() + + gui.Position = UDim2.new(0, mousePosition.X + 20, 0, mousePosition.Y) + end) + ) return connections end diff --git a/lib/debugger/debugger.luau b/lib/debugger/debugger.luau index bf5b410a..4bfa0e95 100644 --- a/lib/debugger/debugger.luau +++ b/lib/debugger/debugger.luau @@ -168,7 +168,6 @@ function Debugger.new(plasma) componentRefreshFrequency = 3, _windowCount = 0, _queries = {}, - _queryDurationSamples = {}, _seenEvents = {}, _eventOrder = {}, _eventBridge = EventBridge.new(function(...) @@ -345,7 +344,6 @@ end ]=] function Debugger:autoInitialize(loop) self.loop = loop - loop._debugger = self self.loop.trackErrors = true diff --git a/lib/debugger/formatTable.luau b/lib/debugger/formatTable.luau index 01dacd2e..57f9d9d0 100644 --- a/lib/debugger/formatTable.luau +++ b/lib/debugger/formatTable.luau @@ -7,7 +7,7 @@ function colorize(value, color) value = string.format("%.1f", value) end - return `{value}` + return `{value}` end function colorizeMany(color, ...) diff --git a/lib/debugger/hookWorld.luau b/lib/debugger/hookWorld.luau index e69bb567..1765ca58 100644 --- a/lib/debugger/hookWorld.luau +++ b/lib/debugger/hookWorld.luau @@ -1,46 +1,14 @@ ---#selene:allow(empty_loop) local useCurrentSystem = require(script.Parent.Parent.topoRuntime).useCurrentSystem local World = require(script.Parent.Parent.World) -local rollingAverage = require(script.Parent.Parent.rollingAverage) local originalQuery = World.query local function hookWorld(debugger) World.query = function(world, ...) if useCurrentSystem() == debugger.debugSystem then - local start = os.clock() - - -- while this seems like a mistake, it is necessary! - -- we duplicate the query to avoid draining the original one. - -- we iterate through so we can calculate the query's budget - -- see https://github.com/matter-ecs/matter/issues/106 - -- and https://github.com/matter-ecs/matter/pull/107 - for _ in originalQuery(world, ...) do - end - - local file, line = debug.info(2, "sl") - - local key = file .. line - local samples = debugger._queryDurationSamples - local sample = samples[key] - if not sample then - sample = {} - samples[key] = sample - end - - local componentNames = {} - for i = 1, select("#", ...) do - table.insert(componentNames, tostring((select(i, ...)))) - end - - local duration = os.clock() - start - rollingAverage.addSample(sample, duration) - - local averageDuration = rollingAverage.getAverage(debugger._queryDurationSamples[file .. line]) - table.insert(debugger._queries, { - averageDuration = averageDuration, - componentNames = componentNames, + components = { ... }, + result = originalQuery(world, ...), }) end diff --git a/lib/debugger/ui.luau b/lib/debugger/ui.luau index aa671a1d..e1127a7a 100644 --- a/lib/debugger/ui.luau +++ b/lib/debugger/ui.luau @@ -29,12 +29,10 @@ end local IS_SERVER = RunService:IsServer() local IS_CLIENT = RunService:IsClient() -local LONG_SYSTEM_NAME = 24 local function ui(debugger, loop) local plasma = debugger.plasma local custom = debugger._customWidgets - local skipSystems = loop._skipSystems plasma.setStyle({ primaryColor = Color3.fromHex("bd515c"), @@ -42,16 +40,11 @@ local function ui(debugger, loop) local objectStack = plasma.useState({}) local worldViewOpen, setWorldViewOpen = plasma.useState(false) - local worldExists = debugger.debugWorld ~= nil - if debugger.hoverEntity and worldExists then - if debugger.debugWorld:contains(debugger.hoverEntity) then - custom.hoverInspect(debugger.debugWorld, debugger.hoverEntity, custom) - end + if debugger.hoverEntity then + custom.hoverInspect(debugger.debugWorld, debugger.hoverEntity, custom) end - local hoveredSystem - custom.container(function() if debugger:_isServerView() then return @@ -145,12 +138,13 @@ local function ui(debugger, loop) }) plasma.space(5) - local listOfSystems = {} + local items = {} for _, system in systems do local samples = loop.profiling[system] if samples then local duration = rollingAverage.getAverage(samples) + durations[system] = duration longestDuration = math.max(longestDuration, duration) end @@ -172,57 +166,27 @@ local function ui(debugger, loop) icon = "\xf0\x9f\x92\xa5" end - local barWidth - if longestDuration == 0 then - barWidth = 0 - else - barWidth = duration / longestDuration - end - - local systemIsDisabled = skipSystems[system] - local systemName = systemName(system) - local length = string.len(systemName) - - if systemIsDisabled then - length += 4 - end - - table.insert(listOfSystems, { - text = if systemIsDisabled then `{systemName}` else systemName, - sideText = if systemIsDisabled then `{"(disabled)"}` else averageFrameTime, + table.insert(items, { + text = systemName(system), + sideText = averageFrameTime, selected = debugger.debugSystem == system, system = system, icon = icon, - barWidth = barWidth, + barWidth = duration / longestDuration, index = index, }) end - local systemList = custom.selectionList(listOfSystems, custom) - local selected = systemList:selected() - local rightClicked = systemList:rightClicked() - hoveredSystem = systemList:hovered() + local selected = custom.selectionList(items):selected() if selected then - local selectedSystem = selected.system - if selectedSystem == debugger.debugSystem then + if selected.system == debugger.debugSystem then debugger.debugSystem = nil else - debugger.debugSystem = selectedSystem - end - elseif rightClicked then - local rightClickedSystem = rightClicked.system - if rightClickedSystem then - skipSystems[rightClickedSystem] = not skipSystems[rightClickedSystem] + debugger.debugSystem = selected.system end end - if debugger.debugSystem then - debugger.debugSystemRuntime = durations[debugger.debugSystem] - else - debugger.debugSystemRuntime = nil - end - plasma.space(10) end end) @@ -263,26 +227,28 @@ local function ui(debugger, loop) }, function() plasma.useKey(name) - if plasma.button(string.format("View queries (%d)", #debugger._queries)):clicked() then - setQueriesOpen(true) - end + plasma.row(function() + if plasma.button(string.format("View queries (%d)", #debugger._queries)):clicked() then + setQueriesOpen(true) + end - if numLogs > 0 then - if plasma.button(string.format("View logs (%d)", numLogs)):clicked() then - setLogsOpen(true) + if numLogs > 0 then + if plasma.button(string.format("View logs (%d)", numLogs)):clicked() then + setLogsOpen(true) + end end - end + end) - local currentlyDisabled = skipSystems[debugger.debugSystem] + local currentlyDisabled = loop._skipSystems[debugger.debugSystem] if plasma - .checkbox("Disable System", { + .checkbox("Disable system", { checked = currentlyDisabled, }) :clicked() then - skipSystems[debugger.debugSystem] = not currentlyDisabled + loop._skipSystems[debugger.debugSystem] = not currentlyDisabled end end) :closed() @@ -338,23 +304,6 @@ local function ui(debugger, loop) direction = Enum.FillDirection.Horizontal, padding = 0, }) - - if hoveredSystem and hoveredSystem.system then - local hoveredSystemName = systemName(hoveredSystem.system) - local length = string.len(hoveredSystemName) - local systemDisabled = skipSystems[hoveredSystem.system] - - if systemDisabled then - length += 2 - end - - if length >= LONG_SYSTEM_NAME then - custom.tooltip(`{hoveredSystemName}{if systemDisabled then " (disabled)" else ""}`, { - tag = "MatterDebuggerTooltip_System", - backgroundTransparency = 0, - }) - end - end end return ui diff --git a/lib/debugger/widgets/hoverInspect.luau b/lib/debugger/widgets/hoverInspect.luau index c0c10d88..d9b5daa3 100644 --- a/lib/debugger/widgets/hoverInspect.luau +++ b/lib/debugger/widgets/hoverInspect.luau @@ -18,9 +18,6 @@ return function(plasma) end end - custom.tooltip(str, { - tag = "MatterDebuggerTooltip_AltHover", - backgroundTransparency = 0.15, - }) + custom.tooltip(str) end) end diff --git a/lib/debugger/widgets/queryInspect.luau b/lib/debugger/widgets/queryInspect.luau index 1771bf93..e0e9f8f6 100644 --- a/lib/debugger/widgets/queryInspect.luau +++ b/lib/debugger/widgets/queryInspect.luau @@ -1,51 +1,65 @@ -local function getColorForBudget(color: Color3): string - local r = math.floor(color.R * 255) - local g = math.floor(color.G * 255) - local b = math.floor(color.B * 255) +local format = require(script.Parent.Parent.formatTable) - return `{r},{g},{b}` -end - -return function(Plasma) - return Plasma.widget(function(debugger) - local queryWindow = Plasma.window({ - title = "Query Resource Usage (%)", - closable = true, - }, function() - if #debugger._queries == 0 then - return Plasma.label("No queries.") - end - - -- Plasma windows do not respect the title, so we need to - -- fill the content frame to extend the width of the widget - Plasma.heading("--------------------------------------------- ") - - for i, query in debugger._queries do - if query.changedComponent then - Plasma.label(string.format("Query Changed %d", i)) - Plasma.heading(tostring(query.changedComponent)) - - continue +return function(plasma) + return plasma.widget(function(debugger) + return plasma + .window({ + title = "Queries", + closable = true, + }, function() + if #debugger._queries == 0 then + return plasma.label("No queries.") end - local budgetUsed = - math.clamp(math.floor((query.averageDuration / debugger.debugSystemRuntime) * 100), 0, 100) + for i, query in debugger._queries do + if query.changedComponent then + plasma.heading(string.format("Query Changed %d", i)) - local color = Color3.fromRGB(135, 255, 111) - if budgetUsed >= 75 then - color = Color3.fromRGB(244, 73, 73) - elseif budgetUsed >= 50 then - color = Color3.fromRGB(255, 157, 0) - elseif budgetUsed >= 25 then - color = Color3.fromRGB(230, 195, 24) - end + plasma.label(tostring(query.changedComponent)) + + continue + end + + plasma.heading(string.format("Query %d", i)) + + local componentNames = {} + + for _, component in query.components do + table.insert(componentNames, tostring(component)) + end - Plasma.label(`Query {i} - {budgetUsed}%`) - Plasma.heading(table.concat(query.componentNames, ", ")) - end - return nil - end) + plasma.label(table.concat(componentNames, ", ")) - return queryWindow:closed() + local items = { { "ID", unpack(componentNames) } } + + while #items <= 10 do + local data = { query.result:next() } + + if #data == 0 then + break + end + + for index, value in data do + if type(value) == "table" then + data[index] = format.formatTable(value) + else + data[index] = tostring(value) + end + end + + table.insert(items, data) + end + + plasma.table(items, { + headings = true, + }) + + if #items > 10 and query.result:next() then + plasma.label("(further results truncated)") + end + end + return nil + end) + :closed() end) end diff --git a/lib/debugger/widgets/selectionList.luau b/lib/debugger/widgets/selectionList.luau index 9d44a774..5842b00f 100644 --- a/lib/debugger/widgets/selectionList.luau +++ b/lib/debugger/widgets/selectionList.luau @@ -1,16 +1,13 @@ return function(Plasma) local create = Plasma.create - local Item = Plasma.widget(function(text, selected, icon, sideText, barWidth, index) + local Item = Plasma.widget(function(text, selected, icon, sideText, _, barWidth, index) local clicked, setClicked = Plasma.useState(false) - local rightClicked, setRightClicked = Plasma.useState(false) - local hovered, setHovered = Plasma.useState(false) local style = Plasma.useStyle() local refs = Plasma.useInstance(function(ref) local button = create("TextButton", { [ref] = "button", - AutoButtonColor = false, Size = UDim2.new(1, 0, 0, 25), Text = "", @@ -37,7 +34,6 @@ return function(Plasma) }), create("TextLabel", { - [ref] = "index", Name = "index", AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.new(0, 0, 1, 0), @@ -62,12 +58,10 @@ return function(Plasma) }), create("TextLabel", { - [ref] = "mainText", AutomaticSize = Enum.AutomaticSize.X, BackgroundTransparency = 1, Size = UDim2.new(0, 0, 1, 0), Text = text, - RichText = true, TextXAlignment = Enum.TextXAlignment.Left, TextSize = 13, TextColor3 = style.textColor, @@ -87,7 +81,6 @@ return function(Plasma) Text = "", TextXAlignment = Enum.TextXAlignment.Left, TextSize = 11, - RichText = true, TextColor3 = style.mutedTextColor, Font = Enum.Font.Gotham, }), @@ -105,20 +98,8 @@ return function(Plasma) ZIndex = 2, }), - InputBegan = function(input: InputObject) - if input.UserInputType == Enum.UserInputType.MouseButton1 then - setClicked(true) - elseif input.UserInputType == Enum.UserInputType.MouseButton2 then - setRightClicked(true) - end - end, - - MouseEnter = function() - setHovered(true) - end, - - MouseLeave = function() - setHovered(false) + Activated = function() + setClicked(true) end, }) @@ -126,7 +107,7 @@ return function(Plasma) end) Plasma.useEffect(function() - refs.mainText.Text = text + refs.button.container.TextLabel.Text = text refs.button.container.Icon.Text = icon or "" refs.button.container.Icon.Visible = icon ~= nil end, text, icon) @@ -134,18 +115,13 @@ return function(Plasma) refs.button.container.sideText.Visible = sideText ~= nil refs.button.container.sideText.Text = if sideText ~= nil then sideText else "" refs.button.container.sideText.TextColor3 = if selected then style.textColor else style.mutedTextColor - refs.mainText.TextTruncate = sideText and Enum.TextTruncate.AtEnd or Enum.TextTruncate.None + refs.button.container.TextLabel.TextTruncate = sideText and Enum.TextTruncate.AtEnd or Enum.TextTruncate.None refs.button.bar.Size = UDim2.new(barWidth or 0, 0, 0, 1) Plasma.useEffect(function() - refs.button.BackgroundColor3 = if selected - then style.primaryColor - elseif hovered then style.bg1 - else style.bg2 - - refs.index.TextColor3 = if selected or hovered then style.textColor else style.mutedTextColor - end, selected, hovered) + refs.button.BackgroundColor3 = if selected then style.primaryColor else style.bg2 + end, selected) return { clicked = function() @@ -156,29 +132,19 @@ return function(Plasma) return false end, - rightClicked = function() - if rightClicked then - setRightClicked(false) - return true - end - - return false - end, - hovered = function() - return hovered - end, } end) - return Plasma.widget(function(items) + return Plasma.widget(function(items, options) + options = options or {} + Plasma.useInstance(function() local frame = create("Frame", { BackgroundTransparency = 1, - Size = UDim2.new(1, 0, 0, 0), + Size = options.width and UDim2.new(0, options.width, 0, 0) or UDim2.new(1, 0, 0, 0), create("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, - Padding = UDim.new(0, 2), }), }) @@ -190,35 +156,19 @@ return function(Plasma) end) local selected - local rightClicked - local hovered for _, item in items do - local buttonInList = Item(item.text, item.selected, item.icon, item.sideText, item.barWidth, item.index) - - if buttonInList:clicked() then + if + Item(item.text, item.selected, item.icon, item.sideText, options.width, item.barWidth, item.index):clicked() + then selected = item end - - if buttonInList:rightClicked() then - rightClicked = item - end - - if buttonInList:hovered() then - hovered = item - end end return { selected = function() return selected end, - rightClicked = function() - return rightClicked - end, - hovered = function() - return hovered - end, } end) end diff --git a/lib/debugger/widgets/tooltip.luau b/lib/debugger/widgets/tooltip.luau index 499479df..a99f969a 100644 --- a/lib/debugger/widgets/tooltip.luau +++ b/lib/debugger/widgets/tooltip.luau @@ -1,14 +1,8 @@ local CollectionService = game:GetService("CollectionService") - -type Options = { - tag: string, - backgroundTransparency: number?, -} - return function(plasma) local create = plasma.create - return plasma.widget(function(text, options: Options) + return plasma.widget(function(text) local refs = plasma.useInstance(function(ref) local style = plasma.useStyle() @@ -22,10 +16,9 @@ return function(plasma) Font = Enum.Font.Code, TextStrokeTransparency = 0.5, TextColor3 = Color3.new(1, 1, 1), - BackgroundTransparency = options.backgroundTransparency or 0.2, + BackgroundTransparency = 0.2, BackgroundColor3 = style.bg1, AutomaticSize = Enum.AutomaticSize.XY, - ZIndex = 100, create("UIPadding", { PaddingBottom = UDim.new(0, 4), @@ -41,7 +34,7 @@ return function(plasma) }), }) - CollectionService:AddTag(ref.label, options.tag) + CollectionService:AddTag(ref.label, "MatterDebuggerTooltip") return ref.label end) diff --git a/lib/debugger/widgets/worldInspect.luau b/lib/debugger/widgets/worldInspect.luau index 094ae13c..1281081e 100644 --- a/lib/debugger/widgets/worldInspect.luau +++ b/lib/debugger/widgets/worldInspect.luau @@ -1,9 +1,6 @@ local formatTableModule = require(script.Parent.Parent.formatTable) local formatTable = formatTableModule.formatTable -local BY_COMPONENT_NAME = "ComponentName" -local BY_ENTITY_COUNT = "EntityCount" - return function(plasma) return plasma.widget(function(debugger, objectStack) local style = plasma.useStyle() @@ -11,10 +8,8 @@ return function(plasma) local world = debugger.debugWorld local cache, setCache = plasma.useState() - - local sortType, setSortType = plasma.useState(BY_COMPONENT_NAME) - local isAscendingOrder, setIsAscendingOrder = plasma.useState(true) - + -- TODO #97 Implement sorting by descending as well. + local ascendingOrder, _ = plasma.useState(false) local skipIntersections, setSkipIntersections = plasma.useState(true) local debugComponent, setDebugComponent = plasma.useState() @@ -76,47 +71,26 @@ return function(plasma) }) end - local indexForSort = if sortType == BY_ENTITY_COUNT then 1 else 2 - table.sort(items, function(a, b) - if isAscendingOrder then - return a[indexForSort] < b[indexForSort] + if ascendingOrder then + return a[1] < b[1] end - return a[indexForSort] > b[indexForSort] + -- Default to alphabetical + return a[2] < b[2] end) - local arrow = if isAscendingOrder then "â–²" else "â–¼" - local countHeading = `{if sortType == BY_ENTITY_COUNT then arrow else ""} Count ` - local componentHeading = `{if sortType == BY_COMPONENT_NAME then arrow else ""} Component` - local headings = { countHeading, componentHeading } - table.insert(items, 1, headings) + table.insert(items, 1, { "Count", "Component" }) plasma.row({ padding = 30 }, function() - local worldInspectTable = plasma.table(items, { - width = 200, - headings = true, - selectable = true, - font = Enum.Font.Code, - }) - - local selectedHeading = worldInspectTable:selectedHeading() - - if headings[selectedHeading] == headings[1] then - if sortType == BY_ENTITY_COUNT then - setIsAscendingOrder(not isAscendingOrder) - else - setSortType(BY_ENTITY_COUNT) - end - elseif headings[selectedHeading] == headings[2] then - if sortType == BY_COMPONENT_NAME then - setIsAscendingOrder(not isAscendingOrder) - else - setSortType(BY_COMPONENT_NAME) - end - end - - local selectedRow = worldInspectTable:selected() + local selectedRow = plasma + .table(items, { + width = 200, + headings = true, + selectable = true, + font = Enum.Font.Code, + }) + :selected() if selectedRow then setDebugComponent(selectedRow.component) diff --git a/wally.toml b/wally.toml index ae150483..d3139569 100644 --- a/wally.toml +++ b/wally.toml @@ -1,7 +1,7 @@ [package] name = "matter-ecs/matter" description = "A modern ECS library for Roblox" -version = "0.8.4" +version = "0.8.3" license = "MIT" authors = [ "Eryn L. K.", From 3197ca27bf5b3cb3bc3b6cd7983ee5c26210e79d Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 27 Aug 2024 21:27:37 -0400 Subject: [PATCH 65/87] Fix some issues introduced in merge --- lib/World.luau | 39 +++++++-------------------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 04a593e2..eec07ecb 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -9,33 +9,6 @@ local archetypeOf = archetypeModule.archetypeOf local negateArchetypeOf = archetypeModule.negateArchetypeOf local areArchetypesCompatible = archetypeModule.areArchetypesCompatible -local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" -local ERROR_EXISTING_ENTITY = - "The world already contains an entity with ID %s. Use world:replace instead if this is intentional." - --- The old solver is not great at resolving intersections, so we redefine entityId each time. -type DespawnCommand = { type: "despawn", entityId: number } - -type InsertCommand = { - type: "insert", - entityId: number, - componentInstances: { [any]: any }, -} - -type RemoveCommand = { - type: "remove", - entityId: number, - components: { [any]: any }, -} - -type ReplaceCommand = { - type: "replace", - entityId: number, - componentInstances: { [any]: any }, -} - -type Command = DespawnCommand | InsertCommand | RemoveCommand | ReplaceCommand - local function assertEntityExists(world, id: number) assert(world:contains(id), "Entity doesn't exist, use world:contains to check if needed") end @@ -475,10 +448,12 @@ local function bufferCommand(world: World, command: Command) if command.type == "despawn" then markedForDeletion[command.entityId] = true - table.insert(world.commands, command) - else - processCommand(world, command) - end + end + + table.insert(world.commands, command) + else + processCommand(world, command) + end end --[=[ @@ -1393,7 +1368,7 @@ function World:insert(id, ...) local componentInstances = { ... } assertValidComponentInstances(componentInstances) - + bufferCommand(self, { type = "insert", entityId = id, componentInstances = componentInstances }) end From a743d60ba689124cedb4ee716d7a2a4dc8ae20da Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 27 Aug 2024 21:35:34 -0400 Subject: [PATCH 66/87] Fix more merge issues --- docs/Guides/Migration.md | 2 +- docs/Installation.md | 2 +- lib/Loop.luau | 17 +++- lib/World.luau | 4 - lib/World.spec.luau | 6 +- lib/debugger/clientBindings.luau | 39 ++++++--- lib/debugger/debugger.luau | 2 + lib/debugger/formatTable.luau | 2 +- lib/debugger/hookWorld.luau | 36 ++++++++- lib/debugger/ui.luau | 97 +++++++++++++++++------ lib/debugger/widgets/hoverInspect.luau | 5 +- lib/debugger/widgets/queryInspect.luau | 100 ++++++++++-------------- lib/debugger/widgets/selectionList.luau | 78 ++++++++++++++---- lib/debugger/widgets/tooltip.luau | 13 ++- lib/debugger/widgets/worldInspect.luau | 56 +++++++++---- wally.toml | 2 +- 16 files changed, 324 insertions(+), 137 deletions(-) diff --git a/docs/Guides/Migration.md b/docs/Guides/Migration.md index 2044fb04..f8205d8a 100644 --- a/docs/Guides/Migration.md +++ b/docs/Guides/Migration.md @@ -4,5 +4,5 @@ Migrating from `evaera/matter` to `matter-ecs/matter` is easy! The only thing yo ```toml title="wally.toml" [dependencies] - matter = "matter-ecs/matter@0.8.3" + matter = "matter-ecs/matter@0.8.4" ``` diff --git a/docs/Installation.md b/docs/Installation.md index 3f4c83a4..02eb87c1 100644 --- a/docs/Installation.md +++ b/docs/Installation.md @@ -25,7 +25,7 @@ wally = "UpliftGames/wally@x.x.x" ```toml title="wally.toml" [dependencies] -matter = "matter-ecs/matter@0.8.3" +matter = "matter-ecs/matter@0.8.4" ``` 6. Run `wally install`. diff --git a/lib/Loop.luau b/lib/Loop.luau index c1ccd5e0..eccb2698 100644 --- a/lib/Loop.luau +++ b/lib/Loop.luau @@ -59,6 +59,7 @@ function Loop.new(...) _middlewares = {}, _systemErrors = {}, _systemLogs = {}, + _debugger = nil, profiling = nil, trackErrors = false, }, Loop) @@ -424,7 +425,21 @@ function Loop:begin(events) profiling[system] = {} end - rollingAverage.addSample(profiling[system], duration) + local debugger = self._debugger + if debugger and debugger.debugSystem == system and debugger._queries then + local totalQueryTime = 0 + + for _, query in debugger._queries do + totalQueryTime += query.averageDuration + end + + rollingAverage.addSample( + profiling[system], + if debugger.debugSystem then duration - totalQueryTime else duration + ) + else + rollingAverage.addSample(profiling[system], duration) + end end if coroutine.status(thread) ~= "dead" then diff --git a/lib/World.luau b/lib/World.luau index eec07ecb..2eac39e2 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -9,10 +9,6 @@ local archetypeOf = archetypeModule.archetypeOf local negateArchetypeOf = archetypeModule.negateArchetypeOf local areArchetypesCompatible = archetypeModule.areArchetypesCompatible -local function assertEntityExists(world, id: number) - assert(world:contains(id), "Entity doesn't exist, use world:contains to check if needed") -end - type EntityId = number type DenseEntityId = number type ComponentId = number diff --git a/lib/World.spec.luau b/lib/World.spec.luau index b94d9cee..86f4042a 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -40,7 +40,7 @@ local function assertDeepEqual(a, b) end return function() - describe("World", function() + describeFOCUS("World", function() describe("buffered", function() local function createDeferredWorld() local world = World.new() @@ -142,7 +142,7 @@ return function() end) end) - describeFOCUS("immediate", function() + describe("immediate", function() it("should work", function() local world = World.new() local A, B = component(), component() @@ -503,7 +503,7 @@ return function() local world = World.new() local loop = Loop.new(world) - + local A = component() local B = component() local C = component() diff --git a/lib/debugger/clientBindings.luau b/lib/debugger/clientBindings.luau index 405aed41..fdac1a05 100644 --- a/lib/debugger/clientBindings.luau +++ b/lib/debugger/clientBindings.luau @@ -1,5 +1,21 @@ local UserInputService = game:GetService("UserInputService") local CollectionService = game:GetService("CollectionService") + +local tags = { + system = "MatterDebuggerTooltip_System", + altHover = "MatterDebuggerTooltip_AltHover", +} + +local function getOffset(mousePos: Vector2, tag: string): UDim2 + if tag == tags.altHover then + return UDim2.fromOffset(mousePos.X + 20, mousePos.Y) + elseif tag == tags.system then + return UDim2.fromOffset(mousePos.X + 20, mousePos.Y + 10) + end + + return UDim2.fromOffset(mousePos.X, mousePos.Y + 10) +end + local function clientBindings(debugger) local connections = {} @@ -21,20 +37,23 @@ local function clientBindings(debugger) local mousePosition = UserInputService:GetMouseLocation() - for _, gui in CollectionService:GetTagged("MatterDebuggerTooltip") do - gui.Position = UDim2.new(0, mousePosition.X + 20, 0, mousePosition.Y) + for _, tag in tags do + for _, gui in CollectionService:GetTagged(tag) do + gui.Position = getOffset(mousePosition, tag) + end end end) ) - table.insert( - connections, - CollectionService:GetInstanceAddedSignal("MatterDebuggerTooltip"):Connect(function(gui) - local mousePosition = UserInputService:GetMouseLocation() - - gui.Position = UDim2.new(0, mousePosition.X + 20, 0, mousePosition.Y) - end) - ) + for _, tag in tags do + table.insert( + connections, + CollectionService:GetInstanceAddedSignal(tag):Connect(function(gui) + local mousePosition = UserInputService:GetMouseLocation() + gui.Position = getOffset(mousePosition, tag) + end) + ) + end return connections end diff --git a/lib/debugger/debugger.luau b/lib/debugger/debugger.luau index 4bfa0e95..bf5b410a 100644 --- a/lib/debugger/debugger.luau +++ b/lib/debugger/debugger.luau @@ -168,6 +168,7 @@ function Debugger.new(plasma) componentRefreshFrequency = 3, _windowCount = 0, _queries = {}, + _queryDurationSamples = {}, _seenEvents = {}, _eventOrder = {}, _eventBridge = EventBridge.new(function(...) @@ -344,6 +345,7 @@ end ]=] function Debugger:autoInitialize(loop) self.loop = loop + loop._debugger = self self.loop.trackErrors = true diff --git a/lib/debugger/formatTable.luau b/lib/debugger/formatTable.luau index 57f9d9d0..01dacd2e 100644 --- a/lib/debugger/formatTable.luau +++ b/lib/debugger/formatTable.luau @@ -7,7 +7,7 @@ function colorize(value, color) value = string.format("%.1f", value) end - return `{value}` + return `{value}` end function colorizeMany(color, ...) diff --git a/lib/debugger/hookWorld.luau b/lib/debugger/hookWorld.luau index 1765ca58..e69bb567 100644 --- a/lib/debugger/hookWorld.luau +++ b/lib/debugger/hookWorld.luau @@ -1,14 +1,46 @@ +--#selene:allow(empty_loop) local useCurrentSystem = require(script.Parent.Parent.topoRuntime).useCurrentSystem local World = require(script.Parent.Parent.World) +local rollingAverage = require(script.Parent.Parent.rollingAverage) local originalQuery = World.query local function hookWorld(debugger) World.query = function(world, ...) if useCurrentSystem() == debugger.debugSystem then + local start = os.clock() + + -- while this seems like a mistake, it is necessary! + -- we duplicate the query to avoid draining the original one. + -- we iterate through so we can calculate the query's budget + -- see https://github.com/matter-ecs/matter/issues/106 + -- and https://github.com/matter-ecs/matter/pull/107 + for _ in originalQuery(world, ...) do + end + + local file, line = debug.info(2, "sl") + + local key = file .. line + local samples = debugger._queryDurationSamples + local sample = samples[key] + if not sample then + sample = {} + samples[key] = sample + end + + local componentNames = {} + for i = 1, select("#", ...) do + table.insert(componentNames, tostring((select(i, ...)))) + end + + local duration = os.clock() - start + rollingAverage.addSample(sample, duration) + + local averageDuration = rollingAverage.getAverage(debugger._queryDurationSamples[file .. line]) + table.insert(debugger._queries, { - components = { ... }, - result = originalQuery(world, ...), + averageDuration = averageDuration, + componentNames = componentNames, }) end diff --git a/lib/debugger/ui.luau b/lib/debugger/ui.luau index e1127a7a..aa671a1d 100644 --- a/lib/debugger/ui.luau +++ b/lib/debugger/ui.luau @@ -29,10 +29,12 @@ end local IS_SERVER = RunService:IsServer() local IS_CLIENT = RunService:IsClient() +local LONG_SYSTEM_NAME = 24 local function ui(debugger, loop) local plasma = debugger.plasma local custom = debugger._customWidgets + local skipSystems = loop._skipSystems plasma.setStyle({ primaryColor = Color3.fromHex("bd515c"), @@ -40,11 +42,16 @@ local function ui(debugger, loop) local objectStack = plasma.useState({}) local worldViewOpen, setWorldViewOpen = plasma.useState(false) + local worldExists = debugger.debugWorld ~= nil - if debugger.hoverEntity then - custom.hoverInspect(debugger.debugWorld, debugger.hoverEntity, custom) + if debugger.hoverEntity and worldExists then + if debugger.debugWorld:contains(debugger.hoverEntity) then + custom.hoverInspect(debugger.debugWorld, debugger.hoverEntity, custom) + end end + local hoveredSystem + custom.container(function() if debugger:_isServerView() then return @@ -138,13 +145,12 @@ local function ui(debugger, loop) }) plasma.space(5) - local items = {} + local listOfSystems = {} for _, system in systems do local samples = loop.profiling[system] if samples then local duration = rollingAverage.getAverage(samples) - durations[system] = duration longestDuration = math.max(longestDuration, duration) end @@ -166,27 +172,57 @@ local function ui(debugger, loop) icon = "\xf0\x9f\x92\xa5" end - table.insert(items, { - text = systemName(system), - sideText = averageFrameTime, + local barWidth + if longestDuration == 0 then + barWidth = 0 + else + barWidth = duration / longestDuration + end + + local systemIsDisabled = skipSystems[system] + local systemName = systemName(system) + local length = string.len(systemName) + + if systemIsDisabled then + length += 4 + end + + table.insert(listOfSystems, { + text = if systemIsDisabled then `{systemName}` else systemName, + sideText = if systemIsDisabled then `{"(disabled)"}` else averageFrameTime, selected = debugger.debugSystem == system, system = system, icon = icon, - barWidth = duration / longestDuration, + barWidth = barWidth, index = index, }) end - local selected = custom.selectionList(items):selected() + local systemList = custom.selectionList(listOfSystems, custom) + local selected = systemList:selected() + local rightClicked = systemList:rightClicked() + hoveredSystem = systemList:hovered() if selected then - if selected.system == debugger.debugSystem then + local selectedSystem = selected.system + if selectedSystem == debugger.debugSystem then debugger.debugSystem = nil else - debugger.debugSystem = selected.system + debugger.debugSystem = selectedSystem + end + elseif rightClicked then + local rightClickedSystem = rightClicked.system + if rightClickedSystem then + skipSystems[rightClickedSystem] = not skipSystems[rightClickedSystem] end end + if debugger.debugSystem then + debugger.debugSystemRuntime = durations[debugger.debugSystem] + else + debugger.debugSystemRuntime = nil + end + plasma.space(10) end end) @@ -227,28 +263,26 @@ local function ui(debugger, loop) }, function() plasma.useKey(name) - plasma.row(function() - if plasma.button(string.format("View queries (%d)", #debugger._queries)):clicked() then - setQueriesOpen(true) - end + if plasma.button(string.format("View queries (%d)", #debugger._queries)):clicked() then + setQueriesOpen(true) + end - if numLogs > 0 then - if plasma.button(string.format("View logs (%d)", numLogs)):clicked() then - setLogsOpen(true) - end + if numLogs > 0 then + if plasma.button(string.format("View logs (%d)", numLogs)):clicked() then + setLogsOpen(true) end - end) + end - local currentlyDisabled = loop._skipSystems[debugger.debugSystem] + local currentlyDisabled = skipSystems[debugger.debugSystem] if plasma - .checkbox("Disable system", { + .checkbox("Disable System", { checked = currentlyDisabled, }) :clicked() then - loop._skipSystems[debugger.debugSystem] = not currentlyDisabled + skipSystems[debugger.debugSystem] = not currentlyDisabled end end) :closed() @@ -304,6 +338,23 @@ local function ui(debugger, loop) direction = Enum.FillDirection.Horizontal, padding = 0, }) + + if hoveredSystem and hoveredSystem.system then + local hoveredSystemName = systemName(hoveredSystem.system) + local length = string.len(hoveredSystemName) + local systemDisabled = skipSystems[hoveredSystem.system] + + if systemDisabled then + length += 2 + end + + if length >= LONG_SYSTEM_NAME then + custom.tooltip(`{hoveredSystemName}{if systemDisabled then " (disabled)" else ""}`, { + tag = "MatterDebuggerTooltip_System", + backgroundTransparency = 0, + }) + end + end end return ui diff --git a/lib/debugger/widgets/hoverInspect.luau b/lib/debugger/widgets/hoverInspect.luau index d9b5daa3..c0c10d88 100644 --- a/lib/debugger/widgets/hoverInspect.luau +++ b/lib/debugger/widgets/hoverInspect.luau @@ -18,6 +18,9 @@ return function(plasma) end end - custom.tooltip(str) + custom.tooltip(str, { + tag = "MatterDebuggerTooltip_AltHover", + backgroundTransparency = 0.15, + }) end) end diff --git a/lib/debugger/widgets/queryInspect.luau b/lib/debugger/widgets/queryInspect.luau index e0e9f8f6..1771bf93 100644 --- a/lib/debugger/widgets/queryInspect.luau +++ b/lib/debugger/widgets/queryInspect.luau @@ -1,65 +1,51 @@ -local format = require(script.Parent.Parent.formatTable) +local function getColorForBudget(color: Color3): string + local r = math.floor(color.R * 255) + local g = math.floor(color.G * 255) + local b = math.floor(color.B * 255) -return function(plasma) - return plasma.widget(function(debugger) - return plasma - .window({ - title = "Queries", - closable = true, - }, function() - if #debugger._queries == 0 then - return plasma.label("No queries.") - end - - for i, query in debugger._queries do - if query.changedComponent then - plasma.heading(string.format("Query Changed %d", i)) - - plasma.label(tostring(query.changedComponent)) - - continue - end - - plasma.heading(string.format("Query %d", i)) - - local componentNames = {} - - for _, component in query.components do - table.insert(componentNames, tostring(component)) - end - - plasma.label(table.concat(componentNames, ", ")) - - local items = { { "ID", unpack(componentNames) } } - - while #items <= 10 do - local data = { query.result:next() } + return `{r},{g},{b}` +end - if #data == 0 then - break - end +return function(Plasma) + return Plasma.widget(function(debugger) + local queryWindow = Plasma.window({ + title = "Query Resource Usage (%)", + closable = true, + }, function() + if #debugger._queries == 0 then + return Plasma.label("No queries.") + end + + -- Plasma windows do not respect the title, so we need to + -- fill the content frame to extend the width of the widget + Plasma.heading("--------------------------------------------- ") + + for i, query in debugger._queries do + if query.changedComponent then + Plasma.label(string.format("Query Changed %d", i)) + Plasma.heading(tostring(query.changedComponent)) + + continue + end - for index, value in data do - if type(value) == "table" then - data[index] = format.formatTable(value) - else - data[index] = tostring(value) - end - end + local budgetUsed = + math.clamp(math.floor((query.averageDuration / debugger.debugSystemRuntime) * 100), 0, 100) - table.insert(items, data) - end + local color = Color3.fromRGB(135, 255, 111) + if budgetUsed >= 75 then + color = Color3.fromRGB(244, 73, 73) + elseif budgetUsed >= 50 then + color = Color3.fromRGB(255, 157, 0) + elseif budgetUsed >= 25 then + color = Color3.fromRGB(230, 195, 24) + end - plasma.table(items, { - headings = true, - }) + Plasma.label(`Query {i} - {budgetUsed}%`) + Plasma.heading(table.concat(query.componentNames, ", ")) + end + return nil + end) - if #items > 10 and query.result:next() then - plasma.label("(further results truncated)") - end - end - return nil - end) - :closed() + return queryWindow:closed() end) end diff --git a/lib/debugger/widgets/selectionList.luau b/lib/debugger/widgets/selectionList.luau index 5842b00f..9d44a774 100644 --- a/lib/debugger/widgets/selectionList.luau +++ b/lib/debugger/widgets/selectionList.luau @@ -1,13 +1,16 @@ return function(Plasma) local create = Plasma.create - local Item = Plasma.widget(function(text, selected, icon, sideText, _, barWidth, index) + local Item = Plasma.widget(function(text, selected, icon, sideText, barWidth, index) local clicked, setClicked = Plasma.useState(false) + local rightClicked, setRightClicked = Plasma.useState(false) + local hovered, setHovered = Plasma.useState(false) local style = Plasma.useStyle() local refs = Plasma.useInstance(function(ref) local button = create("TextButton", { [ref] = "button", + AutoButtonColor = false, Size = UDim2.new(1, 0, 0, 25), Text = "", @@ -34,6 +37,7 @@ return function(Plasma) }), create("TextLabel", { + [ref] = "index", Name = "index", AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.new(0, 0, 1, 0), @@ -58,10 +62,12 @@ return function(Plasma) }), create("TextLabel", { + [ref] = "mainText", AutomaticSize = Enum.AutomaticSize.X, BackgroundTransparency = 1, Size = UDim2.new(0, 0, 1, 0), Text = text, + RichText = true, TextXAlignment = Enum.TextXAlignment.Left, TextSize = 13, TextColor3 = style.textColor, @@ -81,6 +87,7 @@ return function(Plasma) Text = "", TextXAlignment = Enum.TextXAlignment.Left, TextSize = 11, + RichText = true, TextColor3 = style.mutedTextColor, Font = Enum.Font.Gotham, }), @@ -98,8 +105,20 @@ return function(Plasma) ZIndex = 2, }), - Activated = function() - setClicked(true) + InputBegan = function(input: InputObject) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + setClicked(true) + elseif input.UserInputType == Enum.UserInputType.MouseButton2 then + setRightClicked(true) + end + end, + + MouseEnter = function() + setHovered(true) + end, + + MouseLeave = function() + setHovered(false) end, }) @@ -107,7 +126,7 @@ return function(Plasma) end) Plasma.useEffect(function() - refs.button.container.TextLabel.Text = text + refs.mainText.Text = text refs.button.container.Icon.Text = icon or "" refs.button.container.Icon.Visible = icon ~= nil end, text, icon) @@ -115,13 +134,18 @@ return function(Plasma) refs.button.container.sideText.Visible = sideText ~= nil refs.button.container.sideText.Text = if sideText ~= nil then sideText else "" refs.button.container.sideText.TextColor3 = if selected then style.textColor else style.mutedTextColor - refs.button.container.TextLabel.TextTruncate = sideText and Enum.TextTruncate.AtEnd or Enum.TextTruncate.None + refs.mainText.TextTruncate = sideText and Enum.TextTruncate.AtEnd or Enum.TextTruncate.None refs.button.bar.Size = UDim2.new(barWidth or 0, 0, 0, 1) Plasma.useEffect(function() - refs.button.BackgroundColor3 = if selected then style.primaryColor else style.bg2 - end, selected) + refs.button.BackgroundColor3 = if selected + then style.primaryColor + elseif hovered then style.bg1 + else style.bg2 + + refs.index.TextColor3 = if selected or hovered then style.textColor else style.mutedTextColor + end, selected, hovered) return { clicked = function() @@ -132,19 +156,29 @@ return function(Plasma) return false end, + rightClicked = function() + if rightClicked then + setRightClicked(false) + return true + end + + return false + end, + hovered = function() + return hovered + end, } end) - return Plasma.widget(function(items, options) - options = options or {} - + return Plasma.widget(function(items) Plasma.useInstance(function() local frame = create("Frame", { BackgroundTransparency = 1, - Size = options.width and UDim2.new(0, options.width, 0, 0) or UDim2.new(1, 0, 0, 0), + Size = UDim2.new(1, 0, 0, 0), create("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 2), }), }) @@ -156,19 +190,35 @@ return function(Plasma) end) local selected + local rightClicked + local hovered for _, item in items do - if - Item(item.text, item.selected, item.icon, item.sideText, options.width, item.barWidth, item.index):clicked() - then + local buttonInList = Item(item.text, item.selected, item.icon, item.sideText, item.barWidth, item.index) + + if buttonInList:clicked() then selected = item end + + if buttonInList:rightClicked() then + rightClicked = item + end + + if buttonInList:hovered() then + hovered = item + end end return { selected = function() return selected end, + rightClicked = function() + return rightClicked + end, + hovered = function() + return hovered + end, } end) end diff --git a/lib/debugger/widgets/tooltip.luau b/lib/debugger/widgets/tooltip.luau index a99f969a..499479df 100644 --- a/lib/debugger/widgets/tooltip.luau +++ b/lib/debugger/widgets/tooltip.luau @@ -1,8 +1,14 @@ local CollectionService = game:GetService("CollectionService") + +type Options = { + tag: string, + backgroundTransparency: number?, +} + return function(plasma) local create = plasma.create - return plasma.widget(function(text) + return plasma.widget(function(text, options: Options) local refs = plasma.useInstance(function(ref) local style = plasma.useStyle() @@ -16,9 +22,10 @@ return function(plasma) Font = Enum.Font.Code, TextStrokeTransparency = 0.5, TextColor3 = Color3.new(1, 1, 1), - BackgroundTransparency = 0.2, + BackgroundTransparency = options.backgroundTransparency or 0.2, BackgroundColor3 = style.bg1, AutomaticSize = Enum.AutomaticSize.XY, + ZIndex = 100, create("UIPadding", { PaddingBottom = UDim.new(0, 4), @@ -34,7 +41,7 @@ return function(plasma) }), }) - CollectionService:AddTag(ref.label, "MatterDebuggerTooltip") + CollectionService:AddTag(ref.label, options.tag) return ref.label end) diff --git a/lib/debugger/widgets/worldInspect.luau b/lib/debugger/widgets/worldInspect.luau index 1281081e..094ae13c 100644 --- a/lib/debugger/widgets/worldInspect.luau +++ b/lib/debugger/widgets/worldInspect.luau @@ -1,6 +1,9 @@ local formatTableModule = require(script.Parent.Parent.formatTable) local formatTable = formatTableModule.formatTable +local BY_COMPONENT_NAME = "ComponentName" +local BY_ENTITY_COUNT = "EntityCount" + return function(plasma) return plasma.widget(function(debugger, objectStack) local style = plasma.useStyle() @@ -8,8 +11,10 @@ return function(plasma) local world = debugger.debugWorld local cache, setCache = plasma.useState() - -- TODO #97 Implement sorting by descending as well. - local ascendingOrder, _ = plasma.useState(false) + + local sortType, setSortType = plasma.useState(BY_COMPONENT_NAME) + local isAscendingOrder, setIsAscendingOrder = plasma.useState(true) + local skipIntersections, setSkipIntersections = plasma.useState(true) local debugComponent, setDebugComponent = plasma.useState() @@ -71,26 +76,47 @@ return function(plasma) }) end + local indexForSort = if sortType == BY_ENTITY_COUNT then 1 else 2 + table.sort(items, function(a, b) - if ascendingOrder then - return a[1] < b[1] + if isAscendingOrder then + return a[indexForSort] < b[indexForSort] end - -- Default to alphabetical - return a[2] < b[2] + return a[indexForSort] > b[indexForSort] end) - table.insert(items, 1, { "Count", "Component" }) + local arrow = if isAscendingOrder then "â–²" else "â–¼" + local countHeading = `{if sortType == BY_ENTITY_COUNT then arrow else ""} Count ` + local componentHeading = `{if sortType == BY_COMPONENT_NAME then arrow else ""} Component` + local headings = { countHeading, componentHeading } + table.insert(items, 1, headings) plasma.row({ padding = 30 }, function() - local selectedRow = plasma - .table(items, { - width = 200, - headings = true, - selectable = true, - font = Enum.Font.Code, - }) - :selected() + local worldInspectTable = plasma.table(items, { + width = 200, + headings = true, + selectable = true, + font = Enum.Font.Code, + }) + + local selectedHeading = worldInspectTable:selectedHeading() + + if headings[selectedHeading] == headings[1] then + if sortType == BY_ENTITY_COUNT then + setIsAscendingOrder(not isAscendingOrder) + else + setSortType(BY_ENTITY_COUNT) + end + elseif headings[selectedHeading] == headings[2] then + if sortType == BY_COMPONENT_NAME then + setIsAscendingOrder(not isAscendingOrder) + else + setSortType(BY_COMPONENT_NAME) + end + end + + local selectedRow = worldInspectTable:selected() if selectedRow then setDebugComponent(selectedRow.component) diff --git a/wally.toml b/wally.toml index d3139569..ae150483 100644 --- a/wally.toml +++ b/wally.toml @@ -1,7 +1,7 @@ [package] name = "matter-ecs/matter" description = "A modern ECS library for Roblox" -version = "0.8.3" +version = "0.8.4" license = "MIT" authors = [ "Eryn L. K.", From 50861c3400b341eceb03ce6726a666264c5e759c Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 27 Aug 2024 21:36:15 -0400 Subject: [PATCH 67/87] Fix changelog --- CHANGELOG.md | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bae71a70..3a1f5f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,15 +26,28 @@ The format is based on [Keep a Changelog][kac], and this project adheres to ### Added -- Implemented a deferred command mode for the registry. - - The Loop turns deferring on for all worlds given to it. - - The command buffer is flushed between systems. - - Iterator invalidation is now only prevented in deferred mode. +- Better assertions / error messages added to `World` methods that accept + variadic component arguments. At least 1 component must be provided. These + assertions have been added to `get` `insert` `replace` `remove` +- Ability to sort the world inspect table by clicking the table headers (entity + count and component name) +- Ability to disable systems in the debugger list by right clicking them. -### Deprecated +### Changed -- Deprecated the return type of `World:remove()` because it can now be inaccurate. -- Deprecated `World:optimizeQueries()` because it no longer does anything. +- The alt-hover tooltip's text is smaller and the background is slightly darker + for improved legibility. +- Component data now has syntax highlighting applied. This is present in the + **alt-hover tooltip** and the **entity inspector panel** in the debugger. + +### Fixed + +- The alt-hover tooltip now displays component data properly, with each + component being displayed on a new line. +- Removed extra new-lines in component data strings within the debugger entity + inspect tables. +- Fixed alt-hover erroring when hovered entity is despawned. +- Fixed flashing buttons ("View queries" and "View logs") in system inspect panel ## [0.8.3] - 2024-07-02 @@ -315,7 +328,8 @@ The format is based on [Keep a Changelog][kac], and this project adheres to - Initial release -[unreleased]: https://github.com/matter-ecs/matter/compare/v0.8.3...HEAD +[unreleased]: https://github.com/matter-ecs/matter/compare/v0.8.4...HEAD +[0.8.4]: https://github.com/matter-ecs/matter/releases/tag/v0.8.4 [0.8.3]: https://github.com/matter-ecs/matter/releases/tag/v0.8.3 [0.8.2]: https://github.com/matter-ecs/matter/releases/tag/v0.8.2 [0.8.1]: https://github.com/matter-ecs/matter/releases/tag/v0.8.1 From 4fd00c8e39ff15614834d0409b6742a7521292ef Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 27 Aug 2024 21:37:09 -0400 Subject: [PATCH 68/87] Fix README version change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b79836b3..a4332b2b 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Matter can be installed with [Wally] by including it as a dependency in your `wally.toml` file. ```toml -Matter = "matter-ecs/matter@0.8.3" +Matter = "matter-ecs/matter@0.8.4" ``` ## Migration From 097ab691b3036ed3d42e2c8b1a0f6dd0fffce22e Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 4 Oct 2024 13:59:39 -0400 Subject: [PATCH 69/87] add basic archetype tree --- benchmarks/stress.bench.luau | 12 ++-- lib/World.luau | 110 ++++++++++++++++------------------- lib/World.spec.luau | 14 ++++- lib/archetype.luau | 104 --------------------------------- lib/archetypeTree.luau | 100 +++++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 168 deletions(-) create mode 100644 lib/archetypeTree.luau diff --git a/benchmarks/stress.bench.luau b/benchmarks/stress.bench.luau index df7f8c41..6e85a492 100644 --- a/benchmarks/stress.bench.luau +++ b/benchmarks/stress.bench.luau @@ -16,7 +16,7 @@ local function flip() return math.random() > 0.5 end -for i = 1, 200_000 do +for i = 1, 50_000 do local id = i world:spawnAt(id) pinnedWorld:spawnAt(id) @@ -46,16 +46,20 @@ return { Functions = { ["Old Matter"] = function() + local count = 0 for _ in pinnedWorld:query(pinnedA, pinnedB) do + count += 1 end + + --print("old:", count) end, ["New Matter"] = function() - --local count = 0 + local count = 0 for _ in world:query(A, B) do - --count += 1 + count += 1 end - --print(count) + --print("new:", count) end, }, } diff --git a/lib/World.luau b/lib/World.luau index 2eac39e2..f16b51da 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -1,6 +1,7 @@ --!strict local Component = require(script.Parent.component) local archetypeModule = require(script.Parent.archetype) +local archetypeTree = require(script.Parent.archetypeTree) local topoRuntime = require(script.Parent.topoRuntime) local assertValidComponentInstances = Component.assertValidComponentInstances @@ -17,20 +18,7 @@ type Component = { [any]: any } type ComponentInstance = { [any]: any } type ArchetypeId = string -type Archetype = { - ownedEntities: { EntityId }, - - --- The component IDs that are part of this archetype, in no particular order - componentIds: { ComponentId }, - - --- Maps a component ID to its index in the storage - componentIdToStorageIndex: { [ComponentId]: number }, - - --- Maps a storage index to its component ID, useful for iterating the world - storageIndexToComponentId: { [number]: ComponentId }, - - storage: { { ComponentInstance } }, -} +type Archetype = archetypeTree.Archetype type EntityRecord = { indexInArchetype: number, @@ -84,7 +72,6 @@ local World = {} World.__index = World local function createArchetype(world: World, componentIds: { ComponentId }): Archetype - local archetypeId = archetypeOf(componentIds) local componentIdToStorageIndex, storageIndexToComponentId = {}, {} local length = #componentIds local storage = table.create(length) @@ -121,6 +108,7 @@ end ]=] function World.new() local self = setmetatable({ + archetypeTree = archetypeTree.new(), entities = {} :: Entities, componentIdToComponent = {}, componentToArchetypes = {} :: ComponentToArchetypes, @@ -158,7 +146,9 @@ function World.new() _changedStorage = {}, }, World) - self.rootArchetype = createArchetype(self, {}) + self.archetypeTree:ensureArchetype({}) + --print(self.archetypeTree:findNode({ 1, 2, 3 })) + --print(self.archetypeTree) return self end export type World = typeof(World.new()) @@ -216,23 +206,23 @@ function World.__iter(world: World) end local function ensureArchetype(world: World, componentIds: { number }) - local archetypeId = archetypeOf(componentIds) - return world.archetypeIdToArchetype[archetypeId] or createArchetype(world, componentIds) + return world.archetypeTree:ensureArchetype(componentIds) --world.archetypeIdToArchetype[archetypeId] or createArchetype(world, componentIds) end local function ensureRecord(world: World, entityId: number): EntityRecord local entityRecord = world.entities[entityId] if entityRecord == nil then - local rootArchetype = world.rootArchetype + local root = world.archetypeTree.root entityRecord = { - archetype = rootArchetype, - indexInArchetype = #rootArchetype.ownedEntities + 1, + archetype = root.archetype, + indexInArchetype = #root.archetype.ownedEntities + 1, } - table.insert(rootArchetype.ownedEntities, entityId) + table.insert(root.archetype.ownedEntities, entityId) world.entities[entityId] = entityRecord end + --print("record", entityRecord) return entityRecord :: EntityRecord end @@ -245,6 +235,7 @@ local function transitionArchetype( local oldArchetype = entityRecord.archetype local oldEntityIndex = entityRecord.indexInArchetype + --print("transition from", oldArchetype, "to", archetype) -- Add entity to archetype's ownedEntities local ownedEntities = archetype.ownedEntities local entityIndex = #ownedEntities + 1 @@ -253,9 +244,8 @@ local function transitionArchetype( -- Move old storage to new storage if needed local oldNumEntities = #oldArchetype.ownedEntities local wasLastEntity = oldNumEntities == oldEntityIndex - for index, oldComponentStorage in oldArchetype.storage do - local componentStorage = - archetype.storage[archetype.componentIdToStorageIndex[oldArchetype.componentIds[index]]] + for index, oldComponentStorage in oldArchetype.fields do + local componentStorage = archetype.fields[archetype.componentIdToStorageIndex[oldArchetype.componentIds[index]]] -- Does the new storage contain this component? if componentStorage then @@ -327,7 +317,7 @@ local function executeInsert(world: World, insertCommand: InsertCommand) table.insert(componentIds, componentId) archetype = ensureArchetype(world, componentIds) entityIndex = transitionArchetype(world, entityId, entityRecord, archetype) - oldComponentInstance = archetype.storage[archetype.componentIdToStorageIndex[componentId]][entityIndex] + oldComponentInstance = archetype.fields[archetype.componentIdToStorageIndex[componentId]][entityIndex] -- FIXME: -- This shouldn't be in a hotpath, probably better in createArchetype @@ -335,11 +325,10 @@ local function executeInsert(world: World, insertCommand: InsertCommand) else archetype = oldArchetype entityIndex = entityRecord.indexInArchetype - oldComponentInstance = - oldArchetype.storage[oldArchetype.componentIdToStorageIndex[componentId]][entityIndex] + oldComponentInstance = oldArchetype.fields[oldArchetype.componentIdToStorageIndex[componentId]][entityIndex] end - archetype.storage[archetype.componentIdToStorageIndex[componentId]][entityIndex] = componentInstance + archetype.fields[archetype.componentIdToStorageIndex[componentId]][entityIndex] = componentInstance world:_trackChanged(component, entityId, oldComponentInstance, componentInstance) oldArchetype = archetype @@ -697,7 +686,7 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds return end - local storage, componentIdToStorageIndex = currentArchetype.storage, currentArchetype.componentIdToStorageIndex + local storage, componentIdToStorageIndex = currentArchetype.fields, currentArchetype.componentIdToStorageIndex if queryLength == 1 then a = storage[componentIdToStorageIndex[componentIds[1]]] elseif queryLength == 2 then @@ -1146,38 +1135,41 @@ function World.query(self: World, ...) end end - local possibleArchetypes - local compatibleArchetypes = {} - for _, componentId in componentIds do - local associatedArchetypes = self.componentToArchetypes[componentId] - if associatedArchetypes == nil then - return noopQuery - end + local possibleArchetypes = self.archetypeTree:findArchetypes(componentIds) + local compatibleArchetypes = possibleArchetypes + --print("possibleArchetypes:", possibleArchetypes) + -- local possibleArchetypes + -- local compatibleArchetypes = {} + -- for _, componentId in componentIds do + -- local associatedArchetypes = self.componentToArchetypes[componentId] + -- if associatedArchetypes == nil then + -- return noopQuery + -- end - if possibleArchetypes == nil or #possibleArchetypes > #associatedArchetypes then - possibleArchetypes = associatedArchetypes - end - end + -- if possibleArchetypes == nil or #possibleArchetypes > #associatedArchetypes then + -- possibleArchetypes = associatedArchetypes + -- end + -- end -- Narrow the archetypes so only ones that contain all components are searched - for archetypeIndex in possibleArchetypes do - local archetype = self.archetypes[archetypeIndex] - local incompatible = false - for _, componentId in componentIds do - -- Does this archetype have this component? - if archetype.componentIdToStorageIndex[componentId] == nil then - -- Nope, so we can't use this one. - incompatible = true - break - end - end - - if incompatible then - continue - end - - table.insert(compatibleArchetypes, archetype) - end + -- for archetypeIndex in possibleArchetypes do + -- local archetype = self.archetypes[archetypeIndex] + -- local incompatible = false + -- for _, componentId in componentIds do + -- -- Does this archetype have this component? + -- if archetype.componentIdToStorageIndex[componentId] == nil then + -- -- Nope, so we can't use this one. + -- incompatible = true + -- break + -- end + -- end + + -- if incompatible then + -- continue + -- end + + -- table.insert(compatibleArchetypes, archetype) + -- end if #compatibleArchetypes == 0 then return noopQuery diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 86f4042a..97dc2f81 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -40,7 +40,7 @@ local function assertDeepEqual(a, b) end return function() - describeFOCUS("World", function() + describe("World", function() describe("buffered", function() local function createDeferredWorld() local world = World.new() @@ -143,6 +143,18 @@ return function() end) describe("immediate", function() + itFOCUS("test", function() + local world = World.new() + local A, B = component(), component() + world:spawn(A({ a = true }), B({ b = true })) + world:spawn(A({ a = true })) + + for id, a in world:query(A) do + print(id, a) + end + + print("Done:", world.archetypeTree) + end) it("should work", function() local world = World.new() local A, B = component(), component() diff --git a/lib/archetype.luau b/lib/archetype.luau index e51bb6f5..ff7d2571 100644 --- a/lib/archetype.luau +++ b/lib/archetype.luau @@ -1,112 +1,8 @@ -local toSet = require(script.Parent.immutable).toSet - -local valueIds = {} -local nextValueId = 0 -local compatibilityCache = {} -local archetypeCache = {} - -local function getValueId(value) - local valueId = valueIds[value] - if valueId == nil then - valueIds[value] = nextValueId - valueId = nextValueId - nextValueId += 1 - end - - return valueId -end - function archetypeOf(componentIds: { number }) table.sort(componentIds) return table.concat(componentIds, "_") - -- local archetype = "" - -- for _, component in componentIds do - -- archetype = archetype .. "_" .. componentId - -- end - - -- --print("Archetype for", components, archetype) - -- return archetype - -- debug.profilebegin("archetypeOf") - - -- local length = select("#", ...) - - -- local currentNode = archetypeCache - - -- for i = 1, length do - -- local nextNode = currentNode[select(i, ...)] - - -- if not nextNode then - -- nextNode = {} - -- currentNode[select(i, ...)] = nextNode - -- end - - -- currentNode = nextNode - -- end - - -- if currentNode._archetype then - -- debug.profileend() - -- return currentNode._archetype - -- end - - -- local list = table.create(length) - - -- for i = 1, length do - -- list[i] = getValueId(select(i, ...)) - -- end - - -- table.sort(list) - - -- local archetype = table.concat(list, "_") - - -- currentNode._archetype = archetype - - -- debug.profileend() - - -- return archetype -end - -function negateArchetypeOf(...) - return string.gsub(archetypeOf(...), "_", "x") -end - -function areArchetypesCompatible(queryArchetype, targetArchetype) - local archetypes = string.split(queryArchetype, "x") - local baseArchetype = table.remove(archetypes, 1) - - local cachedCompatibility = compatibilityCache[queryArchetype .. "-" .. targetArchetype] - if cachedCompatibility ~= nil then - return cachedCompatibility - end - debug.profilebegin("areArchetypesCompatible") - - local queryIds = string.split(baseArchetype, "_") - local targetIds = toSet(string.split(targetArchetype, "_")) - local excludeIds = toSet(archetypes) - - for _, queryId in ipairs(queryIds) do - if targetIds[queryId] == nil then - compatibilityCache[queryArchetype .. "-" .. targetArchetype] = false - debug.profileend() - return false - end - end - - for excludeId in excludeIds do - if targetIds[excludeId] then - compatibilityCache[queryArchetype .. "-" .. targetArchetype] = false - debug.profileend() - return false - end - end - - compatibilityCache[queryArchetype .. "-" .. targetArchetype] = true - - debug.profileend() - return true end return { archetypeOf = archetypeOf, - negateArchetypeOf = negateArchetypeOf, - areArchetypesCompatible = areArchetypesCompatible, } diff --git a/lib/archetypeTree.luau b/lib/archetypeTree.luau new file mode 100644 index 00000000..34d88cf3 --- /dev/null +++ b/lib/archetypeTree.luau @@ -0,0 +1,100 @@ +type ComponentId = number +type EntityId = number + +export type Archetype = { + ownedEntities: { EntityId }, + + --- The component IDs that are part of this archetype, in no particular order + componentIds: { ComponentId }, + + --- Maps a component ID to its index in the storage + componentIdToStorageIndex: { [ComponentId]: number }, + + --- Maps a storage index to its component ID, useful for iterating the world + storageIndexToComponentId: { [number]: ComponentId }, + + fields: { { any } }, +} + +type Node = { + children: { [ComponentId]: Node? }, + archetype: Archetype?, +} + +type Terms = { ComponentId } + +local function newArchetypeTree() + local function createNode(): Node + local node: Node = { + children = {}, + } + + return node + end + + local root = createNode() + local function findNode(_, terms: Terms): Node + table.sort(terms) + + local node = root + for _, term in terms do + local child = node.children[term] + if child == nil then + child = createNode() + node.children[term] = child + end + + node = child + end + + return node + end + + local function findArchetypes(self, terms: Terms) + table.sort(terms) + + local archetypes = {} + local function collapse(node: Node) + table.insert(archetypes, node.archetype) + + for _, child in node.children do + collapse(child) + end + end + + collapse(findNode(self, terms)) + return archetypes + end + + local function ensureArchetype(self, terms: Terms): Archetype + local node = findNode(self, terms) + if node.archetype == nil then + node.archetype = { + ownedEntities = {}, + componentIds = terms, + componentIdToStorageIndex = {}, + storageIndexToComponentId = {}, + fields = {}, + } + + for index, componentId in terms do + node.archetype.componentIdToStorageIndex[componentId] = index + node.archetype.storageIndexToComponentId[index] = componentId + node.archetype.fields[index] = {} + end + end + + return node.archetype + end + + return table.freeze({ + findNode = findNode, + findArchetypes = findArchetypes, + ensureArchetype = ensureArchetype, + root = root, + }) +end + +return { + new = newArchetypeTree, +} From e9231d9802dabdc31e24ff794c4f6b3407d2e0d0 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 4 Oct 2024 15:18:30 -0400 Subject: [PATCH 70/87] implement findArchetypes --- lib/World.spec.luau | 8 +++++--- lib/archetypeTree.luau | 28 ++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 97dc2f81..437fd2b5 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -145,11 +145,13 @@ return function() describe("immediate", function() itFOCUS("test", function() local world = World.new() - local A, B = component(), component() - world:spawn(A({ a = true }), B({ b = true })) + local A, B, C = component(), component(), component() + world:spawn(A({ a = true }), B({ b = true }), C({ c = true, deep = true })) world:spawn(A({ a = true })) + world:spawn(C({ c = true, top = true })) + world:spawn(B({ b = true }), C({ c = true, nested = true })) - for id, a in world:query(A) do + for id, a in world:query(C) do print(id, a) end diff --git a/lib/archetypeTree.luau b/lib/archetypeTree.luau index 34d88cf3..684a1f16 100644 --- a/lib/archetypeTree.luau +++ b/lib/archetypeTree.luau @@ -50,19 +50,35 @@ local function newArchetypeTree() return node end - local function findArchetypes(self, terms: Terms) + local function findArchetypes(_, terms: Terms) table.sort(terms) local archetypes = {} - local function collapse(node: Node) - table.insert(archetypes, node.archetype) + local function check(node: Node, terms: Terms) + if #terms == 0 then + if node.archetype then + table.insert(archetypes, node.archetype) + end + end - for _, child in node.children do - collapse(child) + for componentId, child in node.children do + local head = terms[1] + if head then + if componentId < head then + check(child, terms) + elseif componentId == head then + local newTerms = table.clone(terms) + table.remove(newTerms, 1) + + check(child, newTerms) + end + else + check(child, terms) + end end end - collapse(findNode(self, terms)) + check(root, terms) return archetypes end From 82b2cb1d83fd4119926fbe8872a0e0c2758c7565 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 4 Oct 2024 15:45:20 -0400 Subject: [PATCH 71/87] update benchmark --- benchmarks/stress.bench.luau | 19 ++++++++++--------- lib/World.spec.luau | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/benchmarks/stress.bench.luau b/benchmarks/stress.bench.luau index 6e85a492..8de32489 100644 --- a/benchmarks/stress.bench.luau +++ b/benchmarks/stress.bench.luau @@ -16,26 +16,26 @@ local function flip() return math.random() > 0.5 end -for i = 1, 50_000 do +for i = 1, 10_000 do local id = i world:spawnAt(id) pinnedWorld:spawnAt(id) if flip() then - world:insert(id, A()) - pinnedWorld:insert(id, pinnedA()) + world:insert(id, A({ a = true })) + pinnedWorld:insert(id, pinnedA({ a = true })) end if flip() then - world:insert(id, B()) - pinnedWorld:insert(id, pinnedB()) + world:insert(id, B({ b = true })) + pinnedWorld:insert(id, pinnedB({ b = true })) end if flip() then - world:insert(id, C()) - pinnedWorld:insert(id, pinnedC()) + world:insert(id, C({ c = true })) + pinnedWorld:insert(id, pinnedC({ c = true })) end if flip() then - world:insert(id, D()) - pinnedWorld:insert(id, pinnedD()) + world:insert(id, D({ d = true })) + pinnedWorld:insert(id, pinnedD({ d = true })) end end @@ -57,6 +57,7 @@ return { local count = 0 for _ in world:query(A, B) do count += 1 + --print("entt", a, b) end --print("new:", count) diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 437fd2b5..97f9dee5 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -151,7 +151,7 @@ return function() world:spawn(C({ c = true, top = true })) world:spawn(B({ b = true }), C({ c = true, nested = true })) - for id, a in world:query(C) do + for id, a in world:query(C, B):without(A) do print(id, a) end From eccc15bc750151396bd63c77e3b8ae869e31032d Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 4 Oct 2024 15:58:15 -0400 Subject: [PATCH 72/87] make tests pass --- lib/World.luau | 67 ++++++++++-------------------------------- lib/World.spec.luau | 4 +-- lib/archetypeTree.luau | 3 +- 3 files changed, 19 insertions(+), 55 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index f16b51da..6d415a82 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -1,14 +1,10 @@ --!strict local Component = require(script.Parent.component) -local archetypeModule = require(script.Parent.archetype) local archetypeTree = require(script.Parent.archetypeTree) local topoRuntime = require(script.Parent.topoRuntime) local assertValidComponentInstances = Component.assertValidComponentInstances local assertValidComponent = Component.assertValidComponent -local archetypeOf = archetypeModule.archetypeOf -local negateArchetypeOf = archetypeModule.negateArchetypeOf -local areArchetypesCompatible = archetypeModule.areArchetypesCompatible type EntityId = number type DenseEntityId = number @@ -71,48 +67,16 @@ type Command = DespawnCommand | InsertCommand | RemoveCommand | ReplaceCommand local World = {} World.__index = World -local function createArchetype(world: World, componentIds: { ComponentId }): Archetype - local componentIdToStorageIndex, storageIndexToComponentId = {}, {} - local length = #componentIds - local storage = table.create(length) - - local indexInArchetypes = #world.archetypes + 1 - local archetype: Archetype = { - ownedEntities = {}, - - componentIds = componentIds, - - componentIdToStorageIndex = componentIdToStorageIndex, - storageIndexToComponentId = storageIndexToComponentId, - storage = storage, - } - - for index, componentId in componentIds do - local associatedArchetypes = world.componentToArchetypes[componentId] or ({} :: any) - associatedArchetypes[indexInArchetypes] = index - world.componentToArchetypes[componentId] = associatedArchetypes - - componentIdToStorageIndex[componentId] = index - storageIndexToComponentId[index] = componentId - - storage[index] = {} - end - - table.insert(world.archetypes, archetype) - world.archetypeIdToArchetype[archetypeId] = archetype - return archetype -end - --[=[ Creates a new World. ]=] function World.new() local self = setmetatable({ - archetypeTree = archetypeTree.new(), + archetypes = archetypeTree.new(), + entities = {} :: Entities, componentIdToComponent = {}, componentToArchetypes = {} :: ComponentToArchetypes, - archetypes = {} :: Archetypes, archetypeIdToArchetype = {} :: { [string]: Archetype? }, -- Map from archetype string --> entity ID --> entity data @@ -146,7 +110,7 @@ function World.new() _changedStorage = {}, }, World) - self.archetypeTree:ensureArchetype({}) + self.rootArchetype = self.archetypes:ensureArchetype({}) --print(self.archetypeTree:findNode({ 1, 2, 3 })) --print(self.archetypeTree) return self @@ -196,7 +160,7 @@ function World.__iter(world: World) local componentIdToComponent = world.componentIdToComponent local archetype = entityRecord.archetype local componentInstances = {} - for index, componentStorage in archetype.storage do + for index, componentStorage in archetype.fields do componentInstances[componentIdToComponent[archetype.storageIndexToComponentId[index]]] = componentStorage[entityRecord.indexInArchetype] end @@ -206,23 +170,22 @@ function World.__iter(world: World) end local function ensureArchetype(world: World, componentIds: { number }) - return world.archetypeTree:ensureArchetype(componentIds) --world.archetypeIdToArchetype[archetypeId] or createArchetype(world, componentIds) + return world.archetypes:ensureArchetype(componentIds) end local function ensureRecord(world: World, entityId: number): EntityRecord local entityRecord = world.entities[entityId] if entityRecord == nil then - local root = world.archetypeTree.root + local root = world.rootArchetype entityRecord = { - archetype = root.archetype, - indexInArchetype = #root.archetype.ownedEntities + 1, + archetype = root, + indexInArchetype = #root.ownedEntities + 1, } - table.insert(root.archetype.ownedEntities, entityId) + table.insert(root.ownedEntities, entityId) world.entities[entityId] = entityRecord end - --print("record", entityRecord) return entityRecord :: EntityRecord end @@ -282,7 +245,7 @@ local function executeDespawn(world: World, despawnCommand: DespawnCommand) local archetype = entityRecord.archetype -- Track changes - for _, componentStorage in archetype.storage do + for _, componentStorage in archetype.fields do local componentInstance = componentStorage[entityRecord.indexInArchetype] local component = getmetatable(componentInstance :: any) world:_trackChanged(component, entityId, componentInstance, nil) @@ -367,7 +330,7 @@ local function executeReplace(world: World, replaceCommand: ReplaceCommand) end -- Track removed - for index, componentStorage in oldArchetype.storage do + for index, componentStorage in oldArchetype.fields do local componentId = oldArchetype.storageIndexToComponentId[index] if componentIdMap[componentId] == nil then local component = world.componentIdToComponent[componentId] @@ -395,7 +358,7 @@ local function executeRemove(world: World, removeCommand: RemoveCommand) local index = table.find(componentIds, componentId) if index then local componentInstance = - archetype.storage[archetype.componentIdToStorageIndex[componentId]][entityRecord.indexInArchetype] + archetype.fields[archetype.componentIdToStorageIndex[componentId]][entityRecord.indexInArchetype] world:_trackChanged(component, entityId, componentInstance, nil) table.remove(componentIds, index) @@ -622,7 +585,7 @@ function World.get(self: World, entityId, ...: Component) end -- Yes - componentInstances[i] = archetype.storage[storageIndex][entityRecord.indexInArchetype] + componentInstances[i] = archetype.fields[storageIndex][entityRecord.indexInArchetype] end return unpack(componentInstances, 1, length) @@ -1135,7 +1098,7 @@ function World.query(self: World, ...) end end - local possibleArchetypes = self.archetypeTree:findArchetypes(componentIds) + local possibleArchetypes = self.archetypes:findArchetypes(componentIds) local compatibleArchetypes = possibleArchetypes --print("possibleArchetypes:", possibleArchetypes) -- local possibleArchetypes @@ -1381,7 +1344,7 @@ function World.remove(self: World, id, ...: Component) local archetype = entityRecord.archetype for _, component in components do local componentId = #component - local storage = archetype.storage[archetype.componentIdToStorageIndex[componentId]] + local storage = archetype.fields[archetype.componentIdToStorageIndex[componentId]] table.insert(componentInstances, if storage then storage[entityRecord.indexInArchetype] else nil) end diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 97f9dee5..f80645e3 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -142,8 +142,8 @@ return function() end) end) - describe("immediate", function() - itFOCUS("test", function() + describeFOCUS("immediate", function() + it("test", function() local world = World.new() local A, B, C = component(), component(), component() world:spawn(A({ a = true }), B({ b = true }), C({ c = true, deep = true })) diff --git a/lib/archetypeTree.luau b/lib/archetypeTree.luau index 684a1f16..8fb42b35 100644 --- a/lib/archetypeTree.luau +++ b/lib/archetypeTree.luau @@ -50,7 +50,8 @@ local function newArchetypeTree() return node end - local function findArchetypes(_, terms: Terms) + local function findArchetypes(_, with: Terms) + local terms = table.clone(with) table.sort(terms) local archetypes = {} From 4609bf82fd30f0573dfbaceee48973e2f4c00aca Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 4 Oct 2024 16:02:30 -0400 Subject: [PATCH 73/87] add more components to stress test --- benchmarks/stress.bench.luau | 43 ++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/benchmarks/stress.bench.luau b/benchmarks/stress.bench.luau index 8de32489..cf968d45 100644 --- a/benchmarks/stress.bench.luau +++ b/benchmarks/stress.bench.luau @@ -8,9 +8,22 @@ local PinnedMatter = require(ReplicatedStorage.PinnedMatter) local world = Matter.World.new() local pinnedWorld = PinnedMatter.World.new() -local A, B, C, D = Matter.component(), Matter.component(), Matter.component(), Matter.component() -local pinnedA, pinnedB, pinnedC, pinnedD = - PinnedMatter.component(), PinnedMatter.component(), PinnedMatter.component(), PinnedMatter.component() +local A, B, C, D, E, F, G = + Matter.component(), + Matter.component(), + Matter.component(), + Matter.component(), + Matter.component(), + Matter.component(), + Matter.component() +local pinnedA, pinnedB, pinnedC, pinnedD, pinnedE, pinnedF, pinnedG = + PinnedMatter.component(), + PinnedMatter.component(), + PinnedMatter.component(), + PinnedMatter.component(), + PinnedMatter.component(), + PinnedMatter.component(), + PinnedMatter.component() local function flip() return math.random() > 0.5 @@ -37,6 +50,18 @@ for i = 1, 10_000 do world:insert(id, D({ d = true })) pinnedWorld:insert(id, pinnedD({ d = true })) end + if flip() then + world:insert(id, E({ e = true })) + pinnedWorld:insert(id, pinnedE({ e = true })) + end + if flip() then + world:insert(id, F({ f = true })) + pinnedWorld:insert(id, pinnedF({ f = true })) + end + if flip() then + world:insert(id, G({ g = true })) + pinnedWorld:insert(id, pinnedG({ g = true })) + end end return { @@ -46,17 +71,17 @@ return { Functions = { ["Old Matter"] = function() - local count = 0 - for _ in pinnedWorld:query(pinnedA, pinnedB) do - count += 1 + --local count = 0 + for _ in pinnedWorld:query(pinnedB, pinnedA) do + --count += 1 end --print("old:", count) end, ["New Matter"] = function() - local count = 0 - for _ in world:query(A, B) do - count += 1 + --local count = 0 + for _ in world:query(B, A) do + --count += 1 --print("entt", a, b) end From 0b461d66e869b03c73887dd62924e650818ca6b0 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 4 Oct 2024 16:57:05 -0400 Subject: [PATCH 74/87] improve query perf --- benchmarks/stress.bench.luau | 30 ++++++------ lib/World.luau | 90 ++++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 40 deletions(-) diff --git a/benchmarks/stress.bench.luau b/benchmarks/stress.bench.luau index cf968d45..a00367a7 100644 --- a/benchmarks/stress.bench.luau +++ b/benchmarks/stress.bench.luau @@ -29,38 +29,38 @@ local function flip() return math.random() > 0.5 end -for i = 1, 10_000 do +for i = 1, 50_000 do local id = i world:spawnAt(id) pinnedWorld:spawnAt(id) if flip() then - world:insert(id, A({ a = true })) - pinnedWorld:insert(id, pinnedA({ a = true })) + world:insert(id, A({ a = true, id = i })) + pinnedWorld:insert(id, pinnedA({ a = true, id = i })) end if flip() then - world:insert(id, B({ b = true })) - pinnedWorld:insert(id, pinnedB({ b = true })) + world:insert(id, B({ b = true, id = i })) + pinnedWorld:insert(id, pinnedB({ b = true, id = i })) end if flip() then - world:insert(id, C({ c = true })) - pinnedWorld:insert(id, pinnedC({ c = true })) + world:insert(id, C({ c = true, id = i })) + pinnedWorld:insert(id, pinnedC({ c = true, id = i })) end if flip() then - world:insert(id, D({ d = true })) - pinnedWorld:insert(id, pinnedD({ d = true })) + world:insert(id, D({ d = true, id = i })) + pinnedWorld:insert(id, pinnedD({ d = true, id = i })) end if flip() then - world:insert(id, E({ e = true })) - pinnedWorld:insert(id, pinnedE({ e = true })) + world:insert(id, E({ e = true, id = i })) + pinnedWorld:insert(id, pinnedE({ e = true, id = i })) end if flip() then - world:insert(id, F({ f = true })) - pinnedWorld:insert(id, pinnedF({ f = true })) + world:insert(id, F({ f = true, id = i })) + pinnedWorld:insert(id, pinnedF({ f = true, id = i })) end if flip() then - world:insert(id, G({ g = true })) - pinnedWorld:insert(id, pinnedG({ g = true })) + world:insert(id, G({ g = true, id = i })) + pinnedWorld:insert(id, pinnedG({ g = true, id = i })) end end diff --git a/lib/World.luau b/lib/World.luau index 6d415a82..77384589 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -639,10 +639,13 @@ local QueryResult = {} QueryResult.__index = QueryResult function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds: { number }) - local a, b, c, d, e, f = nil, nil, nil, nil, nil, nil + local A, B, C, D, E, F, G, H = unpack(componentIds) + local a, b, c, d, e, f, g, h = nil, nil, nil, nil, nil, nil, nil, nil + local currentEntityIndex = 1 local currentArchetypeIndex = 1 local currentArchetype = compatibleArchetypes[1] + local currentEntities = currentArchetype.ownedEntities local function cacheComponentStorages() if currentArchetype == nil then @@ -651,38 +654,55 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds local storage, componentIdToStorageIndex = currentArchetype.fields, currentArchetype.componentIdToStorageIndex if queryLength == 1 then - a = storage[componentIdToStorageIndex[componentIds[1]]] + a = storage[componentIdToStorageIndex[A]] elseif queryLength == 2 then - a = storage[componentIdToStorageIndex[componentIds[1]]] - b = storage[componentIdToStorageIndex[componentIds[2]]] + a = storage[componentIdToStorageIndex[A]] + b = storage[componentIdToStorageIndex[B]] elseif queryLength == 3 then - a = storage[componentIdToStorageIndex[componentIds[1]]] - b = storage[componentIdToStorageIndex[componentIds[2]]] - c = storage[componentIdToStorageIndex[componentIds[3]]] + a = storage[componentIdToStorageIndex[A]] + b = storage[componentIdToStorageIndex[B]] + c = storage[componentIdToStorageIndex[C]] elseif queryLength == 4 then - a = storage[componentIdToStorageIndex[componentIds[1]]] - b = storage[componentIdToStorageIndex[componentIds[2]]] - c = storage[componentIdToStorageIndex[componentIds[3]]] - d = storage[componentIdToStorageIndex[componentIds[4]]] + a = storage[componentIdToStorageIndex[A]] + b = storage[componentIdToStorageIndex[B]] + c = storage[componentIdToStorageIndex[C]] + d = storage[componentIdToStorageIndex[D]] elseif queryLength == 5 then - a = storage[componentIdToStorageIndex[componentIds[1]]] - b = storage[componentIdToStorageIndex[componentIds[2]]] - c = storage[componentIdToStorageIndex[componentIds[3]]] - d = storage[componentIdToStorageIndex[componentIds[4]]] - e = storage[componentIdToStorageIndex[componentIds[5]]] + a = storage[componentIdToStorageIndex[A]] + b = storage[componentIdToStorageIndex[B]] + c = storage[componentIdToStorageIndex[C]] + d = storage[componentIdToStorageIndex[D]] + e = storage[componentIdToStorageIndex[E]] elseif queryLength == 6 then - a = storage[componentIdToStorageIndex[componentIds[1]]] - b = storage[componentIdToStorageIndex[componentIds[2]]] - c = storage[componentIdToStorageIndex[componentIds[3]]] - d = storage[componentIdToStorageIndex[componentIds[4]]] - e = storage[componentIdToStorageIndex[componentIds[5]]] - f = storage[componentIdToStorageIndex[componentIds[6]]] + a = storage[componentIdToStorageIndex[A]] + b = storage[componentIdToStorageIndex[B]] + c = storage[componentIdToStorageIndex[C]] + d = storage[componentIdToStorageIndex[D]] + e = storage[componentIdToStorageIndex[E]] + f = storage[componentIdToStorageIndex[F]] + elseif queryLength == 7 then + a = storage[componentIdToStorageIndex[A]] + b = storage[componentIdToStorageIndex[B]] + c = storage[componentIdToStorageIndex[C]] + d = storage[componentIdToStorageIndex[D]] + e = storage[componentIdToStorageIndex[E]] + f = storage[componentIdToStorageIndex[F]] + g = storage[componentIdToStorageIndex[G]] + elseif queryLength == 8 then + a = storage[componentIdToStorageIndex[A]] + b = storage[componentIdToStorageIndex[B]] + c = storage[componentIdToStorageIndex[C]] + d = storage[componentIdToStorageIndex[D]] + e = storage[componentIdToStorageIndex[E]] + f = storage[componentIdToStorageIndex[F]] + g = storage[componentIdToStorageIndex[G]] + h = storage[componentIdToStorageIndex[H]] end end local entityId: number local function nextEntity(): any - entityId = currentArchetype.ownedEntities[currentEntityIndex] + entityId = currentEntities[currentEntityIndex] while entityId == nil do currentEntityIndex = 1 currentArchetypeIndex += 1 @@ -692,13 +712,14 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds end cacheComponentStorages() - entityId = currentArchetype.ownedEntities[currentEntityIndex] + currentEntities = currentArchetype.ownedEntities + entityId = currentEntities[currentEntityIndex] end local entityIndex = currentEntityIndex currentEntityIndex += 1 - local entityId = currentArchetype.ownedEntities[entityIndex] + local entityId = currentEntities[entityIndex] if queryLength == 1 then return entityId, a[entityIndex] elseif queryLength == 2 then @@ -717,6 +738,25 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds d[entityIndex], e[entityIndex], f[entityIndex] + elseif queryLength == 7 then + return entityId, + a[entityIndex], + b[entityIndex], + c[entityIndex], + d[entityIndex], + e[entityIndex], + f[entityIndex], + g[entityIndex] + elseif queryLength == 8 then + return entityId, + a[entityIndex], + b[entityIndex], + c[entityIndex], + d[entityIndex], + e[entityIndex], + f[entityIndex], + g[entityIndex], + h[entityIndex] else error("Unimplemented Query Length") end From 9a61bed3c22097c5ebe9c8c9f4d2f4beebc71621 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 4 Oct 2024 18:59:06 -0400 Subject: [PATCH 75/87] fix typo --- lib/World.luau | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 77384589..09ff67ca 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -322,7 +322,7 @@ local function executeReplace(world: World, replaceCommand: ReplaceCommand) world:_trackChanged( component, entityId, - if storageIndex then oldArchetype.storage[storageIndex][entityRecord.indexInArchetype] else nil, + if storageIndex then oldArchetype.fields[storageIndex][entityRecord.indexInArchetype] else nil, componentInstance ) @@ -698,6 +698,8 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds g = storage[componentIdToStorageIndex[G]] h = storage[componentIdToStorageIndex[H]] end + + -- For anything longer, we do not cache. end local entityId: number @@ -758,7 +760,13 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds g[entityIndex], h[entityIndex] else - error("Unimplemented Query Length") + local output = table.create(queryLength) + for index, componentId in componentIds do + output[index] = + currentArchetype.fields[currentArchetype.componentToStorageIndex[componentId]][entityIndex] + end + + return unpack(output, 1, queryLength) end end @@ -795,6 +803,8 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds end currentArchetype = compatibleArchetypes[1] + currentEntities = currentArchetype.ownedEntities + cacheComponentStorages() return query end @@ -829,11 +839,14 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds return entities end + local function view() end + cacheComponentStorages() return setmetatable({ next = nextEntity, without = without, snapshot = snapshot, + view = view, }, { __iter = iter, __call = nextEntity, @@ -1134,7 +1147,7 @@ function World.query(self: World, ...) else componentIds = table.create(queryLength) for i = 1, queryLength do - componentIds[i] = select(i, ...) + componentIds[i] = #select(i, ...) end end From a7f8ecd86263bd5b8608607cd4185785bd3bd0f3 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 5 Oct 2024 14:12:29 -0400 Subject: [PATCH 76/87] add archetype deletion --- lib/World.luau | 53 ++++++++++++++++++++++++++++++++++++++---- lib/World.spec.luau | 26 +++++++++++++-------- lib/archetypeTree.luau | 40 +++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 15 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index 09ff67ca..75fddfdb 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -760,13 +760,15 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds g[entityIndex], h[entityIndex] else - local output = table.create(queryLength) + local output = table.create(queryLength + 1) + output[1] = entityIndex + for index, componentId in componentIds do - output[index] = - currentArchetype.fields[currentArchetype.componentToStorageIndex[componentId]][entityIndex] + output[index + 1] = + currentArchetype.fields[currentArchetype.componentIdToStorageIndex[componentId]][entityIndex] end - return unpack(output, 1, queryLength) + return unpack(output, 1, queryLength + 1) end end @@ -839,7 +841,48 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds return entities end - local function view() end + local function view() + local entities = {} + while true do + local entry = table.pack(nextEntity()) + if entry.n == 1 then + break + end + + entities[entry[1]] = table.move(entry, 2, #entry, 1, {}) + end + + local function get(_, entityId) + local components = entities[entityId] + if components == nil then + return nil + end + + return unpack(components, 1, #components) + end + + local function contains(_, entityId) + return entities[entityId] ~= nil + end + + return setmetatable({ + get = get, + contains = contains, + }, { + __iter = function() + local index = 0 + return function() + index += 1 + local entity = entities[index] + if not entity then + return + end + + return index, unpack(entity, 1, #entity) + end + end, + }) + end cacheComponentStorages() return setmetatable({ diff --git a/lib/World.spec.luau b/lib/World.spec.luau index f80645e3..b1fc4c57 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -142,27 +142,32 @@ return function() end) end) - describeFOCUS("immediate", function() + describe("immediate", function() it("test", function() local world = World.new() - local A, B, C = component(), component(), component() - world:spawn(A({ a = true }), B({ b = true }), C({ c = true, deep = true })) - world:spawn(A({ a = true })) - world:spawn(C({ c = true, top = true })) - world:spawn(B({ b = true }), C({ c = true, nested = true })) - - for id, a in world:query(C, B):without(A) do + local A, B, C, D = component(), component(), component(), component() + -- world:spawn(A({ a = true }), B({ b = true }), C({ c = true, deep = true })) + -- world:spawn(A({ a = true })) + -- world:spawn(C({ c = true, top = true })) + -- world:spawn(B({ b = true }), C({ c = true, nested = true })) + + world:spawnAt(15) + world:insert(15, B({})) + world:remove(15, D({})) + world:replace(15, B({}), A({})) + for id, a in world:query(A):without(C) do print(id, a) end - print("Done:", world.archetypeTree) + print("Done:", world.archetypes) end) it("should work", function() local world = World.new() - local A, B = component(), component() + local A, B, C = component(), component(), component() local id = world:spawn(A({ a = true })) world:replace(id, B({ b = true })) + world:insert(id, C({ c = true })) print(world.archetypes) -- world:spawnAt(10) @@ -779,6 +784,7 @@ return function() local viewA = world:query(ComponentA):view() local viewB = world:query(ComponentB):view() + print(viewA) expect(viewA:contains(entityA)).to.equal(true) expect(viewA:contains(entityB)).to.equal(false) expect(viewB:contains(entityB)).to.equal(true) diff --git a/lib/archetypeTree.luau b/lib/archetypeTree.luau index 8fb42b35..355286df 100644 --- a/lib/archetypeTree.luau +++ b/lib/archetypeTree.luau @@ -104,11 +104,51 @@ local function newArchetypeTree() return node.archetype end + local function count() + local archetypes = {} + local function check(node, str) + if node.archetype then + archetypes[str] = (archetypes[str] or 0) + #node.archetype.ownedEntities + end + + for componentId, child in node.children do + check(child, str .. "_" .. componentId) + end + end + + check(root, "") + return archetypes + end + + local function cleanup() + -- TODO: + -- dont potentially delete root node + + local function check(node) + for componentId, child in node.children do + local archetype, children = child.archetype, child.children + if archetype == nil or #archetype.ownedEntities == 0 then + if next(children) == nil then + node.children[componentId] = nil + else + child.archetype = nil + end + end + + check(child) + end + end + + check(root) + end + return table.freeze({ findNode = findNode, findArchetypes = findArchetypes, ensureArchetype = ensureArchetype, root = root, + count = count, + cleanup = cleanup, }) end From 11319a280695b6f67dfc7a5db7bcadc944dcd8f8 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 20 Oct 2024 14:59:19 -0400 Subject: [PATCH 77/87] Add cleanup to tree --- benchmarks/next.bench.luau | 38 +++++++++++ benchmarks/query.bench.luau | 3 + benchmarks/stress.bench.luau | 28 +++++++- benchmarks/without.bench.luau | 90 ++++++++++++++++++++++++++ lib/World.luau | 4 +- lib/World.spec.luau | 10 +-- lib/archetypeTree.luau | 116 ++++++++++++++++++++++++++-------- 7 files changed, 252 insertions(+), 37 deletions(-) create mode 100644 benchmarks/next.bench.luau create mode 100644 benchmarks/without.bench.luau diff --git a/benchmarks/next.bench.luau b/benchmarks/next.bench.luau new file mode 100644 index 00000000..c4941adc --- /dev/null +++ b/benchmarks/next.bench.luau @@ -0,0 +1,38 @@ +--!optimize 2 +--!native +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Matter = require(ReplicatedStorage.Matter) +local PinnedMatter = require(ReplicatedStorage.PinnedMatter) + +local world = Matter.World.new() +local pinnedWorld = PinnedMatter.World.new() + +local A, B = Matter.component(), Matter.component() +local pinnedA, pinnedB = PinnedMatter.component(), PinnedMatter.component() + +for i = 1, 10_000 do + world:spawnAt(i, A({}), B({})) + pinnedWorld:spawnAt(i, pinnedA({}), pinnedB({})) +end + +return { + ParameterGenerator = function() + return + end, + + Functions = { + ["New Matter"] = function() + local query = world:query(A, B) + for _ = 1, 1_000 do + query:next() + end + end, + ["Old Matter"] = function() + local query = pinnedWorld:query(pinnedA, pinnedB) + for _ = 1, 1_000 do + query:next() + end + end, + }, +} diff --git a/benchmarks/query.bench.luau b/benchmarks/query.bench.luau index d1ae6d86..43733d3c 100644 --- a/benchmarks/query.bench.luau +++ b/benchmarks/query.bench.luau @@ -16,6 +16,7 @@ for i = 1, 10_000 do pinnedWorld:spawnAt(i, pinnedA({}), pinnedB({})) end +print(world.archetypes, world.archetypes:count()) return { ParameterGenerator = function() return @@ -27,6 +28,8 @@ return { for _ in world:query(A, B) do count += 1 end + + print("new", count) end, ["Old Matter"] = function() local count = 0 diff --git a/benchmarks/stress.bench.luau b/benchmarks/stress.bench.luau index a00367a7..583ccabe 100644 --- a/benchmarks/stress.bench.luau +++ b/benchmarks/stress.bench.luau @@ -29,40 +29,66 @@ local function flip() return math.random() > 0.5 end -for i = 1, 50_000 do +local archetypes = {} +for i = 1, 1_000 do local id = i world:spawnAt(id) pinnedWorld:spawnAt(id) + local str = "" if flip() then world:insert(id, A({ a = true, id = i })) pinnedWorld:insert(id, pinnedA({ a = true, id = i })) + str ..= "A_" end if flip() then world:insert(id, B({ b = true, id = i })) pinnedWorld:insert(id, pinnedB({ b = true, id = i })) + str ..= "B_" end if flip() then world:insert(id, C({ c = true, id = i })) pinnedWorld:insert(id, pinnedC({ c = true, id = i })) + str ..= "C_" end if flip() then world:insert(id, D({ d = true, id = i })) pinnedWorld:insert(id, pinnedD({ d = true, id = i })) + str ..= "D_" end if flip() then world:insert(id, E({ e = true, id = i })) pinnedWorld:insert(id, pinnedE({ e = true, id = i })) + str ..= "E_" end if flip() then world:insert(id, F({ f = true, id = i })) pinnedWorld:insert(id, pinnedF({ f = true, id = i })) + str ..= "F_" end if flip() then world:insert(id, G({ g = true, id = i })) pinnedWorld:insert(id, pinnedG({ g = true, id = i })) + str ..= "G_" end + + archetypes[str] = (archetypes[str] or 0) + 1 +end + +local total = 0 +for _, count in archetypes do + total += count +end + +print("archetypes:", archetypes) +print(total, "different archetypes") + +print("tree:", world.archetypes:count()) +local total = 0 +for _, count in world.archetypes:count() do + total += count end +print(total, "archetypes") return { ParameterGenerator = function() diff --git a/benchmarks/without.bench.luau b/benchmarks/without.bench.luau new file mode 100644 index 00000000..c2f750a3 --- /dev/null +++ b/benchmarks/without.bench.luau @@ -0,0 +1,90 @@ +--!optimize 2 +--!native +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Matter = require(ReplicatedStorage.Matter) +local PinnedMatter = require(ReplicatedStorage.PinnedMatter) + +local world = Matter.World.new() +local pinnedWorld = PinnedMatter.World.new() + +local A, B, C, D, E, F, G = + Matter.component(), + Matter.component(), + Matter.component(), + Matter.component(), + Matter.component(), + Matter.component(), + Matter.component() +local pinnedA, pinnedB, pinnedC, pinnedD, pinnedE, pinnedF, pinnedG = + PinnedMatter.component(), + PinnedMatter.component(), + PinnedMatter.component(), + PinnedMatter.component(), + PinnedMatter.component(), + PinnedMatter.component(), + PinnedMatter.component() + +local function flip() + return math.random() > 0.5 +end + +for i = 1, 50_000 do + local id = i + world:spawnAt(id) + pinnedWorld:spawnAt(id) + + if flip() then + world:insert(id, A({ a = true, id = i })) + pinnedWorld:insert(id, pinnedA({ a = true, id = i })) + end + if flip() then + world:insert(id, B({ b = true, id = i })) + pinnedWorld:insert(id, pinnedB({ b = true, id = i })) + end + if flip() then + world:insert(id, C({ c = true, id = i })) + pinnedWorld:insert(id, pinnedC({ c = true, id = i })) + end + if flip() then + world:insert(id, D({ d = true, id = i })) + pinnedWorld:insert(id, pinnedD({ d = true, id = i })) + end + if flip() then + world:insert(id, E({ e = true, id = i })) + pinnedWorld:insert(id, pinnedE({ e = true, id = i })) + end + if flip() then + world:insert(id, F({ f = true, id = i })) + pinnedWorld:insert(id, pinnedF({ f = true, id = i })) + end + if flip() then + world:insert(id, G({ g = true, id = i })) + pinnedWorld:insert(id, pinnedG({ g = true, id = i })) + end +end + +return { + ParameterGenerator = function() + return + end, + + Functions = { + ["Old Matter"] = function() + local count = 0 + for _ in pinnedWorld:query(pinnedB):without(pinnedC) do + count += 1 + end + + --print("old:", count) + end, + ["New Matter"] = function() + local count = 0 + for _ in world:query(B):without(C) do + count += 1 + end + + print("new:", count) + end, + }, +} diff --git a/lib/World.luau b/lib/World.luau index 75fddfdb..e800e278 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -110,9 +110,7 @@ function World.new() _changedStorage = {}, }, World) - self.rootArchetype = self.archetypes:ensureArchetype({}) - --print(self.archetypeTree:findNode({ 1, 2, 3 })) - --print(self.archetypeTree) + self.rootArchetype = assert(self.archetypes.root.archetype, "Missing root archetype") return self end export type World = typeof(World.new()) diff --git a/lib/World.spec.luau b/lib/World.spec.luau index b1fc4c57..7e05e85e 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -151,11 +151,11 @@ return function() -- world:spawn(C({ c = true, top = true })) -- world:spawn(B({ b = true }), C({ c = true, nested = true })) - world:spawnAt(15) - world:insert(15, B({})) - world:remove(15, D({})) - world:replace(15, B({}), A({})) - for id, a in world:query(A):without(C) do + world:spawn(A({ a = true }), B({ b = true })) + -- world:insert(15, B({})) + -- world:remove(15, D({})) + -- world:replace(15, B({}), A({})) + for id, a in world:query(B, C) do print(id, a) end diff --git a/lib/archetypeTree.luau b/lib/archetypeTree.luau index 355286df..f5c50b73 100644 --- a/lib/archetypeTree.luau +++ b/lib/archetypeTree.luau @@ -27,21 +27,29 @@ local function newArchetypeTree() local function createNode(): Node local node: Node = { children = {}, + numChildren = 0, } return node end local root = createNode() - local function findNode(_, terms: Terms): Node - table.sort(terms) + root.archetype = { + ownedEntities = {}, + componentIds = {}, + componentIdToStorageIndex = {}, + storageIndexToComponentId = {}, + fields = {}, + } :: Archetype + local function findNode(_, terms: Terms): Node local node = root for _, term in terms do local child = node.children[term] if child == nil then child = createNode() node.children[term] = child + node.numChildren += 1 end node = child @@ -54,54 +62,105 @@ local function newArchetypeTree() local terms = table.clone(with) table.sort(terms) + --print("finding:", terms) + --print(" ", root) local archetypes = {} - local function check(node: Node, terms: Terms) - if #terms == 0 then - if node.archetype then + local count = 0 + local function check(node: Node, ...: ComponentId) + -- print("check:", node, ...) + count += 1 + if select("#", ...) == 0 then + if node.archetype and #node.archetype.ownedEntities > 0 then table.insert(archetypes, node.archetype) end end for componentId, child in node.children do - local head = terms[1] + local head = select(1, ...) + -- print(" child", componentId, child, head) if head then if componentId < head then - check(child, terms) + check(child, ...) elseif componentId == head then - local newTerms = table.clone(terms) - table.remove(newTerms, 1) - - check(child, newTerms) + check(child, select(2, ...)) end else - check(child, terms) + check(child, ...) end end end - check(root, terms) + -- Make sure that a node exists for each term + -- If it doesn't then we know that the archetypes will be empty + local smallestHeight, smallestNode, smallestNodeIndex = math.huge, nil, -1 + for index, componentId in terms do + local node = root.children[componentId] + if node == nil then + return {} + end + + if node.numChildren < smallestHeight then + smallestNode = node + smallestNodeIndex = index + smallestHeight = node.numChildren + end + end + + if smallestNode == nil then + return {} + end + + -- TODO: + -- Only traverse path with smallest num children + local newTerms = table.clone(terms) + table.remove(newTerms, smallestNodeIndex) + + -- print("terms", terms, smallestNode, "newTerms", newTerms) + check(smallestNode, unpack(newTerms, 1, #newTerms)) + -- if terms[1] == 152 then -- terms[1] == 5 and terms[2] == 150 then + -- print("jumps:", count, newTerms, smallestNode) + -- end + return archetypes end local function ensureArchetype(self, terms: Terms): Archetype - local node = findNode(self, terms) - if node.archetype == nil then - node.archetype = { - ownedEntities = {}, - componentIds = terms, - componentIdToStorageIndex = {}, - storageIndexToComponentId = {}, - fields = {}, - } - - for index, componentId in terms do - node.archetype.componentIdToStorageIndex[componentId] = index - node.archetype.storageIndexToComponentId[index] = componentId - node.archetype.fields[index] = {} + table.sort(terms) + + --print("ensure:", terms) + local archetype + for skipIndex = 1, #terms do + local newTerms = table.clone(terms) + table.remove(newTerms, skipIndex) + table.insert(newTerms, 1, terms[skipIndex]) + + --print(" newTerms", skipIndex, newTerms) + local node = findNode(self, newTerms) + if node.archetype then + archetype = node.archetype + else + if archetype == nil then + archetype = { + ownedEntities = {}, + componentIds = terms, + componentIdToStorageIndex = {}, + storageIndexToComponentId = {}, + fields = {}, + } + + for index, componentId in terms do + archetype.componentIdToStorageIndex[componentId] = index + archetype.storageIndexToComponentId[index] = componentId + archetype.fields[index] = {} + end + end + + node.archetype = archetype end end - return node.archetype + --print(" ret:", archetype) + return archetype end local function count() @@ -130,6 +189,7 @@ local function newArchetypeTree() if archetype == nil or #archetype.ownedEntities == 0 then if next(children) == nil then node.children[componentId] = nil + node.numChildren -= 1 else child.archetype = nil end From b5d68f9db523ffbdcdb932e9e8f264e169954890 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 20 Oct 2024 15:57:33 -0400 Subject: [PATCH 78/87] Remove archetype tree --- lib/World.luau | 667 +++++++++++++++------------------------- lib/World.spec.luau | 53 ---- lib/archetype.luau | 46 ++- lib/archetype.spec.luau | 29 +- lib/archetypeTree.luau | 217 ------------- 5 files changed, 298 insertions(+), 714 deletions(-) delete mode 100644 lib/archetypeTree.luau diff --git a/lib/World.luau b/lib/World.luau index e800e278..def64a45 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -1,20 +1,21 @@ --!strict +--!native +--!optimize 2 + +local Archetype = require(script.Parent.Archetype) local Component = require(script.Parent.component) -local archetypeTree = require(script.Parent.archetypeTree) local topoRuntime = require(script.Parent.topoRuntime) local assertValidComponentInstances = Component.assertValidComponentInstances local assertValidComponent = Component.assertValidComponent -type EntityId = number -type DenseEntityId = number -type ComponentId = number +type EntityId = Archetype.EntityId +type ComponentId = Archetype.ComponentId -type Component = { [any]: any } -type ComponentInstance = { [any]: any } +type Component = Archetype.Component +type ComponentInstance = Archetype.ComponentInstance -type ArchetypeId = string -type Archetype = archetypeTree.Archetype +type Archetype = Archetype.Archetype type EntityRecord = { indexInArchetype: number, @@ -24,9 +25,6 @@ type EntityRecord = { -- Find archetype for entity type Entities = { [EntityId]: EntityRecord? } --- Find archetypes containing component -type ComponentToArchetypes = { [ComponentId]: { number } } - -- Find archetype from all components type Archetypes = { Archetype } @@ -67,38 +65,51 @@ type Command = DespawnCommand | InsertCommand | RemoveCommand | ReplaceCommand local World = {} World.__index = World +local function ensureArchetype(world: World, componentIds: { ComponentId }) + local archetypeId = Archetype.hash(componentIds) + local existingArchetype = world.hashToArchetype[archetypeId] + if existingArchetype then + return existingArchetype + end + + -- Create new archetype + local archetype, archetypeId = Archetype.new(componentIds) + world.hashToArchetype[archetypeId] = archetype + table.insert(world.archetypes, archetype) + + for _, componentId in componentIds do + local associatedArchetypes = world.componentToArchetypes[componentId] + if associatedArchetypes == nil then + associatedArchetypes = {} + world.componentToArchetypes[componentId] = associatedArchetypes + end + + table.insert(associatedArchetypes, archetype) + end + + return archetype +end + --[=[ Creates a new World. ]=] function World.new() local self = setmetatable({ - archetypes = archetypeTree.new(), - - entities = {} :: Entities, - componentIdToComponent = {}, - componentToArchetypes = {} :: ComponentToArchetypes, - archetypeIdToArchetype = {} :: { [string]: Archetype? }, + archetypes = {} :: { Archetype }, + allEntities = {} :: { [EntityId]: EntityRecord? }, - -- Map from archetype string --> entity ID --> entity data - storage = {}, + componentIdToComponent = {} :: { [ComponentId]: Component }, + componentToArchetypes = {} :: { [ComponentId]: { Archetype } }, + hashToArchetype = {} :: { [string]: Archetype? }, + -- Is the world buffering commands? deferring = false, - commands = {} :: { Command }, - markedForDeletion = {}, - - -- Map from entity ID -> archetype string - _entityArchetypes = {}, - - -- Cache of the component metatables on each entity. Used for generating archetype. - -- Map of entity ID -> array - _entityMetatablesCache = {}, - -- Cache of what query archetypes are compatible with what component archetypes - _queryCache = {}, + -- The commands that are queued + commands = {} :: { Command }, - -- Cache of what entity archetypes have ever existed in the game. This is used for knowing - -- when to update the queryCache. - _entityArchetypeCache = {}, + -- Entities marked for deletion by commands, but not deleted yet + markedForDeletion = {}, -- The next ID that will be assigned with World:spawn _nextId = 1, @@ -110,25 +121,11 @@ function World.new() _changedStorage = {}, }, World) - self.rootArchetype = assert(self.archetypes.root.archetype, "Missing root archetype") + self.rootArchetype = ensureArchetype(self, {}) return self end -export type World = typeof(World.new()) - -function World:_getEntity(id) - local archetype = self._entityArchetypes[id] - return self.storage[archetype][id] -end - -function World:_next(last) - local entityId, archetype = next(self._entityArchetypes, last) - if entityId == nil then - return nil - end - - return entityId, self.storage[archetype][entityId] -end +export type World = typeof(World.new()) --[=[ Iterates over all entities in this World. Iteration returns entity ID followed by a dictionary mapping @@ -148,7 +145,7 @@ end function World.__iter(world: World) local lastEntityId = nil return function(): (number?, ...any) - local entityId, entityRecord = next(world.entities, lastEntityId) + local entityId, entityRecord = next(world.allEntities, lastEntityId) if entityId == nil or entityRecord == nil then return nil end @@ -159,7 +156,7 @@ function World.__iter(world: World) local archetype = entityRecord.archetype local componentInstances = {} for index, componentStorage in archetype.fields do - componentInstances[componentIdToComponent[archetype.storageIndexToComponentId[index]]] = + componentInstances[componentIdToComponent[archetype.indexToId[index]]] = componentStorage[entityRecord.indexInArchetype] end @@ -167,21 +164,17 @@ function World.__iter(world: World) end end -local function ensureArchetype(world: World, componentIds: { number }) - return world.archetypes:ensureArchetype(componentIds) -end - local function ensureRecord(world: World, entityId: number): EntityRecord - local entityRecord = world.entities[entityId] + local entityRecord = world.allEntities[entityId] if entityRecord == nil then - local root = world.rootArchetype + local rootArchetype = world.rootArchetype entityRecord = { - archetype = root, - indexInArchetype = #root.ownedEntities + 1, + archetype = rootArchetype, + indexInArchetype = #rootArchetype.entities + 1, } - table.insert(root.ownedEntities, entityId) - world.entities[entityId] = entityRecord + table.insert(rootArchetype.entities, entityId) + world.allEntities[entityId] = entityRecord end return entityRecord :: EntityRecord @@ -196,17 +189,16 @@ local function transitionArchetype( local oldArchetype = entityRecord.archetype local oldEntityIndex = entityRecord.indexInArchetype - --print("transition from", oldArchetype, "to", archetype) - -- Add entity to archetype's ownedEntities - local ownedEntities = archetype.ownedEntities - local entityIndex = #ownedEntities + 1 - ownedEntities[entityIndex] = entityId + -- Add entity to archetype's entities + local entities = archetype.entities + local entityIndex = #entities + 1 + entities[entityIndex] = entityId -- Move old storage to new storage if needed - local oldNumEntities = #oldArchetype.ownedEntities + local oldNumEntities = #oldArchetype.entities local wasLastEntity = oldNumEntities == oldEntityIndex for index, oldComponentStorage in oldArchetype.fields do - local componentStorage = archetype.fields[archetype.componentIdToStorageIndex[oldArchetype.componentIds[index]]] + local componentStorage = archetype.fields[archetype.idToIndex[oldArchetype.componentIds[index]]] -- Does the new storage contain this component? if componentStorage then @@ -223,12 +215,12 @@ local function transitionArchetype( -- Swap entity location marker if not wasLastEntity then - oldArchetype.ownedEntities[oldEntityIndex] = oldArchetype.ownedEntities[oldNumEntities]; - (world.entities[oldArchetype.ownedEntities[oldEntityIndex]] :: EntityRecord).indexInArchetype = oldEntityIndex + oldArchetype.entities[oldEntityIndex] = oldArchetype.entities[oldNumEntities]; + (world.allEntities[oldArchetype.entities[oldEntityIndex]] :: EntityRecord).indexInArchetype = oldEntityIndex end -- Remove from old archetype - oldArchetype.ownedEntities[oldNumEntities] = nil + oldArchetype.entities[oldNumEntities] = nil -- Mark entity as being in new archetype entityRecord.indexInArchetype = entityIndex @@ -252,8 +244,8 @@ local function executeDespawn(world: World, despawnCommand: DespawnCommand) -- TODO: -- Optimize remove so no cascades transitionArchetype(world, entityId, entityRecord, world.rootArchetype) - table.remove(world.rootArchetype.ownedEntities, entityRecord.indexInArchetype) - world.entities[entityId] = nil + table.remove(world.rootArchetype.entities, entityRecord.indexInArchetype) + world.allEntities[entityId] = nil world._size -= 1 end @@ -274,11 +266,11 @@ local function executeInsert(world: World, insertCommand: InsertCommand) local archetype: Archetype local entityIndex: number local oldComponentInstance: ComponentInstance? - if oldArchetype.componentIdToStorageIndex[componentId] == nil then + if oldArchetype.idToIndex[componentId] == nil then table.insert(componentIds, componentId) archetype = ensureArchetype(world, componentIds) entityIndex = transitionArchetype(world, entityId, entityRecord, archetype) - oldComponentInstance = archetype.fields[archetype.componentIdToStorageIndex[componentId]][entityIndex] + oldComponentInstance = archetype.fields[archetype.idToIndex[componentId]][entityIndex] -- FIXME: -- This shouldn't be in a hotpath, probably better in createArchetype @@ -286,10 +278,10 @@ local function executeInsert(world: World, insertCommand: InsertCommand) else archetype = oldArchetype entityIndex = entityRecord.indexInArchetype - oldComponentInstance = oldArchetype.fields[oldArchetype.componentIdToStorageIndex[componentId]][entityIndex] + oldComponentInstance = oldArchetype.fields[oldArchetype.idToIndex[componentId]][entityIndex] end - archetype.fields[archetype.componentIdToStorageIndex[componentId]][entityIndex] = componentInstance + archetype.fields[archetype.idToIndex[componentId]][entityIndex] = componentInstance world:_trackChanged(component, entityId, oldComponentInstance, componentInstance) oldArchetype = archetype @@ -316,7 +308,7 @@ local function executeReplace(world: World, replaceCommand: ReplaceCommand) local componentId = #component table.insert(componentIds, componentId) - local storageIndex = oldArchetype.componentIdToStorageIndex[componentId] + local storageIndex = oldArchetype.idToIndex[componentId] world:_trackChanged( component, entityId, @@ -329,7 +321,7 @@ local function executeReplace(world: World, replaceCommand: ReplaceCommand) -- Track removed for index, componentStorage in oldArchetype.fields do - local componentId = oldArchetype.storageIndexToComponentId[index] + local componentId = oldArchetype.indexToId[index] if componentIdMap[componentId] == nil then local component = world.componentIdToComponent[componentId] world:_trackChanged(component, entityId, componentStorage[entityRecord.indexInArchetype], nil) @@ -355,8 +347,7 @@ local function executeRemove(world: World, removeCommand: RemoveCommand) local componentId = #component local index = table.find(componentIds, componentId) if index then - local componentInstance = - archetype.fields[archetype.componentIdToStorageIndex[componentId]][entityRecord.indexInArchetype] + local componentInstance = archetype.fields[archetype.idToIndex[componentId]][entityRecord.indexInArchetype] world:_trackChanged(component, entityId, componentInstance, nil) table.remove(componentIds, index) @@ -471,7 +462,6 @@ function World:spawnAt(id: number, ...) end self.markedForDeletion[id] = nil - self._entityMetatablesCache[id] = {} ensureRecord(self, id) bufferCommand(self, { type = "insert", entityId = id, componentInstances = componentInstances }) @@ -551,7 +541,7 @@ end @return bool -- `true` if the entity exists ]=] function World.contains(self: typeof(World.new()), id) - return self.entities[id] ~= nil + return self.allEntities[id] ~= nil end --[=[ @@ -562,7 +552,7 @@ end @return ... -- Returns the component values in the same order they were passed in ]=] function World.get(self: World, entityId, ...: Component) - local entityRecord = self.entities[entityId] + local entityRecord = self.allEntities[entityId] if entityRecord == nil then error(ERROR_NO_ENTITY, 2) end @@ -571,13 +561,13 @@ function World.get(self: World, entityId, ...: Component) local componentInstances = table.create(length, nil) local archetype = entityRecord.archetype - local componentIdToStorageIndex = archetype.componentIdToStorageIndex + local idToIndex = archetype.idToIndex for i = 1, length do local component = select(i, ...) assertValidComponent(component, i) -- Does this component belong to the archetype that this entity is in? - local storageIndex = componentIdToStorageIndex[#component] + local storageIndex = idToIndex[#component] if storageIndex == nil then continue end @@ -636,71 +626,97 @@ local noopQuery = setmetatable({ local QueryResult = {} QueryResult.__index = QueryResult -function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds: { number }) +function QueryResult.new(compatibleArchetypes: { Archetype }, queryLength: number, componentIds: { number }) local A, B, C, D, E, F, G, H = unpack(componentIds) local a, b, c, d, e, f, g, h = nil, nil, nil, nil, nil, nil, nil, nil local currentEntityIndex = 1 local currentArchetypeIndex = 1 local currentArchetype = compatibleArchetypes[1] - local currentEntities = currentArchetype.ownedEntities + local currentEntities = currentArchetype.entities - local function cacheComponentStorages() + local function cacheFields() if currentArchetype == nil then return end - local storage, componentIdToStorageIndex = currentArchetype.fields, currentArchetype.componentIdToStorageIndex + local storage, idToIndex = currentArchetype.fields, currentArchetype.idToIndex if queryLength == 1 then - a = storage[componentIdToStorageIndex[A]] + a = storage[idToIndex[A]] elseif queryLength == 2 then - a = storage[componentIdToStorageIndex[A]] - b = storage[componentIdToStorageIndex[B]] + a = storage[idToIndex[A]] + b = storage[idToIndex[B]] elseif queryLength == 3 then - a = storage[componentIdToStorageIndex[A]] - b = storage[componentIdToStorageIndex[B]] - c = storage[componentIdToStorageIndex[C]] + a = storage[idToIndex[A]] + b = storage[idToIndex[B]] + c = storage[idToIndex[C]] elseif queryLength == 4 then - a = storage[componentIdToStorageIndex[A]] - b = storage[componentIdToStorageIndex[B]] - c = storage[componentIdToStorageIndex[C]] - d = storage[componentIdToStorageIndex[D]] + a = storage[idToIndex[A]] + b = storage[idToIndex[B]] + c = storage[idToIndex[C]] + d = storage[idToIndex[D]] elseif queryLength == 5 then - a = storage[componentIdToStorageIndex[A]] - b = storage[componentIdToStorageIndex[B]] - c = storage[componentIdToStorageIndex[C]] - d = storage[componentIdToStorageIndex[D]] - e = storage[componentIdToStorageIndex[E]] + a = storage[idToIndex[A]] + b = storage[idToIndex[B]] + c = storage[idToIndex[C]] + d = storage[idToIndex[D]] + e = storage[idToIndex[E]] elseif queryLength == 6 then - a = storage[componentIdToStorageIndex[A]] - b = storage[componentIdToStorageIndex[B]] - c = storage[componentIdToStorageIndex[C]] - d = storage[componentIdToStorageIndex[D]] - e = storage[componentIdToStorageIndex[E]] - f = storage[componentIdToStorageIndex[F]] + a = storage[idToIndex[A]] + b = storage[idToIndex[B]] + c = storage[idToIndex[C]] + d = storage[idToIndex[D]] + e = storage[idToIndex[E]] + f = storage[idToIndex[F]] elseif queryLength == 7 then - a = storage[componentIdToStorageIndex[A]] - b = storage[componentIdToStorageIndex[B]] - c = storage[componentIdToStorageIndex[C]] - d = storage[componentIdToStorageIndex[D]] - e = storage[componentIdToStorageIndex[E]] - f = storage[componentIdToStorageIndex[F]] - g = storage[componentIdToStorageIndex[G]] + a = storage[idToIndex[A]] + b = storage[idToIndex[B]] + c = storage[idToIndex[C]] + d = storage[idToIndex[D]] + e = storage[idToIndex[E]] + f = storage[idToIndex[F]] + g = storage[idToIndex[G]] elseif queryLength == 8 then - a = storage[componentIdToStorageIndex[A]] - b = storage[componentIdToStorageIndex[B]] - c = storage[componentIdToStorageIndex[C]] - d = storage[componentIdToStorageIndex[D]] - e = storage[componentIdToStorageIndex[E]] - f = storage[componentIdToStorageIndex[F]] - g = storage[componentIdToStorageIndex[G]] - h = storage[componentIdToStorageIndex[H]] + a = storage[idToIndex[A]] + b = storage[idToIndex[B]] + c = storage[idToIndex[C]] + d = storage[idToIndex[D]] + e = storage[idToIndex[E]] + f = storage[idToIndex[F]] + g = storage[idToIndex[G]] + h = storage[idToIndex[H]] end -- For anything longer, we do not cache. end local entityId: number + --[=[ + Returns the next set of values from the query result. Once all results have been returned, the + QueryResult is exhausted and is no longer useful. + + :::info + This function is equivalent to calling the QueryResult as a function. When used in a for loop, this is implicitly + done by the language itself. + ::: + + ```lua + -- Using world:query in this position will make Lua invoke the table as a function. This is conventional. + for id, enemy, charge, model in world:query(Enemy, Charge, Model) do + -- Do something + end + ``` + + If you wanted to iterate over the QueryResult without a for loop, it's recommended that you call `next` directly + instead of calling the QueryResult as a function. + ```lua + local id, enemy, charge, model = world:query(Enemy, Charge, Model):next() + local id, enemy, charge, model = world:query(Enemy, Charge, Model)() -- Possible, but unconventional + ``` + + @return id -- Entity ID + @return ...ComponentInstance -- The requested component values + ]=] local function nextEntity(): any entityId = currentEntities[currentEntityIndex] while entityId == nil do @@ -711,8 +727,8 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds return nil end - cacheComponentStorages() - currentEntities = currentArchetype.ownedEntities + cacheFields() + currentEntities = currentArchetype.entities entityId = currentEntities[currentEntityIndex] end @@ -758,15 +774,12 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds g[entityIndex], h[entityIndex] else - local output = table.create(queryLength + 1) - output[1] = entityIndex - + local output: { ComponentInstance } = table.create(queryLength + 1) for index, componentId in componentIds do - output[index + 1] = - currentArchetype.fields[currentArchetype.componentIdToStorageIndex[componentId]][entityIndex] + output[index] = currentArchetype.fields[currentArchetype.idToIndex[componentId]][entityIndex] end - return unpack(output, 1, queryLength + 1) + return entityId, unpack(output, 1, queryLength) end end @@ -774,6 +787,19 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds return nextEntity end + --[=[ + Returns an iterator that will skip any entities that also have the given components. + The filtering is done at the archetype level, and so it is faster than manually skipping entities. + + @param ... Component -- The component types to filter against. + @return () -> (id, ...ComponentInstance) -- Iterator of entity ID followed by the requested component values + + ```lua + for id in world:query(Target):without(Model) do + -- Do something + end + ``` + ]=] local function without(query, ...: Component) local numComponents = select("#", ...) local numCompatibleArchetypes = #compatibleArchetypes @@ -782,7 +808,7 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds local shouldRemove = false for componentIndex = 1, numComponents do local component = select(componentIndex, ...) - if archetype.componentIdToStorageIndex[#component] then + if archetype.idToIndex[#component] then shouldRemove = true break end @@ -803,9 +829,9 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds end currentArchetype = compatibleArchetypes[1] - currentEntities = currentArchetype.ownedEntities + currentEntities = currentArchetype.entities - cacheComponentStorages() + cacheFields() return query end @@ -825,6 +851,32 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds end, } + --[=[ + Creates a "snapshot" of this query, draining this QueryResult and returning a list containing all of its results. + + By default, iterating over a QueryResult happens in "real time": it iterates over the actual data in the ECS, so + changes that occur during the iteration will affect future results. + + By contrast, `QueryResult:snapshot()` creates a list of all of the results of this query at the moment it is called, + so changes made while iterating over the result of `QueryResult:snapshot` do not affect future results of the + iteration. + + Of course, this comes with a cost: we must allocate a new list and iterate over everything returned from the + QueryResult in advance, so using this method is slower than iterating over a QueryResult directly. + + The table returned from this method has a custom `__iter` method, which lets you use it as you would use QueryResult + directly: + + ```lua + for entityId, health, player in world:query(Health, Player):snapshot() do + + end + ``` + + However, the table itself is just a list of sub-tables structured like `{entityId, component1, component2, ...etc}`. + + @return {{entityId: number, component: ComponentInstance, component: ComponentInstance, component: ComponentInstance, ...}} + ]=] local function snapshot() local entities: { any } = setmetatable({}, Snapshot) :: any while true do @@ -839,6 +891,21 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds return entities end + --[=[ + Creates a View of the query and does all of the iterator tasks at once at an amortized cost. + This is used for many repeated random access to an entity. If you only need to iterate, just use a query. + + ```lua + local inflicting = world:query(Damage, Hitting, Player):view() + for _, source in world:query(DamagedBy) do + local damage = inflicting:get(source.from) + end + + for _ in world:query(Damage):view() do end -- You can still iterate views if you want! + ``` + + @return View See [View](/api/View) docs. + ]=] local function view() local entities = {} while true do @@ -850,6 +917,11 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds entities[entry[1]] = table.move(entry, 2, #entry, 1, {}) end + --[=[ + Retrieve the query results to corresponding `entity` + @param entity number - the entity ID + @return ...ComponentInstance + ]=] local function get(_, entityId) local components = entities[entityId] if components == nil then @@ -859,6 +931,11 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds return unpack(components, 1, #components) end + --[=[ + Equivalent to `world:contains()` + @param entity number - the entity ID + @return boolean + ]=] local function contains(_, entityId) return entities[entityId] ~= nil end @@ -882,7 +959,7 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds }) end - cacheComponentStorages() + cacheFields() return setmetatable({ next = nextEntity, without = without, @@ -894,132 +971,6 @@ function QueryResult.new(compatibleArchetypes, queryLength: number, componentIds }) end ---[=[ - Returns the next set of values from the query result. Once all results have been returned, the - QueryResult is exhausted and is no longer useful. - - :::info - This function is equivalent to calling the QueryResult as a function. When used in a for loop, this is implicitly - done by the language itself. - ::: - - ```lua - -- Using world:query in this position will make Lua invoke the table as a function. This is conventional. - for id, enemy, charge, model in world:query(Enemy, Charge, Model) do - -- Do something - end - ``` - - If you wanted to iterate over the QueryResult without a for loop, it's recommended that you call `next` directly - instead of calling the QueryResult as a function. - ```lua - local id, enemy, charge, model = world:query(Enemy, Charge, Model):next() - local id, enemy, charge, model = world:query(Enemy, Charge, Model)() -- Possible, but unconventional - ``` - - @return id -- Entity ID - @return ...ComponentInstance -- The requested component values -]=] -function QueryResult:next() - return self._expand(nextItem(self)) -end - -local snapshot = { - __iter = function(self): any - local i = 0 - return function() - i += 1 - - local data = self[i] - - if data then - return unpack(data, 1, data.n) - end - return - end - end, -} - ---[=[ - Creates a "snapshot" of this query, draining this QueryResult and returning a list containing all of its results. - - By default, iterating over a QueryResult happens in "real time": it iterates over the actual data in the ECS, so - changes that occur during the iteration will affect future results. - - By contrast, `QueryResult:snapshot()` creates a list of all of the results of this query at the moment it is called, - so changes made while iterating over the result of `QueryResult:snapshot` do not affect future results of the - iteration. - - Of course, this comes with a cost: we must allocate a new list and iterate over everything returned from the - QueryResult in advance, so using this method is slower than iterating over a QueryResult directly. - - The table returned from this method has a custom `__iter` method, which lets you use it as you would use QueryResult - directly: - - ```lua - for entityId, health, player in world:query(Health, Player):snapshot() do - - end - ``` - - However, the table itself is just a list of sub-tables structured like `{entityId, component1, component2, ...etc}`. - - @return {{entityId: number, component: ComponentInstance, component: ComponentInstance, component: ComponentInstance, ...}} -]=] -function QueryResult:snapshot() - local list = setmetatable({}, snapshot) - - local function iter() - return nextItem(self) - end - - for entityId, entityData in iter do - if entityId then - table.insert(list, table.pack(self._expand(entityId, entityData))) - end - end - - return list -end - ---[=[ - Returns an iterator that will skip any entities that also have the given components. - - :::tip - This is essentially equivalent to querying normally, using `World:get` to check if a component is present, - and using Lua's `continue` keyword to skip this iteration (though, using `:without` is faster). - - This means that you should avoid queries that return a very large amount of results only to filter them down - to a few with `:without`. If you can, always prefer adding components and making your query more specific. - ::: - - @param ... Component -- The component types to filter against. - @return () -> (id, ...ComponentInstance) -- Iterator of entity ID followed by the requested component values - - ```lua - for id in world:query(Target):without(Model) do - -- Do something - end - ``` -]=] - -function QueryResult:without(...) - local world = self.world - local filter = negateArchetypeOf(...) - - local negativeArchetype = `{self._queryArchetype}x{filter}` - - if world._queryCache[negativeArchetype] == nil then - world:_newQueryArchetype(negativeArchetype) - end - - local compatibleArchetypes = world._queryCache[negativeArchetype] - - self.compatibleArchetypes = compatibleArchetypes - self.currentCompatibleArchetype = next(compatibleArchetypes) - return self -end - --[=[ @class View @@ -1034,120 +985,6 @@ end ``` ]=] ---[=[ - Creates a View of the query and does all of the iterator tasks at once at an amortized cost. - This is used for many repeated random access to an entity. If you only need to iterate, just use a query. - - ```lua - local inflicting = world:query(Damage, Hitting, Player):view() - for _, source in world:query(DamagedBy) do - local damage = inflicting:get(source.from) - end - - for _ in world:query(Damage):view() do end -- You can still iterate views if you want! - ``` - - @return View See [View](/api/View) docs. -]=] - -function QueryResult:view() - local components = {} - local metatables = self.metatables - local queryLength = #metatables - local componentRecords = {} - for index, metatable in metatables do - components[index] = {} - componentRecords[metatable] = index - end - - local function iter() - return nextItem(self) - end - - local entities = {} - local entityIndex = 0 - local entityRecords = {} - - for entityId, entityData in iter do - entityIndex += 1 - - for metatable, componentIndex in componentRecords do - components[componentIndex][entityId] = entityData[metatable] - end - - entities[entityIndex] = entityId - entityRecords[entityId] = entityIndex - end - - local View = {} - View.__index = View - - local tuple = {} - local function expand(entity) - if queryLength == 1 then - return components[1][entity] - elseif queryLength == 2 then - return components[1][entity], components[2][entity] - elseif queryLength == 3 then - return components[1][entity], components[2][entity], components[3][entity] - elseif queryLength == 4 then - return components[1][entity], components[2][entity], components[3][entity], components[4][entity] - elseif queryLength == 5 then - return components[1][entity], - components[2][entity], - components[3][entity], - components[4][entity], - components[5][entity] - end - - for index, componentField in components do - tuple[index] = componentField[entity] - end - - return unpack(tuple) - end - - function View:__iter() - local index = 0 - return function() - index += 1 - local entity = entities[index] - if not entity then - return - end - - return entity, expand(entity) - end - end - - --[=[ - @within View - Retrieve the query results to corresponding `entity` - @param entity number - the entity ID - @return ...ComponentInstance - ]=] - function View:get(entity) - if not self:contains(entity) then - return - end - - return expand(entity) - end - - --[=[ - @within View - Equivalent to `world:contains()` - @param entity number - the entity ID - @return boolean - ]=] - - function View:contains(entity) - return entityRecords[entity] ~= nil - end - - return setmetatable({}, View) -end - --[=[ Performs a query against the entities in this World. Returns a [QueryResult](/api/QueryResult), which iterates over the results of the query. @@ -1168,8 +1005,8 @@ end @return QueryResult -- See [QueryResult](/api/QueryResult) docs. ]=] -function World.query(self: World, ...) - local A, B, C, D, E, F = ... +function World:query(...) + local A, B, C, D, E, F, G, H = ... local componentIds: { number } local queryLength = select("#", ...) @@ -1185,6 +1022,10 @@ function World.query(self: World, ...) componentIds = { #A, #B, #C, #D, #E } elseif queryLength == 6 then componentIds = { #A, #B, #C, #D, #E, #F } + elseif queryLength == 7 then + componentIds = { #A, #B, #C, #D, #E, #F, #G } + elseif queryLength == 8 then + componentIds = { #A, #B, #C, #D, #E, #F, #G, #H } else componentIds = table.create(queryLength) for i = 1, queryLength do @@ -1192,41 +1033,37 @@ function World.query(self: World, ...) end end - local possibleArchetypes = self.archetypes:findArchetypes(componentIds) - local compatibleArchetypes = possibleArchetypes - --print("possibleArchetypes:", possibleArchetypes) - -- local possibleArchetypes - -- local compatibleArchetypes = {} - -- for _, componentId in componentIds do - -- local associatedArchetypes = self.componentToArchetypes[componentId] - -- if associatedArchetypes == nil then - -- return noopQuery - -- end - - -- if possibleArchetypes == nil or #possibleArchetypes > #associatedArchetypes then - -- possibleArchetypes = associatedArchetypes - -- end - -- end + local possibleArchetypes: { Archetype } + local compatibleArchetypes: { Archetype } = {} + for _, componentId in componentIds do + local associatedArchetypes = self.componentToArchetypes[componentId] + if associatedArchetypes == nil then + return noopQuery + end + + if possibleArchetypes == nil or #possibleArchetypes > #associatedArchetypes then + possibleArchetypes = associatedArchetypes + end + end -- Narrow the archetypes so only ones that contain all components are searched - -- for archetypeIndex in possibleArchetypes do - -- local archetype = self.archetypes[archetypeIndex] - -- local incompatible = false - -- for _, componentId in componentIds do - -- -- Does this archetype have this component? - -- if archetype.componentIdToStorageIndex[componentId] == nil then - -- -- Nope, so we can't use this one. - -- incompatible = true - -- break - -- end - -- end - - -- if incompatible then - -- continue - -- end - - -- table.insert(compatibleArchetypes, archetype) - -- end + for _, archetype in possibleArchetypes do + local incompatible = false + for _, componentId in componentIds do + -- Does this archetype have this component? + if archetype.idToIndex[componentId] == nil then + -- Nope, so we can't use this one. + incompatible = true + break + end + end + + if incompatible then + continue + end + + table.insert(compatibleArchetypes, archetype) + end if #compatibleArchetypes == 0 then return noopQuery @@ -1428,7 +1265,7 @@ end @param ... Component -- The components to remove ]=] function World.remove(self: World, id, ...: Component) - local entityRecord = self.entities[id] + local entityRecord = self.allEntities[id] if entityRecord == nil then error(ERROR_NO_ENTITY, 2) end @@ -1438,7 +1275,7 @@ function World.remove(self: World, id, ...: Component) local archetype = entityRecord.archetype for _, component in components do local componentId = #component - local storage = archetype.fields[archetype.componentIdToStorageIndex[componentId]] + local storage = archetype.fields[archetype.idToIndex[componentId]] table.insert(componentInstances, if storage then storage[entityRecord.indexInArchetype] else nil) end diff --git a/lib/World.spec.luau b/lib/World.spec.luau index 7e05e85e..0986a654 100644 --- a/lib/World.spec.luau +++ b/lib/World.spec.luau @@ -143,56 +143,6 @@ return function() end) describe("immediate", function() - it("test", function() - local world = World.new() - local A, B, C, D = component(), component(), component(), component() - -- world:spawn(A({ a = true }), B({ b = true }), C({ c = true, deep = true })) - -- world:spawn(A({ a = true })) - -- world:spawn(C({ c = true, top = true })) - -- world:spawn(B({ b = true }), C({ c = true, nested = true })) - - world:spawn(A({ a = true }), B({ b = true })) - -- world:insert(15, B({})) - -- world:remove(15, D({})) - -- world:replace(15, B({}), A({})) - for id, a in world:query(B, C) do - print(id, a) - end - - print("Done:", world.archetypes) - end) - it("should work", function() - local world = World.new() - local A, B, C = component(), component(), component() - - local id = world:spawn(A({ a = true })) - world:replace(id, B({ b = true })) - world:insert(id, C({ c = true })) - - print(world.archetypes) - -- world:spawnAt(10) - -- world:insert(10, A({ a = true }), B({ b = true })) - -- world:remove(10, A, B) - -- world:insert(10, A({ a = true, second = true })) - -- world:insert(10, B({ b = true, second = true })) - - --world:spawnAt(50, A({ a = true, two = true }), B({ b = true, two = true })) - -- world:spawnAt(11) - -- world:insert(11, A({ a = true, two = true }), B({ b = true, two = true })) - --print(world.archetypes) - -- world:spawnAt(10, A({ a = true })) - -- world:spawnAt(11, A({ a = true, two = true })) - -- world:spawnAt(12, A({ a = true, three = true })) - - -- world:insert(10, B({ b = true })) - --world:spawnAt(10, A({ a = true }), B({ b = true })) - --world:remove(10, B) - -- for id, a in world:query(A) do - -- print(id, a) - -- end - --print(world) - end) - it("should be iterable", function() local world = World.new() local A = component() @@ -204,7 +154,6 @@ return function() local count = 0 for id, data in world do - print(id, data) count += 1 if id == eA then expect(data[A]).to.be.ok() @@ -582,7 +531,6 @@ return function() local ran = false for entityId, record in w:queryChanged(A) do - print(entityId, record, tostring(record.new)) if additionalQuery then if w:get(entityId, additionalQuery) == nil then continue @@ -784,7 +732,6 @@ return function() local viewA = world:query(ComponentA):view() local viewB = world:query(ComponentB):view() - print(viewA) expect(viewA:contains(entityA)).to.equal(true) expect(viewA:contains(entityB)).to.equal(false) expect(viewB:contains(entityB)).to.equal(true) diff --git a/lib/archetype.luau b/lib/archetype.luau index ff7d2571..799253e3 100644 --- a/lib/archetype.luau +++ b/lib/archetype.luau @@ -1,8 +1,50 @@ -function archetypeOf(componentIds: { number }) +export type EntityId = number +export type ComponentId = number +export type ComponentIds = { ComponentId } + +export type Component = { [any]: any } +export type ComponentInstance = { [any]: any } + +export type ArchetypeId = string +export type Archetype = { + entities: { EntityId }, + componentIds: { ComponentId }, + idToIndex: { [ComponentId]: number }, + indexToId: { [number]: ComponentId }, + fields: { { ComponentInstance } }, +} + +function hash(componentIds: { number }) table.sort(componentIds) return table.concat(componentIds, "_") end +function new(componentIds: { ComponentId }): (Archetype, ArchetypeId) + local length = #componentIds + local archetypeId = hash(componentIds) + + local idToIndex, indexToId = {}, {} + local fields = table.create(length) + + local archetype: Archetype = { + entities = {}, + componentIds = componentIds, + idToIndex = idToIndex, + indexToId = indexToId, + fields = fields, + } + + for index, componentId in componentIds do + idToIndex[componentId] = index + indexToId[index] = componentId + + fields[index] = {} + end + + return archetype, archetypeId +end + return { - archetypeOf = archetypeOf, + new = new, + hash = hash, } diff --git a/lib/archetype.spec.luau b/lib/archetype.spec.luau index ac19d77f..373a6c8d 100644 --- a/lib/archetype.spec.luau +++ b/lib/archetype.spec.luau @@ -1,4 +1,4 @@ -local archetype = require(script.Parent.archetype) +local Archetype = require(script.Parent.Archetype) local component = require(script.Parent).component return function() @@ -6,32 +6,7 @@ return function() it("should report same sets as same archetype", function() local a = component() local b = component() - expect(archetype.archetypeOf(a, b)).to.equal(archetype.archetypeOf(b, a)) - end) - it("should identify compatible archetypes", function() - local a = component() - local b = component() - local c = component() - - local archetypeA = archetype.archetypeOf(a, b, c) - local archetypeB = archetype.archetypeOf(a, b) - local archetypeC = archetype.archetypeOf(b, c) - - expect(archetype.areArchetypesCompatible(archetypeA, archetypeB)).to.equal(false) - expect(archetype.areArchetypesCompatible(archetypeB, archetypeA)).to.equal(true) - - expect(archetype.areArchetypesCompatible(archetypeC, archetypeA)).to.equal(true) - expect(archetype.areArchetypesCompatible(archetypeB, archetypeC)).to.equal(false) - end) - it("should identify compatible archetypes with negations", function() - local a = component() - local b = component() - local c = component() - - local archetypeAB = archetype.archetypeOf(a, b) - local negativeArchetypeBC = archetype.negateArchetypeOf(b, c) - - expect(archetype.areArchetypesCompatible(negativeArchetypeBC, archetypeAB)).to.equal(true) + expect(Archetype.hash({ #a, #b })).to.equal(Archetype.hash({ #b, #a })) end) end) end diff --git a/lib/archetypeTree.luau b/lib/archetypeTree.luau deleted file mode 100644 index f5c50b73..00000000 --- a/lib/archetypeTree.luau +++ /dev/null @@ -1,217 +0,0 @@ -type ComponentId = number -type EntityId = number - -export type Archetype = { - ownedEntities: { EntityId }, - - --- The component IDs that are part of this archetype, in no particular order - componentIds: { ComponentId }, - - --- Maps a component ID to its index in the storage - componentIdToStorageIndex: { [ComponentId]: number }, - - --- Maps a storage index to its component ID, useful for iterating the world - storageIndexToComponentId: { [number]: ComponentId }, - - fields: { { any } }, -} - -type Node = { - children: { [ComponentId]: Node? }, - archetype: Archetype?, -} - -type Terms = { ComponentId } - -local function newArchetypeTree() - local function createNode(): Node - local node: Node = { - children = {}, - numChildren = 0, - } - - return node - end - - local root = createNode() - root.archetype = { - ownedEntities = {}, - componentIds = {}, - componentIdToStorageIndex = {}, - storageIndexToComponentId = {}, - fields = {}, - } :: Archetype - - local function findNode(_, terms: Terms): Node - local node = root - for _, term in terms do - local child = node.children[term] - if child == nil then - child = createNode() - node.children[term] = child - node.numChildren += 1 - end - - node = child - end - - return node - end - - local function findArchetypes(_, with: Terms) - local terms = table.clone(with) - table.sort(terms) - - --print("finding:", terms) - --print(" ", root) - local archetypes = {} - local count = 0 - local function check(node: Node, ...: ComponentId) - -- print("check:", node, ...) - count += 1 - if select("#", ...) == 0 then - if node.archetype and #node.archetype.ownedEntities > 0 then - table.insert(archetypes, node.archetype) - end - end - - for componentId, child in node.children do - local head = select(1, ...) - -- print(" child", componentId, child, head) - if head then - if componentId < head then - check(child, ...) - elseif componentId == head then - check(child, select(2, ...)) - end - else - check(child, ...) - end - end - end - - -- Make sure that a node exists for each term - -- If it doesn't then we know that the archetypes will be empty - local smallestHeight, smallestNode, smallestNodeIndex = math.huge, nil, -1 - for index, componentId in terms do - local node = root.children[componentId] - if node == nil then - return {} - end - - if node.numChildren < smallestHeight then - smallestNode = node - smallestNodeIndex = index - smallestHeight = node.numChildren - end - end - - if smallestNode == nil then - return {} - end - - -- TODO: - -- Only traverse path with smallest num children - local newTerms = table.clone(terms) - table.remove(newTerms, smallestNodeIndex) - - -- print("terms", terms, smallestNode, "newTerms", newTerms) - check(smallestNode, unpack(newTerms, 1, #newTerms)) - -- if terms[1] == 152 then -- terms[1] == 5 and terms[2] == 150 then - -- print("jumps:", count, newTerms, smallestNode) - -- end - - return archetypes - end - - local function ensureArchetype(self, terms: Terms): Archetype - table.sort(terms) - - --print("ensure:", terms) - local archetype - for skipIndex = 1, #terms do - local newTerms = table.clone(terms) - table.remove(newTerms, skipIndex) - table.insert(newTerms, 1, terms[skipIndex]) - - --print(" newTerms", skipIndex, newTerms) - local node = findNode(self, newTerms) - if node.archetype then - archetype = node.archetype - else - if archetype == nil then - archetype = { - ownedEntities = {}, - componentIds = terms, - componentIdToStorageIndex = {}, - storageIndexToComponentId = {}, - fields = {}, - } - - for index, componentId in terms do - archetype.componentIdToStorageIndex[componentId] = index - archetype.storageIndexToComponentId[index] = componentId - archetype.fields[index] = {} - end - end - - node.archetype = archetype - end - end - - --print(" ret:", archetype) - return archetype - end - - local function count() - local archetypes = {} - local function check(node, str) - if node.archetype then - archetypes[str] = (archetypes[str] or 0) + #node.archetype.ownedEntities - end - - for componentId, child in node.children do - check(child, str .. "_" .. componentId) - end - end - - check(root, "") - return archetypes - end - - local function cleanup() - -- TODO: - -- dont potentially delete root node - - local function check(node) - for componentId, child in node.children do - local archetype, children = child.archetype, child.children - if archetype == nil or #archetype.ownedEntities == 0 then - if next(children) == nil then - node.children[componentId] = nil - node.numChildren -= 1 - else - child.archetype = nil - end - end - - check(child) - end - end - - check(root) - end - - return table.freeze({ - findNode = findNode, - findArchetypes = findArchetypes, - ensureArchetype = ensureArchetype, - root = root, - count = count, - cleanup = cleanup, - }) -end - -return { - new = newArchetypeTree, -} From a2cf6cb74f0eaa1e23b0defa44adf76ae2f11f9e Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 20 Oct 2024 16:04:07 -0400 Subject: [PATCH 79/87] Add back most world operation validators --- benchmarks/query.bench.luau | 3 --- benchmarks/stress.bench.luau | 20 ++++---------------- benchmarks/without.bench.luau | 4 ---- example/src/shared/start.luau | 8 -------- lib/World.luau | 23 +++++++++++++++-------- lib/component.luau | 2 +- 6 files changed, 20 insertions(+), 40 deletions(-) diff --git a/benchmarks/query.bench.luau b/benchmarks/query.bench.luau index 43733d3c..d1ae6d86 100644 --- a/benchmarks/query.bench.luau +++ b/benchmarks/query.bench.luau @@ -16,7 +16,6 @@ for i = 1, 10_000 do pinnedWorld:spawnAt(i, pinnedA({}), pinnedB({})) end -print(world.archetypes, world.archetypes:count()) return { ParameterGenerator = function() return @@ -28,8 +27,6 @@ return { for _ in world:query(A, B) do count += 1 end - - print("new", count) end, ["Old Matter"] = function() local count = 0 diff --git a/benchmarks/stress.bench.luau b/benchmarks/stress.bench.luau index 583ccabe..1188bcab 100644 --- a/benchmarks/stress.bench.luau +++ b/benchmarks/stress.bench.luau @@ -83,13 +83,6 @@ end print("archetypes:", archetypes) print(total, "different archetypes") -print("tree:", world.archetypes:count()) -local total = 0 -for _, count in world.archetypes:count() do - total += count -end -print(total, "archetypes") - return { ParameterGenerator = function() return @@ -97,21 +90,16 @@ return { Functions = { ["Old Matter"] = function() - --local count = 0 + local count = 0 for _ in pinnedWorld:query(pinnedB, pinnedA) do - --count += 1 + count += 1 end - - --print("old:", count) end, ["New Matter"] = function() - --local count = 0 + local count = 0 for _ in world:query(B, A) do - --count += 1 - --print("entt", a, b) + count += 1 end - - --print("new:", count) end, }, } diff --git a/benchmarks/without.bench.luau b/benchmarks/without.bench.luau index c2f750a3..c755e5f1 100644 --- a/benchmarks/without.bench.luau +++ b/benchmarks/without.bench.luau @@ -75,16 +75,12 @@ return { for _ in pinnedWorld:query(pinnedB):without(pinnedC) do count += 1 end - - --print("old:", count) end, ["New Matter"] = function() local count = 0 for _ in world:query(B):without(C) do count += 1 end - - print("new:", count) end, }, } diff --git a/example/src/shared/start.luau b/example/src/shared/start.luau index 0cb24861..07df6552 100644 --- a/example/src/shared/start.luau +++ b/example/src/shared/start.luau @@ -93,14 +93,6 @@ local function start(containers) end) end - if RunService:IsServer() then - task.spawn(function() - while task.wait(1) do - print(world.archetypes) - end - end) - end - return world, state end diff --git a/lib/World.luau b/lib/World.luau index def64a45..3af41127 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -8,6 +8,7 @@ local topoRuntime = require(script.Parent.topoRuntime) local assertValidComponentInstances = Component.assertValidComponentInstances local assertValidComponent = Component.assertValidComponent +local assertComponentArgsProvided = Component.assertComponentArgsProvided type EntityId = Archetype.EntityId type ComponentId = Archetype.ComponentId @@ -55,6 +56,15 @@ type ReplaceCommand = { type Command = DespawnCommand | InsertCommand | RemoveCommand | ReplaceCommand +local function assertEntityExists(world, id: number) + assert(world:contains(id), "Entity doesn't exist, use world:contains to check if needed") +end + +local function assertWorldOperationIsValid(world, id: number, ...) + assertEntityExists(world, id) + assertComponentArgsProvided(...) +end + --[=[ @class World @@ -500,6 +510,7 @@ end function World:replace(id, ...) local componentInstances = { ... } assertValidComponentInstances(componentInstances) + bufferCommand(self, { type = "replace", entityId = id, componentInstances = componentInstances }) end @@ -551,15 +562,13 @@ end @param ... Component -- The components to fetch @return ... -- Returns the component values in the same order they were passed in ]=] -function World.get(self: World, entityId, ...: Component) - local entityRecord = self.allEntities[entityId] - if entityRecord == nil then - error(ERROR_NO_ENTITY, 2) - end +function World:get(entityId, ...: Component) + assertWorldOperationIsValid(self, entityId, ...) local length = select("#", ...) local componentInstances = table.create(length, nil) + local entityRecord = self.allEntities[entityId] local archetype = entityRecord.archetype local idToIndex = archetype.idToIndex for i = 1, length do @@ -1244,9 +1253,7 @@ end @param ... ComponentInstance -- The component values to insert ]=] function World:insert(id, ...) - if not self:contains(id) then - error(ERROR_NO_ENTITY, 2) - end + assertWorldOperationIsValid(self, id, ...) local componentInstances = { ... } assertValidComponentInstances(componentInstances) diff --git a/lib/component.luau b/lib/component.luau index aa562faf..c257ab12 100644 --- a/lib/component.luau +++ b/lib/component.luau @@ -181,6 +181,6 @@ return { newComponent = newComponent, assertValidComponentInstance = assertValidComponentInstance, assertValidComponentInstances = assertValidComponentInstances, - assertValidComponentArgsProvided = assertValidComponentArgsProvided, + assertComponentArgsProvided = assertComponentArgsProvided, assertValidComponent = assertValidComponent, } From 4cbf9071bb7663a2e7757a273c79e07ea266faa7 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 20 Oct 2024 16:05:00 -0400 Subject: [PATCH 80/87] Remove strict directive --- lib/World.luau | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/World.luau b/lib/World.luau index 3af41127..c1297b79 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -1,4 +1,3 @@ ---!strict --!native --!optimize 2 From 2a8a8625fb6a24614e7a10331778fc603e9a6afe Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 20 Oct 2024 16:12:18 -0400 Subject: [PATCH 81/87] Fix lints --- lib/World.luau | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/lib/World.luau b/lib/World.luau index c1297b79..234f387e 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -477,28 +477,6 @@ function World:spawnAt(id: number, ...) return id end -function World:_newQueryArchetype(queryArchetype) - if self._queryCache[queryArchetype] == nil then - self._queryCache[queryArchetype] = {} - else - return -- Archetype isn't actually new - end - - for entityArchetype in self.storage do - if areArchetypesCompatible(queryArchetype, entityArchetype) then - self._queryCache[queryArchetype][entityArchetype] = true - end - end -end - -function World:_updateQueryCache(entityArchetype) - for queryArchetype, compatibleArchetypes in pairs(self._queryCache) do - if areArchetypesCompatible(queryArchetype, entityArchetype) then - compatibleArchetypes[entityArchetype] = true - end - end -end - --[=[ Replaces a given entity by ID with an entirely new set of components. Equivalent to removing all components from an entity, and then adding these ones. @@ -557,7 +535,7 @@ end --[=[ Gets a specific component (or set of components) from a specific entity in this world. - @param id number -- The entity ID + @param entityId number -- The entity ID @param ... Component -- The components to fetch @return ... -- Returns the component values in the same order they were passed in ]=] From ce8e048a8125ed7e79b50ee2ac1ef4523d67d805 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 20 Oct 2024 16:14:41 -0400 Subject: [PATCH 82/87] Add within tags --- lib/World.luau | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/World.luau b/lib/World.luau index 234f387e..1f29bcec 100644 --- a/lib/World.luau +++ b/lib/World.luau @@ -528,7 +528,7 @@ end @param id number -- The entity ID @return bool -- `true` if the entity exists ]=] -function World.contains(self: typeof(World.new()), id) +function World:contains(id) return self.allEntities[id] ~= nil end @@ -678,6 +678,8 @@ function QueryResult.new(compatibleArchetypes: { Archetype }, queryLength: numbe local entityId: number --[=[ + @within QueryResult + Returns the next set of values from the query result. Once all results have been returned, the QueryResult is exhausted and is no longer useful. @@ -774,6 +776,8 @@ function QueryResult.new(compatibleArchetypes: { Archetype }, queryLength: numbe end --[=[ + @within QueryResult + Returns an iterator that will skip any entities that also have the given components. The filtering is done at the archetype level, and so it is faster than manually skipping entities. @@ -838,6 +842,8 @@ function QueryResult.new(compatibleArchetypes: { Archetype }, queryLength: numbe } --[=[ + @within QueryResult + Creates a "snapshot" of this query, draining this QueryResult and returning a list containing all of its results. By default, iterating over a QueryResult happens in "real time": it iterates over the actual data in the ECS, so @@ -878,6 +884,8 @@ function QueryResult.new(compatibleArchetypes: { Archetype }, queryLength: numbe end --[=[ + @class View + Creates a View of the query and does all of the iterator tasks at once at an amortized cost. This is used for many repeated random access to an entity. If you only need to iterate, just use a query. @@ -904,6 +912,8 @@ function QueryResult.new(compatibleArchetypes: { Archetype }, queryLength: numbe end --[=[ + @within View + Retrieve the query results to corresponding `entity` @param entity number - the entity ID @return ...ComponentInstance @@ -918,6 +928,8 @@ function QueryResult.new(compatibleArchetypes: { Archetype }, queryLength: numbe end --[=[ + @within View + Equivalent to `world:contains()` @param entity number - the entity ID @return boolean From e4db549853e52645c4e78822b38259306aec5e9b Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 21 Oct 2024 12:14:36 -0400 Subject: [PATCH 83/87] Update pinned Matter to 0.8.4 --- benchmark.project.json | 2 +- benchmarks/insert.bench.luau | 4 ++-- benchmarks/next.bench.luau | 4 ++-- benchmarks/query.bench.luau | 4 ++-- benchmarks/stress.bench.luau | 13 ++++++------- benchmarks/without.bench.luau | 4 ++-- pinned/Matter_0_8_3.rbxm | Bin 63606 -> 0 bytes pinned/Matter_0_8_4.rbxm | Bin 0 -> 67354 bytes 8 files changed, 15 insertions(+), 16 deletions(-) delete mode 100644 pinned/Matter_0_8_3.rbxm create mode 100644 pinned/Matter_0_8_4.rbxm diff --git a/benchmark.project.json b/benchmark.project.json index f456965f..8bf9157a 100644 --- a/benchmark.project.json +++ b/benchmark.project.json @@ -9,7 +9,7 @@ "$path": "lib" }, "PinnedMatter": { - "$path": "pinned/Matter_0_8_3.rbxm" + "$path": "pinned/Matter_0_8_4.rbxm" } }, "ServerStorage": { diff --git a/benchmarks/insert.bench.luau b/benchmarks/insert.bench.luau index 9e7d45fd..d4ad3ba4 100644 --- a/benchmarks/insert.bench.luau +++ b/benchmarks/insert.bench.luau @@ -22,12 +22,12 @@ return { end, Functions = { - ["New"] = function(_, world) + ["Matter 0.9"] = function(_, world) for i = 1, N do world:insert(i, A({ i })) end end, - ["Old"] = function(_, _, world) + ["Matter 0.8.4"] = function(_, _, world) for i = 1, N do world:insert(i, A({ i })) end diff --git a/benchmarks/next.bench.luau b/benchmarks/next.bench.luau index c4941adc..d42bc16d 100644 --- a/benchmarks/next.bench.luau +++ b/benchmarks/next.bench.luau @@ -22,13 +22,13 @@ return { end, Functions = { - ["New Matter"] = function() + ["Matter 0.9"] = function() local query = world:query(A, B) for _ = 1, 1_000 do query:next() end end, - ["Old Matter"] = function() + ["Matter 0.8.4"] = function() local query = pinnedWorld:query(pinnedA, pinnedB) for _ = 1, 1_000 do query:next() diff --git a/benchmarks/query.bench.luau b/benchmarks/query.bench.luau index d1ae6d86..e096dab8 100644 --- a/benchmarks/query.bench.luau +++ b/benchmarks/query.bench.luau @@ -22,13 +22,13 @@ return { end, Functions = { - ["New Matter"] = function() + ["Matter 0.9"] = function() local count = 0 for _ in world:query(A, B) do count += 1 end end, - ["Old Matter"] = function() + ["Matter 0.8.4"] = function() local count = 0 for _ in pinnedWorld:query(pinnedA, pinnedB) do count += 1 diff --git a/benchmarks/stress.bench.luau b/benchmarks/stress.bench.luau index 1188bcab..aa2f6aac 100644 --- a/benchmarks/stress.bench.luau +++ b/benchmarks/stress.bench.luau @@ -30,7 +30,7 @@ local function flip() end local archetypes = {} -for i = 1, 1_000 do +for i = 1, 30_000 do local id = i world:spawnAt(id) pinnedWorld:spawnAt(id) @@ -69,18 +69,17 @@ for i = 1, 1_000 do if flip() then world:insert(id, G({ g = true, id = i })) pinnedWorld:insert(id, pinnedG({ g = true, id = i })) - str ..= "G_" + str ..= "G" end archetypes[str] = (archetypes[str] or 0) + 1 end local total = 0 -for _, count in archetypes do - total += count +for _ in archetypes do + total += 1 end -print("archetypes:", archetypes) print(total, "different archetypes") return { @@ -89,13 +88,13 @@ return { end, Functions = { - ["Old Matter"] = function() + ["Matter 0.8.4"] = function() local count = 0 for _ in pinnedWorld:query(pinnedB, pinnedA) do count += 1 end end, - ["New Matter"] = function() + ["Matter 0.9"] = function() local count = 0 for _ in world:query(B, A) do count += 1 diff --git a/benchmarks/without.bench.luau b/benchmarks/without.bench.luau index c755e5f1..7c130b35 100644 --- a/benchmarks/without.bench.luau +++ b/benchmarks/without.bench.luau @@ -70,13 +70,13 @@ return { end, Functions = { - ["Old Matter"] = function() + ["Matter 0.8.4"] = function() local count = 0 for _ in pinnedWorld:query(pinnedB):without(pinnedC) do count += 1 end end, - ["New Matter"] = function() + ["Matter 0.9"] = function() local count = 0 for _ in world:query(B):without(C) do count += 1 diff --git a/pinned/Matter_0_8_3.rbxm b/pinned/Matter_0_8_3.rbxm deleted file mode 100644 index 892d5cf147feb214cf43907bd85a877c7ad10cd5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63606 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