local None = {} local function merge(one, two) local new = table.clone(one) for key, value in two do if value == None then new[key] = nil else new[key] = value end end return new end -- https://github.com/freddylist/llama/blob/master/src/List/toSet.lua local function toSet(list) local set = {} for _, v in ipairs(list) do set[v] = true end return set end -- https://github.com/freddylist/llama/blob/master/src/Dictionary/values.lua local function values(dictionary) local valuesList = {} local index = 1 for _, value in pairs(dictionary) do valuesList[index] = value index = index + 1 end return valuesList end 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(...) 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 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 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 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 return false end end for excludeId in excludeIds do if targetIds[excludeId] then compatibilityCache[queryArchetype .. "-" .. targetArchetype] = false return false end end compatibilityCache[queryArchetype .. "-" .. targetArchetype] = true return true end local stack = {} local function newStackFrame(node) return { node = node, accessedKeys = {}, } end local function cleanup() local currentFrame = stack[#stack] for baseKey, state in pairs(currentFrame.node.system) do for key, value in pairs(state.storage) do if not currentFrame.accessedKeys[baseKey] or not currentFrame.accessedKeys[baseKey][key] then local cleanupCallback = state.cleanupCallback if cleanupCallback then local shouldAbortCleanup = cleanupCallback(value) if shouldAbortCleanup then continue end end state.storage[key] = nil end end end end local function start(node, fn) table.insert(stack, newStackFrame(node)) fn() cleanup() table.remove(stack, #stack) end local function withinTopoContext() return #stack ~= 0 end local function useFrameState() return stack[#stack].node.frame end local function useCurrentSystem() if #stack == 0 then return end return stack[#stack].node.currentSystem end --[=[ @within Matter :::tip **Don't use this function directly in your systems.** This function is used for implementing your own topologically-aware functions. It should not be used in your systems directly. You should use this function to implement your own utilities, similar to `useEvent` and `useThrottle`. ::: `useHookState` does one thing: it returns a table. An empty, pristine table. Here's the cool thing though: it always returns the *same* table, based on the script and line where *your function* (the function calling `useHookState`) was called. ### Uniqueness If your function is called multiple times from the same line, perhaps within a loop, the default behavior of `useHookState` is to uniquely identify these by call count, and will return a unique table for each call. However, you can override this behavior: you can choose to key by any other value. This means that in addition to script and line number, the storage will also only return the same table if the unique value (otherwise known as the "discriminator") is the same. ### Cleaning up As a second optional parameter, you can pass a function that is automatically invoked when your storage is about to be cleaned up. This happens when your function (and by extension, `useHookState`) ceases to be called again next frame (keyed by script, line number, and discriminator). Your cleanup callback is passed the storage table that's about to be cleaned up. You can then perform cleanup work, like disconnecting events. *Or*, you could return `true`, and abort cleaning up altogether. If you abort cleanup, your storage will stick around another frame (even if your function wasn't called again). This can be used when you know that the user will (or might) eventually call your function again, even if they didn't this frame. (For example, caching a value for a number of seconds). If cleanup is aborted, your cleanup function will continue to be called every frame, until you don't abort cleanup, or the user actually calls your function again. ### Example: useThrottle This is the entire implementation of the built-in `useThrottle` function: ```lua local function cleanup(storage) return os.clock() < storage.expiry end local function useThrottle(seconds, discriminator) local storage = useHookState(discriminator, cleanup) if storage.time == nil or os.clock() - storage.time >= seconds then storage.time = os.clock() storage.expiry = os.clock() + seconds return true end return false end ``` A lot of talk for something so simple, right? @param discriminator? any -- A unique value to additionally key by @param cleanupCallback (storage: {}) -> boolean? -- A function to run when the storage for this hook is cleaned up ]=] local function useHookState(discriminator, cleanupCallback): {} local file, line = debug.info(3, "sl") local fn = debug.info(2, "f") local baseKey = string.format("%s:%s:%d", tostring(fn), file, line) local currentFrame = stack[#stack] if currentFrame == nil then error("Attempt to access topologically-aware storage outside of a Loop-system context.", 3) end if not currentFrame.accessedKeys[baseKey] then currentFrame.accessedKeys[baseKey] = {} end local accessedKeys = currentFrame.accessedKeys[baseKey] local key = #accessedKeys if discriminator ~= nil then if type(discriminator) == "number" then discriminator = tostring(discriminator) end key = discriminator end accessedKeys[key] = true if not currentFrame.node.system[baseKey] then currentFrame.node.system[baseKey] = { storage = {}, cleanupCallback = cleanupCallback, } end local storage = currentFrame.node.system[baseKey].storage if not storage[key] then storage[key] = {} end return storage[key] end local topoRuntime = { start = start, useHookState = useHookState, useFrameState = useFrameState, useCurrentSystem = useCurrentSystem, withinTopoContext = withinTopoContext, } local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" --[=[ @class World A World contains entities which have components. The World is queryable and can be used to get entities with a specific set of components. Entities are simply ever-increasing integers. ]=] local World = {} 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 }, -- The most recent storage that has not been dirtied by an iterator _pristineStorage = firstStorage, -- 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 = {}, -- Cache of what entity archetypes have ever existed in the game. This is used for knowing -- when to update the queryCache. _entityArchetypeCache = {}, -- The next ID that will be assigned with World:spawn _nextId = 1, -- The total number of active entities in the world _size = 0, -- Storage for `queryChanged` _changedStorage = {}, }, World) end -- 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] end function World:_next(last) local entityId, archetype = next(self._entityArchetypes, last) if entityId == nil then return nil end local storage = self:_getStorageWithEntity(archetype, entityId) return entityId, storage[archetype][entityId] end --[=[ Iterates over all entities in this World. Iteration returns entity ID followed by a dictionary mapping Component to Component Instance. **Usage:** ```lua for entityId, entityData in world do print(entityId, entityData[Components.Example]) end ``` @return number @return {[Component]: ComponentInstance} ]=] 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 --[=[ @class Component A component is a named piece of data that exists on an entity. Components are created and removed in the [World](/api/World). In the docs, the terms "Component" and "ComponentInstance" are used: - **"Component"** refers to the base class of a specific type of component you've created. This is what [`Matter.component`](/api/Matter#component) returns. - **"Component Instance"** refers to an actual piece of data that can exist on an entity. The metatable of a component instance table is its respective Component table. Component instances are *plain-old data*: they do not contain behaviors or methods. Since component instances are immutable, one helper function exists on all component instances, `patch`, which allows reusing data from an existing component instance to make up for the ergonomic loss of mutations. ]=] --[=[ @within Component @type ComponentInstance {} The `ComponentInstance` type refers to an actual piece of data that can exist on an entity. The metatable of the component instance table is set to its particular Component table. A component instance can be created by calling the Component table: ```lua -- Component: local MyComponent = Matter.component("My component") -- component instance: local myComponentInstance = MyComponent({ some = "data" }) print(getmetatable(myComponentInstance) == MyComponent) --> true ``` ]=] -- This is a special value we set inside the component's metatable that will allow us to detect when -- a Component is accidentally inserted as a Component Instance. -- It should not be accessible through indexing into a component instance directly. local DIAGNOSTIC_COMPONENT_MARKER = {} local function newComponent(name, defaultData) name = name or debug.info(2, "s") .. "@" .. debug.info(2, "l") assert( defaultData == nil or type(defaultData) == "table", "if component default data is specified, it must be a table" ) local component = {} component.__index = component function component.new(data) data = data or {} if defaultData then data = merge(defaultData, data) end return table.freeze(setmetatable(data, component)) end --[=[ @within Component ```lua for id, target in world:query(Target) do if shouldChangeTarget(target) then world:insert(id, target:patch({ -- modify the existing component currentTarget = getNewTarget() })) end end ``` A utility function used to immutably modify an existing component instance. Key/value pairs from the passed table will override those of the existing component instance. As all components are immutable and frozen, it is not possible to modify the existing component directly. You can use the `Matter.None` constant to remove a value from the component instance: ```lua target:patch({ currentTarget = Matter.None -- sets currentTarget to nil }) ``` @param partialNewData {} -- The table to be merged with the existing component data. @return ComponentInstance -- A copy of the component instance with values from `partialNewData` overriding existing values. ]=] function component:patch(partialNewData) local patch = getmetatable(self).new(merge(self, partialNewData)) return patch end setmetatable(component, { __call = function(_, ...) return component.new(...) end, __tostring = function() return name end, [DIAGNOSTIC_COMPONENT_MARKER] = true, }) return component end local function assertValidType(value, position) if typeof(value) ~= "table" then error(string.format("Component #%d is invalid: not a table", position), 3) end local metatable = getmetatable(value) if metatable == nil then error(string.format("Component #%d is invalid: has no metatable", position), 3) end end local function assertValidComponent(value, position) assertValidType(value, position) local metatable = getmetatable(value) if getmetatable(metatable) ~= nil and getmetatable(metatable)[DIAGNOSTIC_COMPONENT_MARKER] then error( string.format( "Component #%d is invalid: Component Instance %s was passed instead of the Component itself!", position, tostring(metatable) ), 3 ) end end local function assertValidComponentInstance(value, position) assertValidType(value, position) if getmetatable(value)[DIAGNOSTIC_COMPONENT_MARKER] ~= nil then error( string.format( "Component #%d is invalid: passed a Component instead of a Component instance; " .. "did you forget to call it as a function?", position ), 3 ) end 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 error( string.format( "The world already contains an entity with ID %d. Use World:replace instead if this is intentional.", id ), 2 ) end self._size += 1 if id >= self._nextId then self._nextId = id + 1 end local components = {} local metatables = {} 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, nil, newComponent) components[metatable] = newComponent table.insert(metatables, metatable) end self._entityMetatablesCache[id] = metatables self:_transitionArchetype(id, components) 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 _, storage in self._storages do for entityArchetype in storage do if areArchetypesCompatible(queryArchetype, entityArchetype) then self._queryCache[queryArchetype][entityArchetype] = true end 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 function World:_transitionArchetype(id, components) local newArchetype = nil local oldArchetype = self._entityArchetypes[id] local oldStorage if oldArchetype then oldStorage = self:_getStorageWithEntity(oldArchetype, id) if not components then oldStorage[oldArchetype][id] = nil end end if components then newArchetype = archetypeOf(unpack(self._entityMetatablesCache[id])) if oldArchetype ~= newArchetype then if oldStorage then oldStorage[oldArchetype][id] = nil end if self._pristineStorage[newArchetype] == nil then self._pristineStorage[newArchetype] = {} end if self._entityArchetypeCache[newArchetype] == nil then self._entityArchetypeCache[newArchetype] = true self:_updateQueryCache(newArchetype) end self._pristineStorage[newArchetype][id] = components else oldStorage[newArchetype][id] = components end end self._entityArchetypes[id] = newArchetype 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. @param id number -- The entity ID @param ... ComponentInstance -- The component values to spawn the entity with. ]=] function World:replace(id, ...) 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) end --[=[ Despawns a given entity by ID, removing it and all its components from the world entirely. @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 end --[=[ Removes all entities from the world. :::caution Removing entities in this way is not reported by `queryChanged`. ::: ]=] function World:clear() local firstStorage = {} self._storages = { firstStorage } self._pristineStorage = firstStorage self._entityArchetypes = {} self._entityMetatablesCache = {} self._size = 0 self._changedStorage = {} end --[=[ Checks if the given entity ID is currently spawned in this world. @param id number -- The entity ID @return bool -- `true` if the entity exists ]=] function World:contains(id) return self._entityArchetypes[id] ~= nil end --[=[ Gets a specific component (or set of components) from a specific entity in this world. @param id number -- The entity ID @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 error(ERROR_NO_ENTITY, 2) end local entity = self:_getEntity(id) local length = select("#", ...) if length == 1 then assertValidComponent((...), 1) return entity[...] end local components = {} for i = 1, length do local metatable = select(i, ...) assertValidComponent(metatable, i) components[i] = entity[metatable] end return unpack(components, 1, length) end local function noop() end local noopQuery = setmetatable({ next = noop, snapshot = noop, without = function(self) return self end, view = { get = noop, contains = noop, }, }, { __iter = function() return noop end, }) --[=[ @class QueryResult A result from the [`World:query`](/api/World#query) function. Calling the table or the `next` method allows iteration over the results. Once all results have been returned, the QueryResult is exhausted and is no longer useful. ```lua for id, enemy, charge, model in world:query(Enemy, Charge, Model) do -- Do something end ``` ]=] local QueryResult = {} QueryResult.__index = QueryResult function QueryResult.new(world, expand, queryArchetype, compatibleArchetypes) return setmetatable({ world = world, seenEntities = {}, currentCompatibleArchetype = next(compatibleArchetypes), compatibleArchetypes = compatibleArchetypes, storageIndex = 1, _expand = expand, _queryArchetype = queryArchetype, }, QueryResult) 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 continue elseif nextStorage[currentCompatibleArchetype] == nil then continue end entityId, entityData = next(nextStorage[currentCompatibleArchetype]) end query.lastEntityId = entityId until seenEntities[entityId] == nil query.currentCompatibleArchetype = currentCompatibleArchetype seenEntities[entityId] = true return entityId, entityData end function QueryResult:__iter() return function() return self._expand(nextItem(self)) end end function QueryResult:__call() return self._expand(nextItem(self)) 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 Provides random access to the results of a query. Calling the View is equivalent to iterating a query. ```lua for id, player, health, poison in world:query(Player, Health, Poison):view() do -- Do something 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 function iter() return nextItem(self) end local fetches = {} local list = {} :: any local View = {} View.__index = View function View:__iter() local current = list.head return function() if not current then return end local entity = current.entity local fetch = fetches[entity] current = current.next return entity, unpack(fetch, 1, fetch.n) 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 local fetch = fetches[entity] local queryLength = fetch.n if queryLength == 1 then return fetch[1] elseif queryLength == 2 then return fetch[1], fetch[2] elseif queryLength == 3 then return fetch[1], fetch[2], fetch[3] elseif queryLength == 4 then return fetch[1], fetch[2], fetch[3], fetch[4] elseif queryLength == 5 then return fetch[1], fetch[2], fetch[3], fetch[4], fetch[5] end return unpack(fetch, 1, fetch.n) end --[=[ @within View Equivalent to `world:contains()` @param entity number - the entity ID @return boolean ]=] function View:contains(entity) return fetches[entity] ~= nil end for entityId, entityData in iter do if entityId then -- We start at 2 on Select since we don't need want to pack the entity id. local fetch = table.pack(select(2, self._expand(entityId, entityData))) local node = { entity = entityId, next = nil } fetches[entityId] = fetch if not list.head then list.head = node else local current = list.head while current.next do current = current.next end current.next = node end end 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. Order of iteration is not guaranteed. ```lua for id, enemy, charge, model in world:query(Enemy, Charge, Model) do -- Do something end for id in world:query(Target):without(Model) do -- Again, with feeling end ``` @param ... Component -- The component types to query. Only entities with *all* of these components will be returned. @return QueryResult -- See [QueryResult](/api/QueryResult) docs. ]=] function 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] 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) end local function cleanupQueryChanged(hookState) local world = hookState.world local componentToTrack = hookState.componentToTrack for index, object in world._changedStorage[componentToTrack] do if object == hookState.storage then table.remove(world._changedStorage[componentToTrack], index) break end end if next(world._changedStorage[componentToTrack]) == nil then world._changedStorage[componentToTrack] = nil end end --[=[ @interface ChangeRecord @within World .new? ComponentInstance -- The new value of the component. Nil if just removed. .old? ComponentInstance -- The former value of the component. Nil if just added. ]=] --[=[ :::info Topologically-aware function This function is only usable if called within the context of [`Loop:begin`](/api/Loop#begin). ::: Queries for components that have changed **since the last time your system ran `queryChanged`**. Only one changed record is returned per entity, even if the same entity changed multiple times. The order in which changed records are returned is not guaranteed to be the order that the changes occurred in. It should be noted that `queryChanged` does not have the same iterator invalidation concerns as `World:query`. :::tip The first time your system runs (i.e., on the first frame), all existing entities in the world that match your query are returned as "new" change records. ::: :::info Calling this function from your system creates storage internally for your system. Then, changes meeting your criteria are pushed into your storage. Calling `queryChanged` again each frame drains this storage. If your system isn't called every frame, the storage will continually fill up and does not empty unless you drain it. If you stop calling `queryChanged` in your system, changes will stop being tracked. ::: ### Returns `queryChanged` returns an iterator function, so you call it in a for loop just like `World:query`. The iterator returns the entity ID, followed by a [`ChangeRecord`](#ChangeRecord). The `ChangeRecord` type is a table that contains two fields, `new` and `old`, respectively containing the new component instance, and the old component instance. `new` and `old` will never be the same value. `new` will be nil if the component was removed (or the entity was despawned), and `old` will be nil if the component was just added. The `old` field will be the value of the component the last time this system observed it, not necessarily the value it changed from most recently. The `ChangeRecord` table is potentially shared with multiple systems tracking changes for this component, so it cannot be modified. ```lua for id, record in world:queryChanged(Model) do if record.new == nil then -- Model was removed if enemy.type == "this is a made up example" then world:remove(id, Enemy) end end end ``` @param componentToTrack Component -- The component you want to listen to changes for. @return () -> (id, ChangeRecord) -- Iterator of entity ID and change record ]=] function World:queryChanged(componentToTrack, ...: nil) if ... then error("World:queryChanged does not take any additional parameters", 2) end local hookState = topoRuntime.useHookState(componentToTrack, cleanupQueryChanged) if hookState.storage then return function(): any local entityId, record = next(hookState.storage) if entityId then hookState.storage[entityId] = nil return entityId, record end return end end if not self._changedStorage[componentToTrack] then self._changedStorage[componentToTrack] = {} end local storage = {} hookState.storage = storage hookState.world = self hookState.componentToTrack = componentToTrack table.insert(self._changedStorage[componentToTrack], storage) local queryResult = self:query(componentToTrack) return function(): any local entityId, component = queryResult:next() if entityId then return entityId, table.freeze({ new = component }) end return end end function World:_trackChanged(metatable, id, old, new) if not self._changedStorage[metatable] then return end if old == new then return end local record = table.freeze({ old = old, new = new, }) for _, storage in ipairs(self._changedStorage[metatable]) do -- If this entity has changed since the last time this system read it, -- we ensure that the "old" value is whatever the system saw it as last, instead of the -- "old" value we have here. if storage[id] then storage[id] = table.freeze({ old = storage[id].old, new = new }) else storage[id] = record end end end --[=[ Inserts a component (or set of components) into an existing entity. If another instance of a given component already exists on this entity, it is replaced. ```lua world:insert( entityId, ComponentA({ foo = "bar" }), ComponentB({ baz = "qux" }) ) ``` @param id number -- The entity ID @param ... ComponentInstance -- The component values to insert ]=] function World:insert(id, ...) 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 end --[=[ Removes a component (or set of components) from an existing entity. ```lua local removedA, removedB = world:remove(entityId, ComponentA, ComponentB) ``` @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) return unpack(removed, 1, length) end --[=[ Returns the number of entities currently spawned in the world. ]=] function World:size() return self._size end --[=[ :::tip [Loop] automatically calls this function on your World(s), so there is no need to call it yourself if you're using a Loop. ::: If you are not using a Loop, you should call this function at a regular interval (i.e., once per frame) to optimize the internal storage for queries. This is part of a strategy to eliminate iterator invalidation when modifying the World while inside a query from [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 return { World = World, component = newComponent }