diff --git a/.gitignore b/.gitignore index c3a366f4..2f97ec94 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,7 @@ WallyPatches # Misc roblox.toml sourcemap.json -drafts/*.lua +drafts/ # Cached Vitepress (docs) @@ -61,4 +61,4 @@ drafts/*.lua /docs/.vitepress/dist .vitepress/cache -.vitepress/dist \ No newline at end of file +.vitepress/dist diff --git a/src/init.luau b/src/init.luau index 4ccb761d..f86d1bd3 100644 --- a/src/init.luau +++ b/src/init.luau @@ -1,4 +1,3 @@ - --!optimize 2 --!native --!strict @@ -17,6 +16,7 @@ type ArchetypeEdge = { remove: Archetype, } + type Archetype = { id: number, edges: { [i53]: ArchetypeEdge }, @@ -669,6 +669,32 @@ do end end +local world_has: () -> boolean +do + function world_has(world, entity_id, ...) + local id = entity_id + local record = world.entityIndex.sparse[id] + if not record then + return false + end + + local archetype = record.archetype + if not archetype then + return false + end + + local tr = archetype.records + + for i = 1, select("#", ...) do + if not tr[select(i, ...)] then + return false + end + end + + return true + end +end + type Item = () -> (number, ...any) export type Query = typeof({ __iter = function(): Item @@ -678,8 +704,8 @@ export type Query = typeof({ end, }) & { next: Item, - replace: (Query, ...any) -> (), - without: (Query) -> Query + without: (Query) -> Query, + replace: (Query, (...any) -> (...any)) -> () } type CompatibleArchetype = { archetype: Archetype, indices: { number } } @@ -688,135 +714,145 @@ local world_query: (World, ...i53) -> Query do local noop: Item = function() - return nil :: any + return nil :: any end local EmptyQuery: Query = { - __iter = function(): Item - return noop - end, - next = noop :: Item, - replace = noop :: (Query, ...any) -> (), - without = function(self: Query, ...) - return self - end + __iter = function(): Item + return noop + end, + next = noop :: Item, + replace = noop :: (Query, ...any) -> (), + without = function(self: Query, ...) + return self + end } setmetatable(EmptyQuery, EmptyQuery) - local indices: { { number } } - local compatibleArchetypes: { Archetype } - local length - local components: { number } - local queryLength: number local lastArchetype: number local archetype: Archetype + local queryOutput: { any } + local queryLength: number + local entities: { number } + local i: number - local queryOutput: { any } + local compatible_archetypes: { Archetype } + local column_indices: { { number} } + local ids: { number } - local entities: {} - local i: number - - local function world_query_next() + local function world_query_next(): any local entityId = entities[i] - while entityId == nil do + while entityId == nil do lastArchetype += 1 - archetype = compatibleArchetypes[lastArchetype] - + archetype = compatible_archetypes[lastArchetype] if not archetype then - return - end - - entities = archetype.entities + return nil + end + entities = archetype.entities i = #entities - entityId = entities[i] - end - - local row = i - i-=1 - - local columns = archetype.columns - local tr = indices[lastArchetype] - - if queryLength == 1 then - return entityId, columns[tr[1]][row] - elseif queryLength == 2 then - return entityId, columns[tr[1]][row], columns[tr[2]][row] - elseif queryLength == 3 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] - elseif queryLength == 4 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row] - elseif queryLength == 5 then - return entityId,columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], - columns[tr[5]][row] - elseif queryLength == 6 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], - columns[tr[5]][row], - columns[tr[6]][row] - elseif queryLength == 7 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], - columns[tr[5]][row], - columns[tr[6]][row], - columns[tr[7]][row] - elseif queryLength == 8 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], - columns[tr[5]][row], - columns[tr[6]][row], - columns[tr[7]][row], - columns[tr[8]][row] - end - - for i in components do - queryOutput[i] = columns[tr[i]][row] - end + entityId = entities[i] + end + + local row = i + i-=1 + + local columns = archetype.columns + local tr = column_indices[lastArchetype] + + if queryLength == 1 then + return entityId, columns[tr[1]][row] + elseif queryLength == 2 then + return entityId, columns[tr[1]][row], columns[tr[2]][row] + elseif queryLength == 3 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] + elseif queryLength == 4 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row] + elseif queryLength == 5 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row] + elseif queryLength == 6 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row], + columns[tr[6]][row] + elseif queryLength == 7 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row], + columns[tr[6]][row], + columns[tr[7]][row] + elseif queryLength == 8 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row], + columns[tr[6]][row], + columns[tr[7]][row], + columns[tr[8]][row] + end + + for j in ids do + queryOutput[j] = columns[tr[j]][row] + end + + return entityId, unpack(queryOutput, 1, queryLength) + end - return entityId, unpack(queryOutput, 1, queryLength) + local function world_query_iter() + return world_query_next end - local function world_query_without(self, ...): Query + local function world_query_without(self, ...) local withoutComponents = { ... } - for i = #compatibleArchetypes, 1, -1 do - local archetype = compatibleArchetypes[i] + for i = #compatible_archetypes, 1, -1 do + local archetype = compatible_archetypes[i] local records = archetype.records local shouldRemove = false for _, componentId in withoutComponents do - if records[componentId] then - shouldRemove = true - break - end + if records[componentId] then + shouldRemove = true + break + end end if shouldRemove then - table.remove(compatibleArchetypes, i) + table.remove(compatible_archetypes, i) end - end - - if #compatibleArchetypes == 0 then - return EmptyQuery - end + end - return self - end - - local function world_query_iter() lastArchetype = 1 - archetype = compatibleArchetypes[1] - entities = archetype.entities - i = #entities + archetype = compatible_archetypes[lastArchetype] - return world_query_next + if not archetype then + return EmptyQuery + end + + return self end local function world_query_replace_values(row, columns, ...) - for i, column in columns do - column[row] = select(i, ...) - end + for i, column in columns do + column[row] = select(i, ...) + end end - local function world_query_replace(_, fn: any) - for i, archetype in compatibleArchetypes do - local tr = indices[i] + local function world_query_replace(_, fn: (...any) -> (...any)) + for i, archetype in compatible_archetypes do + local tr = column_indices[i] local columns = archetype.columns for row in archetype.entities do @@ -855,80 +891,88 @@ do end end - function world_query(world: World, ...: number): Query - -- breaking? - if (...) == nil then - error("Missing components") - end - - indices = {} - compatibleArchetypes = {} - length = 0 - components = { ... } - - local archetypes: { Archetype } = world.archetypes :: any - local firstArchetypeMap: ArchetypeMap - local componentIndex = world.componentIndex - - for _, componentId in components do - local map: ArchetypeMap = componentIndex[componentId] :: any - if not map then - return EmptyQuery - end - - if (firstArchetypeMap :: any) == nil or firstArchetypeMap.size > map.size then - firstArchetypeMap = map - end - end - - for id in firstArchetypeMap.cache do - local compatibleArchetype = archetypes[id] - local archetypeRecords = compatibleArchetype.records - - local records: { number } = {} - local skip = false - - for i, componentId in components do - local index = archetypeRecords[componentId] - if not index then - skip = true - break - end - -- index should be index.offset - records[i] = index - end - - if skip then - continue - end - - length += 1 - compatibleArchetypes[length] = compatibleArchetype - indices[length] = records - end - - lastArchetype = 1 - archetype = compatibleArchetypes[lastArchetype] - - if not archetype then - return EmptyQuery - end - - queryOutput = {} - queryLength = #components - - entities = archetype.entities - i = #entities - - local it = { - __iter = world_query_iter, - next = world_query_next, - without = world_query_without, - replace = world_query_replace - } - - return setmetatable(it, it) :: any - end + + function world_query(world: World, ...: any): Query + -- breaking? + if (...) == nil then + error("Missing components") + end + + local indices = {} + local compatibleArchetypes = {} + local length = 0 + + local components = { ... } :: any + local archetypes = world.archetypes + + local firstArchetypeMap: ArchetypeMap + local componentIndex = world.componentIndex + + for _, componentId in components do + local map = componentIndex[componentId] + if not map then + return EmptyQuery + end + + if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then + firstArchetypeMap = map + end + end + + for id in firstArchetypeMap.cache do + local compatibleArchetype = archetypes[id] + local archetypeRecords = compatibleArchetype.records + + local records = {} + local skip = false + + for i, componentId in components do + local index = archetypeRecords[componentId] + if not index then + skip = true + break + end + -- index should be index.offset + records[i] = index + end + + if skip then + continue + end + + length += 1 + compatibleArchetypes[length] = compatibleArchetype + indices[length] = records + end + + compatible_archetypes = compatibleArchetypes + column_indices = indices + ids = components + + lastArchetype = 1 + archetype = compatible_archetypes[lastArchetype] + + if not archetype then + return EmptyQuery + end + + queryOutput = {} + queryLength = #ids + + entities = archetype.entities + i = #entities + + local it = { + __iter = world_query_iter, + next = world_query_next, + without = world_query_without, + replace = world_query_replace + } :: any + + setmetatable(it, it) + + return it + end end type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53) @@ -1077,6 +1121,7 @@ World.component = world_component World.add = world_add World.set = world_set World.get = world_get +World.has = world_has World.target = world_target World.parent = world_parent diff --git a/test/tests.luau b/test/tests.luau index 67559cb5..f95fca68 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -60,10 +60,9 @@ TEST("world", function() world:clear(e) CHECK(world:get(e, A) == nil) CHECK(world:get(e, B) == nil) - end - do CASE("iterator should not drain the query") + do CASE("should drain query while iterating") local world = jecs.World.new() :: World local A = world:component() local B = world:component() @@ -85,7 +84,8 @@ TEST("world", function() for _ in q do j+=1 end - CHECK(i == j) + CHECK(i == 2) + CHECK(j == 0) end do CASE("should be able to get next results")