Fix column pointers invalidated by upvalues (#88)

* Initial commit

* Add types to world:has()
This commit is contained in:
Marcus 2024-07-26 02:55:36 +02:00 committed by GitHub
parent 76d1ff91c0
commit 0f8f9348f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 180 additions and 159 deletions

4
.gitignore vendored
View file

@ -53,7 +53,7 @@ WallyPatches
# Misc # Misc
roblox.toml roblox.toml
sourcemap.json sourcemap.json
drafts/*.lua drafts/
# Cached Vitepress (docs) # Cached Vitepress (docs)
@ -61,4 +61,4 @@ drafts/*.lua
/docs/.vitepress/dist /docs/.vitepress/dist
.vitepress/cache .vitepress/cache
.vitepress/dist .vitepress/dist

View file

@ -1,4 +1,3 @@
--!optimize 2 --!optimize 2
--!native --!native
--!strict --!strict
@ -17,6 +16,7 @@ type ArchetypeEdge = {
remove: Archetype, remove: Archetype,
} }
type Archetype = { type Archetype = {
id: number, id: number,
edges: { [i53]: ArchetypeEdge }, edges: { [i53]: ArchetypeEdge },
@ -669,7 +669,8 @@ do
end end
end end
local world_has: () -> boolean local world_has: (world: World, entityId: number, ...i53) -> boolean
do do
function world_has(world, entity_id, ...) function world_has(world, entity_id, ...)
local id = entity_id local id = entity_id
@ -704,8 +705,8 @@ export type Query = typeof({
end, end,
}) & { }) & {
next: Item, next: Item,
replace: (Query, ...any) -> (), without: (Query) -> Query,
without: (Query) -> Query replace: (Query, (...any) -> (...any)) -> ()
} }
type CompatibleArchetype = { archetype: Archetype, indices: { number } } type CompatibleArchetype = { archetype: Archetype, indices: { number } }
@ -714,135 +715,145 @@ local world_query: (World, ...i53) -> Query
do do
local noop: Item = function() local noop: Item = function()
return nil :: any return nil :: any
end end
local EmptyQuery: Query = { local EmptyQuery: Query = {
__iter = function(): Item __iter = function(): Item
return noop return noop
end, end,
next = noop :: Item, next = noop :: Item,
replace = noop :: (Query, ...any) -> (), replace = noop :: (Query, ...any) -> (),
without = function(self: Query, ...) without = function(self: Query, ...)
return self return self
end end
} }
setmetatable(EmptyQuery, EmptyQuery) setmetatable(EmptyQuery, EmptyQuery)
local indices: { { number } }
local compatibleArchetypes: { Archetype }
local length
local components: { number }
local queryLength: number
local lastArchetype: number local lastArchetype: number
local archetype: Archetype 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 function world_query_next(): any
local i: number
local function world_query_next()
local entityId = entities[i] local entityId = entities[i]
while entityId == nil do while entityId == nil do
lastArchetype += 1 lastArchetype += 1
archetype = compatibleArchetypes[lastArchetype] archetype = compatible_archetypes[lastArchetype]
if not archetype then if not archetype then
return return nil
end end
entities = archetype.entities
entities = archetype.entities
i = #entities i = #entities
entityId = entities[i] entityId = entities[i]
end end
local row = i local row = i
i-=1 i-=1
local columns = archetype.columns local columns = archetype.columns
local tr = indices[lastArchetype] local tr = column_indices[lastArchetype]
if queryLength == 1 then if queryLength == 1 then
return entityId, columns[tr[1]][row] return entityId, columns[tr[1]][row]
elseif queryLength == 2 then elseif queryLength == 2 then
return entityId, columns[tr[1]][row], columns[tr[2]][row] return entityId, columns[tr[1]][row], columns[tr[2]][row]
elseif queryLength == 3 then elseif queryLength == 3 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
elseif queryLength == 4 then elseif queryLength == 4 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row] return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row]
elseif queryLength == 5 then elseif queryLength == 5 then
return entityId,columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], return entityId,
columns[tr[5]][row] columns[tr[1]][row],
elseif queryLength == 6 then columns[tr[2]][row],
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], columns[tr[3]][row],
columns[tr[5]][row], columns[tr[4]][row],
columns[tr[6]][row] columns[tr[5]][row]
elseif queryLength == 7 then elseif queryLength == 6 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], return entityId,
columns[tr[5]][row], columns[tr[1]][row],
columns[tr[6]][row], columns[tr[2]][row],
columns[tr[7]][row] columns[tr[3]][row],
elseif queryLength == 8 then columns[tr[4]][row],
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], columns[tr[5]][row],
columns[tr[5]][row], columns[tr[6]][row]
columns[tr[6]][row], elseif queryLength == 7 then
columns[tr[7]][row], return entityId,
columns[tr[8]][row] columns[tr[1]][row],
end 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 for j in ids do
queryOutput[i] = columns[tr[i]][row] queryOutput[j] = columns[tr[j]][row]
end end
return entityId, unpack(queryOutput, 1, queryLength) return entityId, unpack(queryOutput, 1, queryLength)
end end
local function world_query_without(self, ...): Query local function world_query_iter()
return world_query_next
end
local function world_query_without(self, ...)
local withoutComponents = { ... } local withoutComponents = { ... }
for i = #compatibleArchetypes, 1, -1 do for i = #compatible_archetypes, 1, -1 do
local archetype = compatibleArchetypes[i] local archetype = compatible_archetypes[i]
local records = archetype.records local records = archetype.records
local shouldRemove = false local shouldRemove = false
for _, componentId in withoutComponents do for _, componentId in withoutComponents do
if records[componentId] then if records[componentId] then
shouldRemove = true shouldRemove = true
break break
end end
end end
if shouldRemove then if shouldRemove then
table.remove(compatibleArchetypes, i) table.remove(compatible_archetypes, i)
end end
end end
if #compatibleArchetypes == 0 then
return EmptyQuery
end
return self
end
local function world_query_iter()
lastArchetype = 1 lastArchetype = 1
archetype = compatibleArchetypes[1] archetype = compatible_archetypes[lastArchetype]
entities = archetype.entities
i = #entities
return world_query_next if not archetype then
return EmptyQuery
end
return self
end end
local function world_query_replace_values(row, columns, ...) local function world_query_replace_values(row, columns, ...)
for i, column in columns do for i, column in columns do
column[row] = select(i, ...) column[row] = select(i, ...)
end end
end end
local function world_query_replace(_, fn: any) local function world_query_replace(_, fn: (...any) -> (...any))
for i, archetype in compatibleArchetypes do for i, archetype in compatible_archetypes do
local tr = indices[i] local tr = column_indices[i]
local columns = archetype.columns local columns = archetype.columns
for row in archetype.entities do for row in archetype.entities do
@ -881,80 +892,88 @@ do
end end
end end
function world_query(world: World, ...: number): Query
-- breaking?
if (...) == nil then
error("Missing components")
end
indices = {} function world_query(world: World, ...: any): Query
compatibleArchetypes = {} -- breaking?
length = 0 if (...) == nil then
components = { ... } error("Missing components")
end
local archetypes: { Archetype } = world.archetypes :: any local indices = {}
local firstArchetypeMap: ArchetypeMap local compatibleArchetypes = {}
local componentIndex = world.componentIndex local length = 0
for _, componentId in components do local components = { ... } :: any
local map: ArchetypeMap = componentIndex[componentId] :: any local archetypes = world.archetypes
if not map then
return EmptyQuery
end
if (firstArchetypeMap :: any) == nil or firstArchetypeMap.size > map.size then local firstArchetypeMap: ArchetypeMap
firstArchetypeMap = map local componentIndex = world.componentIndex
end
end
for id in firstArchetypeMap.cache do for _, componentId in components do
local compatibleArchetype = archetypes[id] local map = componentIndex[componentId]
local archetypeRecords = compatibleArchetype.records if not map then
return EmptyQuery
end
local records: { number } = {} if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
local skip = false firstArchetypeMap = map
end
end
for i, componentId in components do for id in firstArchetypeMap.cache do
local index = archetypeRecords[componentId] local compatibleArchetype = archetypes[id]
if not index then local archetypeRecords = compatibleArchetype.records
skip = true
break
end
-- index should be index.offset
records[i] = index
end
if skip then local records = {}
continue local skip = false
end
length += 1 for i, componentId in components do
compatibleArchetypes[length] = compatibleArchetype local index = archetypeRecords[componentId]
indices[length] = records if not index then
end skip = true
break
end
-- index should be index.offset
records[i] = index
end
lastArchetype = 1 if skip then
archetype = compatibleArchetypes[lastArchetype] continue
end
if not archetype then length += 1
return EmptyQuery compatibleArchetypes[length] = compatibleArchetype
end indices[length] = records
end
queryOutput = {} compatible_archetypes = compatibleArchetypes
queryLength = #components column_indices = indices
ids = components
entities = archetype.entities lastArchetype = 1
i = #entities archetype = compatible_archetypes[lastArchetype]
local it = { if not archetype then
__iter = world_query_iter, return EmptyQuery
next = world_query_next, end
without = world_query_without,
replace = world_query_replace
}
return setmetatable(it, it) :: any queryOutput = {}
end 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 end
type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53) type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53)
@ -997,11 +1016,13 @@ export type WorldShim = typeof(setmetatable(
--- Removes a component from the given entity --- Removes a component from the given entity
remove: (WorldShim, id: Entity, component: Entity) -> (), remove: (WorldShim, id: Entity, component: Entity) -> (),
--- Retrieves the value of up to 4 components. These values may be nil. --- Retrieves the value of up to 4 components. These values may be nil.
get: (<A>(WorldShim, id: any, Entity<A>) -> A) get: (<A>(WorldShim, id: Entity, Entity<A>) -> A)
& (<A, B>(WorldShim, id: Entity, Entity<A>, Entity<B>) -> (A, B)) & (<A, B>(WorldShim, id: Entity, Entity<A>, Entity<B>) -> (A, B))
& (<A, B, C>(WorldShim, id: Entity, Entity<A>, Entity<B>, Entity<C>) -> (A, B, C)) & (<A, B, C>(WorldShim, id: Entity, Entity<A>, Entity<B>, Entity<C>) -> (A, B, C))
& <A, B, C, D>(WorldShim, id: Entity, Entity<A>, Entity<B>, Entity<C>, Entity<D>) -> (A, B, C, D), & <A, B, C, D>(WorldShim, id: Entity, Entity<A>, Entity<B>, Entity<C>, Entity<D>) -> (A, B, C, D),
has: (WorldShim, Entity, ...Entity) -> boolean,
--- Searches the world for entities that match a given query --- Searches the world for entities that match a given query
query: (<A>(WorldShim, Entity<A>) -> QueryShim<A>) query: (<A>(WorldShim, Entity<A>) -> QueryShim<A>)
& (<A, B>(WorldShim, Entity<A>, Entity<B>) -> QueryShim<A, B>) & (<A, B>(WorldShim, Entity<A>, Entity<B>) -> QueryShim<A, B>)

View file

@ -60,10 +60,9 @@ TEST("world", function()
world:clear(e) world:clear(e)
CHECK(world:get(e, A) == nil) CHECK(world:get(e, A) == nil)
CHECK(world:get(e, B) == nil) CHECK(world:get(e, B) == nil)
end end
do CASE("iterator should not drain the query") do CASE("should drain query while iterating")
local world = jecs.World.new() :: World local world = jecs.World.new() :: World
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
@ -85,7 +84,8 @@ TEST("world", function()
for _ in q do for _ in q do
j+=1 j+=1
end end
CHECK(i == j) CHECK(i == 2)
CHECK(j == 0)
end end
do CASE("should be able to get next results") do CASE("should be able to get next results")