From 3e639db371ac38b4383d803c74bf78ea0df053f7 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sat, 3 Aug 2024 04:17:36 +0200 Subject: [PATCH 1/3] Fix upvalues conflict in nested queries --- src/init.luau | 562 +++++++++++++++++++++++++------------------------- 1 file changed, 282 insertions(+), 280 deletions(-) diff --git a/src/init.luau b/src/init.luau index a085d16..1ed450a 100644 --- a/src/init.luau +++ b/src/init.luau @@ -714,44 +714,277 @@ export type Query = typeof({ type CompatibleArchetype = { archetype: Archetype, indices: { number } } -local world_query: (World, ...i53) -> Query -do +local noop: Item = function() + return nil :: any +end - local noop: Item = function() - return nil :: any - end +local Arm = function(self: Query, ...) + return self +end +local EmptyQuery: Query = { + __iter = function(): Item + return noop + end, + drain = Arm, + next = noop :: Item, + replace = noop :: (Query, ...any) -> (), + with = Arm, + without = Arm, +} - local Arm = function(self: Query, ...) - return self - end - local EmptyQuery: Query = { - __iter = function(): Item - return noop - end, - drain = Arm, - next = noop :: Item, - replace = noop :: (Query, ...any) -> (), - with = Arm, - without = Arm, - } +setmetatable(EmptyQuery, EmptyQuery) - setmetatable(EmptyQuery, EmptyQuery) +local function world_query(world, ...) + -- breaking + if (...) == nil then + error("Missing components") + end - local lastArchetype: number - local archetype: Archetype - local queryOutput: { any } - local entities: { number } - local i: number + local compatible_archetypes = {} + local length = 0 - local compatible_archetypes: { Archetype } - local ids: { number } - local columns - - local A, B, C, D, E, F, G, H, I + local components = { ... } :: any + local A, B, C, D, E, F, G, H, I = ... local a, b, c, d, e, f, g, h - local init - local drain + 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 skip = false + + for i, componentId in components do + local index = archetypeRecords[componentId] + if not index then + skip = true + break + end + end + + if skip then + continue + end + + length += 1 + compatible_archetypes[length] = compatibleArchetype + end + + + local init = false + local drain = false + local ids = components + + local world_query_iter_next + + local lastArchetype = 1 + local archetype + local columns + local entities + local i + local queryOutput + + local function world_query_iter_create() + if not B then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + a = columns[records[A].column] + end + + local row = i + i-=1 + + return entityId, a[row] + end + elseif not C then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + a = columns[records[A].column] + b = columns[records[B].column] + end + + local row = i + i-=1 + + return entityId, a[row], b[row] + end + elseif not D then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + end + + local row = i + i-=1 + + return entityId, a[row], b[row], c[row] + end + elseif not E then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + end + + local row = i + i-=1 + + return entityId, a[row], b[row], c[row], d[row] + end + else + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + + if not F then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + elseif not G then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + elseif not H then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + elseif not I then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + h = columns[records[H].column] + end + end + + local row = i + i-=1 + + if not F then + return entityId, a[row], b[row], c[row], d[row], e[row] + elseif not G then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row] + elseif not H then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + elseif not I then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + end + + local field = archetype.records + for j, id in ids do + queryOutput[j] = columns[field[id].column][row] + end + + return entityId, unpack(queryOutput) + end + end + end + local function query_init(query) if init and drain then @@ -763,7 +996,7 @@ do archetype = compatible_archetypes[lastArchetype] if not archetype then - return + return false end queryOutput = {} @@ -808,205 +1041,19 @@ do e = columns[records[E].column] f = columns[records[F].column] g = columns[records[G].column] - elseif H then + elseif not I then a = columns[records[A].column] b = columns[records[B].column] - c = columns[records[D].column] + c = columns[records[C].column] + d = columns[records[D].column] e = columns[records[E].column] f = columns[records[F].column] g = columns[records[G].column] h = columns[records[H].column] end + return true end - local world_query_iter_next - - local function world_query_iter_create() - if not B then - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - a = columns[records[A].column] - end - - local row = i - i-=1 - - return entityId, a[row] - end - elseif not C then - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - a = columns[records[A].column] - b = columns[records[B].column] - end - - local row = i - i-=1 - - return entityId, a[row], b[row] - end - elseif not D then - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - end - - local row = i - i-=1 - - return entityId, a[row], b[row], c[row] - end - elseif not E then - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - end - - local row = i - i-=1 - - return entityId, a[row], b[row], c[row], d[row] - end - else - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - - if not F then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - elseif not G then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - elseif not H then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - g = columns[records[G].column] - elseif not I then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - g = columns[records[G].column] - h = columns[records[H].column] - end - end - - local row = i - i-=1 - - if not F then - return entityId, a[row], b[row], c[row], d[row], e[row] - elseif not G then - return entityId, a[row], b[row], c[row], d[row], e[row], f[row] - elseif not H then - return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row] - elseif not I then - return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] - end - - local field = archetype.records - for j, id in ids do - queryOutput[j] = columns[field[id].column][row] - end - - return entityId, unpack(queryOutput) - end - end - end - local function world_query_without(self, ...) local withoutComponents = { ... } for i = #compatible_archetypes, 1, -1 do @@ -1119,8 +1166,10 @@ do local function world_query_drain(query) drain = true - query_init(query) - return query + if query_init(query) then + return query + end + return EmptyQuery end local function world_query_iter(query) @@ -1135,6 +1184,10 @@ do return world_query_iter_next() end + if #compatible_archetypes == 0 then + return EmptyQuery + end + local it = { __iter = world_query_iter, drain = world_query_drain, @@ -1147,64 +1200,13 @@ do setmetatable(it, it) - function world_query(world: World, ...: any): Query - -- breaking - if (...) == nil then - error("Missing components") - end + drain = false + init = false + ids = components - compatible_archetypes = {} - local length = 0 + world_query_iter_create() - local components = { ... } :: any - A, B, C, D, E, F, G, H, I = ... - - 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 skip = false - - for i, componentId in components do - local index = archetypeRecords[componentId] - if not index then - skip = true - break - end - end - - if skip then - continue - end - - length += 1 - compatible_archetypes[length] = compatibleArchetype - end - - drain = false - init = false - ids = components - - world_query_iter_create() - - return it - end + return it end type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53) From e5634b10b2c56c831a4681ee2b87a3e6bb11be59 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sat, 3 Aug 2024 04:49:45 +0200 Subject: [PATCH 2/3] Fix upvalues conflict in nested queries --- src/init.luau | 918 +++++++++++++++++++++++++------------------------- 1 file changed, 458 insertions(+), 460 deletions(-) diff --git a/src/init.luau b/src/init.luau index 1ed450a..c7cccdd 100644 --- a/src/init.luau +++ b/src/init.luau @@ -721,364 +721,21 @@ end local Arm = function(self: Query, ...) return self end -local EmptyQuery: Query = { - __iter = function(): Item - return noop - end, - drain = Arm, - next = noop :: Item, - replace = noop :: (Query, ...any) -> (), - with = Arm, - without = Arm, -} +local world_query +do -setmetatable(EmptyQuery, EmptyQuery) + local EmptyQuery: Query = { + __iter = function(): Item + return noop + end, + drain = Arm, + next = noop :: Item, + replace = noop :: (Query, ...any) -> (), + with = Arm, + without = Arm, + } -local function world_query(world, ...) - -- breaking - if (...) == nil then - error("Missing components") - end - - local compatible_archetypes = {} - local length = 0 - - local components = { ... } :: any - local A, B, C, D, E, F, G, H, I = ... - local a, b, c, d, e, f, g, h - - 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 skip = false - - for i, componentId in components do - local index = archetypeRecords[componentId] - if not index then - skip = true - break - end - end - - if skip then - continue - end - - length += 1 - compatible_archetypes[length] = compatibleArchetype - end - - - local init = false - local drain = false - local ids = components - - local world_query_iter_next - - local lastArchetype = 1 - local archetype - local columns - local entities - local i - local queryOutput - - local function world_query_iter_create() - if not B then - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - a = columns[records[A].column] - end - - local row = i - i-=1 - - return entityId, a[row] - end - elseif not C then - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - a = columns[records[A].column] - b = columns[records[B].column] - end - - local row = i - i-=1 - - return entityId, a[row], b[row] - end - elseif not D then - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - end - - local row = i - i-=1 - - return entityId, a[row], b[row], c[row] - end - elseif not E then - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - end - - local row = i - i-=1 - - return entityId, a[row], b[row], c[row], d[row] - end - else - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - - if not F then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - elseif not G then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - elseif not H then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - g = columns[records[G].column] - elseif not I then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - g = columns[records[G].column] - h = columns[records[H].column] - end - end - - local row = i - i-=1 - - if not F then - return entityId, a[row], b[row], c[row], d[row], e[row] - elseif not G then - return entityId, a[row], b[row], c[row], d[row], e[row], f[row] - elseif not H then - return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row] - elseif not I then - return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] - end - - local field = archetype.records - for j, id in ids do - queryOutput[j] = columns[field[id].column][row] - end - - return entityId, unpack(queryOutput) - end - end - end - - - local function query_init(query) - if init and drain then - return - end - - init = true - lastArchetype = 1 - archetype = compatible_archetypes[lastArchetype] - - if not archetype then - return false - end - - queryOutput = {} - - entities = archetype.entities - i = #entities - columns = archetype.columns - - local records = archetype.records - if not B then - a = columns[records[A].column] - elseif not C then - a = columns[records[A].column] - b = columns[records[B].column] - elseif not D then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - elseif not E then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - elseif not F then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - elseif not G then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - elseif not H then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - g = columns[records[G].column] - elseif not I then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - g = columns[records[G].column] - h = columns[records[H].column] - end - return true - end - - local function world_query_without(self, ...) - local withoutComponents = { ... } - 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 - end - - if shouldRemove then - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil - end - end - - return self - end + setmetatable(EmptyQuery, EmptyQuery) local function world_query_replace_values(row, columns, ...) for i, column in columns do @@ -1086,133 +743,474 @@ local function world_query(world, ...) end end - local function world_query_replace(query, fn: (...any) -> (...any)) - query_init(query) + function world_query(world, ...) + -- breaking + if (...) == nil then + error("Missing components") + end - for i, archetype in compatible_archetypes do - local columns = archetype.columns - local tr = archetype.records - for row in archetype.entities do - if not B then - local va = columns[tr[A].column] - local pa = fn(va[row]) + local compatible_archetypes = {} + local length = 0 - va[row] = pa - elseif not C then - local va = columns[tr[A].column] - local vb = columns[tr[B].column] + local ids = { ... } + local A, B, C, D, E, F, G, H, I = ... + local a, b, c, d, e, f, g, h - va[row], vb[row] = fn(va[row], vb[row]) - elseif not D then - local va = columns[tr[A].column] - local vb = columns[tr[B].column] - local vc = columns[tr[C].column] + local archetypes = world.archetypes - va[row], vb[row], vc[row] = fn(va[row], vb[row], vc[row]) - elseif not E then - local va = columns[tr[A].column] - local vb = columns[tr[B].column] - local vc = columns[tr[C].column] - local vd = columns[tr[D].column] + local idr: ArchetypeMap + local componentIndex = world.componentIndex - va[row], vb[row], vc[row], vd[row] = fn( - va[row], vb[row], vc[row], vd[row]) - else - local field = archetype.records - for j, id in ids do - queryOutput[j] = columns[field[id].column][row] - end - world_query_replace_values(row, columns, - fn(unpack(queryOutput))) - end + for _, id in ids do + local map = componentIndex[id] + if not map then + return EmptyQuery + end + + if idr == nil or map.size < idr.size then + idr = map + end + end + + for archetype_id in idr.cache do + local compatibleArchetype = archetypes[archetype_id] + local tr = compatibleArchetype.records + + local skip = false + + for i, id in ids do + local index = tr[id] + if not index then + skip = true + break + end + end + + if skip then + continue + end + + length += 1 + compatible_archetypes[length] = compatibleArchetype + end + + if length == 0 then + return EmptyQuery + end + + local lastArchetype = 1 + local archetype + local columns + local entities + local i + local queryOutput + + local world_query_iter_next + + if not B then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + a = columns[records[A].column] end + + local row = i + i-=1 + + return entityId, a[row] end - end + elseif not C then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end - local function world_query_with(query, ...) - local ids = { ... } - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local records = archetype.records - local shouldRemove = false + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + a = columns[records[A].column] + b = columns[records[B].column] + end - for _, id in ids do - if not records[id] then - shouldRemove = true - break - end + local row = i + i-=1 + + return entityId, a[row], b[row] + end + elseif not D then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] end - if shouldRemove then - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] + local row = i + i-=1 + + return entityId, a[row], b[row], c[row] + end + elseif not E then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + end + + local row = i + i-=1 + + return entityId, a[row], b[row], c[row], d[row] + end + else + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + + if not F then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + elseif not G then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + elseif not H then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + elseif not I then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + h = columns[records[H].column] + end + end + + local row = i + i-=1 + + if not F then + return entityId, a[row], b[row], c[row], d[row], e[row] + elseif not G then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row] + elseif not H then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + elseif not I then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + end + + local field = archetype.records + for j, id in ids do + queryOutput[j] = columns[field[id].column][row] + end + + return entityId, unpack(queryOutput) + end + end + + local init = false + local drain = false + + local function query_init(query) + if init and drain then + return + end + + init = true + lastArchetype = 1 + archetype = compatible_archetypes[lastArchetype] + + if not archetype then + return false + end + + queryOutput = {} + + entities = archetype.entities + i = #entities + columns = archetype.columns + + local records = archetype.records + if not B then + a = columns[records[A].column] + elseif not C then + a = columns[records[A].column] + b = columns[records[B].column] + elseif not D then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + elseif not E then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + elseif not F then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + elseif not G then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + elseif not H then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + elseif not I then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + h = columns[records[H].column] + end + return true + end + + local function world_query_without(query, ...) + local withoutComponents = { ... } + 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 + end + + if shouldRemove then + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil + length -= 1 end - compatible_archetypes[last] = nil - end + end + + if length == 0 then + return EmptyQuery + end + + return query + end + + local function world_query_replace(query, fn: (...any) -> (...any)) + query_init(query) + + for i, archetype in compatible_archetypes do + local columns = archetype.columns + local tr = archetype.records + for row in archetype.entities do + if not B then + local va = columns[tr[A].column] + local pa = fn(va[row]) + + va[row] = pa + elseif not C then + local va = columns[tr[A].column] + local vb = columns[tr[B].column] + + va[row], vb[row] = fn(va[row], vb[row]) + elseif not D then + local va = columns[tr[A].column] + local vb = columns[tr[B].column] + local vc = columns[tr[C].column] + + va[row], vb[row], vc[row] = fn(va[row], vb[row], vc[row]) + elseif not E then + local va = columns[tr[A].column] + local vb = columns[tr[B].column] + local vc = columns[tr[C].column] + local vd = columns[tr[D].column] + + va[row], vb[row], vc[row], vd[row] = fn( + va[row], vb[row], vc[row], vd[row]) + else + local field = archetype.records + for j, id in ids do + queryOutput[j] = columns[field[id].column][row] + end + world_query_replace_values(row, columns, + fn(unpack(queryOutput))) + end + end + end + end + + local function world_query_with(query, ...) + local with = { ... } + for i = #compatible_archetypes, 1, -1 do + local archetype = compatible_archetypes[i] + local tr = archetype.records + local shouldRemove = false + + for _, id in with do + if not tr[id] then + shouldRemove = true + break + end + end + + if shouldRemove then + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil + length -= 1 + end + end + if length == 0 then + return EmptyQuery + end + return query + end + + -- Meant for directly iterating over archetypes to minimize + -- function call overhead. Should not be used unless iterating over + -- hundreds of thousands of entities in bulk. + local function world_query_archetypes() + return compatible_archetypes + end + + local function world_query_drain(query) + drain = true + if query_init(query) then + return query end + return EmptyQuery + end + local function world_query_iter(query) query_init(query) - - return query - end - - -- Meant for directly iterating over archetypes to minimize - -- function call overhead. Should not be used unless iterating over - -- hundreds of thousands of entities in bulk. - local function world_query_archetypes() - return compatible_archetypes - end - - local function world_query_drain(query) - drain = true - if query_init(query) then - return query + return world_query_iter_next end - return EmptyQuery - end - local function world_query_iter(query) - query_init(query) - return world_query_iter_next - end - - local function world_query_next() - if not drain then - error("Did you forget to call query:drain()?") + local function world_query_next(world) + if not drain then + error("Did you forget to call query:drain()?") + end + return world_query_iter_next(world) end - return world_query_iter_next() + + local it = { + __iter = world_query_iter, + drain = world_query_drain, + next = world_query_next, + with = world_query_with, + without = world_query_without, + replace = world_query_replace, + archetypes = world_query_archetypes + } :: any + + setmetatable(it, it) + + return it end - - if #compatible_archetypes == 0 then - return EmptyQuery - end - - local it = { - __iter = world_query_iter, - drain = world_query_drain, - next = world_query_next, - with = world_query_with, - without = world_query_without, - replace = world_query_replace, - archetypes = world_query_archetypes - } :: any - - setmetatable(it, it) - - drain = false - init = false - ids = components - - world_query_iter_create() - - return it end -type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53) -- __nominal_type_dont_use could not be any or T as it causes a type error -- or produces a union -export type Entity = number & { __nominal_type_dont_use: T } +export type Entity = number & { __DO_NOT_USE_OR_YOU_WILL_BE_FIRED: T } export type Pair = number export type QueryShim = typeof(setmetatable({ From a9f449b3fbf862fab462d11d8b8151ff63ce4c10 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sat, 3 Aug 2024 05:48:48 +0200 Subject: [PATCH 3/3] Example (#93) * Initial commit * Return static function * Fix upvalues conflict * Add examples to luau * rename example folder * Add queries example * Add changetracking example * Add wildcards example * Delete example.project.json --- demo.project.json | 71 ++++++ demo/.gitignore | 6 + demo/README.md | 17 ++ demo/src/client/init.client.luau | 1 + demo/src/server/init.server.luau | 1 + demo/src/shared/common.luau | 258 ++++++++++++++++++++++ examples/README.md | 11 + examples/luau/entities/basics.luau | 45 ++++ examples/luau/entities/hierarchy.luau | 125 +++++++++++ examples/luau/queries/basics.luau | 75 +++++++ examples/luau/queries/changetracking.luau | 242 ++++++++++++++++++++ examples/luau/queries/wildcards.luau | 37 ++++ test/btree.luau | 152 +++++++++++++ 13 files changed, 1041 insertions(+) create mode 100644 demo.project.json create mode 100644 demo/.gitignore create mode 100644 demo/README.md create mode 100644 demo/src/client/init.client.luau create mode 100644 demo/src/server/init.server.luau create mode 100644 demo/src/shared/common.luau create mode 100644 examples/README.md create mode 100644 examples/luau/entities/basics.luau create mode 100644 examples/luau/entities/hierarchy.luau create mode 100644 examples/luau/queries/basics.luau create mode 100644 examples/luau/queries/changetracking.luau create mode 100644 examples/luau/queries/wildcards.luau create mode 100644 test/btree.luau diff --git a/demo.project.json b/demo.project.json new file mode 100644 index 0000000..54314ef --- /dev/null +++ b/demo.project.json @@ -0,0 +1,71 @@ +{ + "name": "demo", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "Shared": { + "$path": "demo/src/shared" + }, + "ecs": { + "$path": "src" + } + }, + "ServerScriptService": { + "Server": { + "$path": "demo/src/server" + } + }, + "StarterPlayer": { + "StarterPlayerScripts": { + "Client": { + "$path": "demo/src/client" + } + } + }, + "Workspace": { + "$properties": { + "FilteringEnabled": true + }, + "Baseplate": { + "$className": "Part", + "$properties": { + "Anchored": true, + "Color": [ + 0.38823, + 0.37254, + 0.38823 + ], + "Locked": true, + "Position": [ + 0, + -10, + 0 + ], + "Size": [ + 512, + 20, + 512 + ] + } + } + }, + "Lighting": { + "$properties": { + "Ambient": [ + 0, + 0, + 0 + ], + "Brightness": 2, + "GlobalShadows": true, + "Outlines": false, + "Technology": "Voxel" + } + }, + "SoundService": { + "$properties": { + "RespectFilteringEnabled": true + } + } + } +} diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 0000000..cf9d94d --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1,6 @@ +# Project place file +/example.rbxlx + +# Roblox Studio lock files +/*.rbxlx.lock +/*.rbxl.lock \ No newline at end of file diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..5223a3e --- /dev/null +++ b/demo/README.md @@ -0,0 +1,17 @@ +# example +Generated by [Rojo](https://github.com/rojo-rbx/rojo) 7.4.1. + +## Getting Started +To build the place from scratch, use: + +```bash +rojo build -o "example.rbxlx" +``` + +Next, open `example.rbxlx` in Roblox Studio and start the Rojo server: + +```bash +rojo serve +``` + +For more help, check out [the Rojo documentation](https://rojo.space/docs). \ No newline at end of file diff --git a/demo/src/client/init.client.luau b/demo/src/client/init.client.luau new file mode 100644 index 0000000..505f71c --- /dev/null +++ b/demo/src/client/init.client.luau @@ -0,0 +1 @@ +print("Hello world, from client!") \ No newline at end of file diff --git a/demo/src/server/init.server.luau b/demo/src/server/init.server.luau new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/demo/src/server/init.server.luau @@ -0,0 +1 @@ + diff --git a/demo/src/shared/common.luau b/demo/src/shared/common.luau new file mode 100644 index 0000000..38b1ff3 --- /dev/null +++ b/demo/src/shared/common.luau @@ -0,0 +1,258 @@ +--!optimize 2 +--!native + +local jecs = require(game:GetService("ReplicatedStorage").ecs) + +type World = jecs.WorldShim +type Entity = jecs.Entity + +local function panic(str) + -- We don't want to interrupt the loop when we error + task.spawn(error, str) +end + +local function Scheduler(world, ...) + local systems = { ... } + local systemsNames = {} + local N = #systems + local system + local dt + + for i, module in systems do + local sys = require(module) + systems[i] = sys + local file, line = debug.info(2, "sl") + systemsNames[sys] = `{file}->::{line}::->{debug.info(sys, "n")}` + end + + local function run() + local name = systemsNames[system] + + debug.profilebegin(name) + debug.setmemorycategory(name) + system(world, dt) + debug.profileend() + end + + local function loop(sinceLastFrame) + debug.profilebegin("loop()") + + for i = N, 1, -1 do + system = systems[i] + + dt = sinceLastFrame + + local didNotYield, why = xpcall(function() + for _ in run do end + end, debug.traceback) + + if didNotYield then + continue + end + + if string.find(why, "thread is not yieldable") then + N -= 1 + local name = table.remove(systems, i) + panic("Not allowed to yield in the systems." + .. "\n" + .. `System: {name} has been ejected` + ) + else + panic(why) + end + end + + debug.profileend() + debug.resetmemorycategory() + end + + return loop +end + +type Tracker = { track: (world: World, fn: (changes: { + added: () -> () -> (number, T), + removed: () -> () -> number, + changed: () -> () -> (number, T, T) + }) -> ()) -> () +} + +type Entity = number & { __nominal_type_dont_use: T } + +local function diff(a, b) + local size = 0 + for k, v in a do + if b[k] ~= v then + return true + end + size += 1 + end + for k, v in b do + size -= 1 + end + + if size ~= 0 then + return true + end + + return false +end + +local function ChangeTracker(world, T: Entity): Tracker + local PreviousT = jecs.pair(jecs.Rest, T) + local add = {} + local added + local removed + local is_trivial + + local function changes_added() + added = true + local q = world:query(T):without(PreviousT):drain() + return function() + local id, data = q.next() + if not id then + return nil + end + + is_trivial = typeof(data) ~= "table" + + add[id] = data + + return id, data + end + end + + local function changes_changed() + local q = world:query(T, PreviousT):drain() + + return function() + local id, new, old = q.next() + while true do + if not id then + return nil + end + + if not is_trivial then + if diff(new, old) then + break + end + elseif new ~= old then + break + end + + id, new, old = q.next() + end + + local record = world.entityIndex.sparse[id] + local archetype = record.archetype + local column = archetype.records[PreviousT].column + local data = if is_trivial then new else table.clone(new) + archetype.columns[column][record.row] = data + + return id, old, new + end + end + + local function changes_removed() + removed = true + + local q = world:query(PreviousT):without(T):drain() + return function() + local id = q.next() + if id then + world:remove(id, PreviousT) + end + return id + end + end + + local changes = { + added = changes_added, + changed = changes_changed, + removed = changes_removed, + } + + local function track(fn) + added = false + removed = false + + fn(changes) + + if not added then + for _ in changes_added() do + end + end + + if not removed then + for _ in changes_removed() do + end + end + + for e, data in add do + world:set(e, PreviousT, if is_trivial then data else table.clone(data)) + end + end + + local tracker = { track = track } + + return tracker +end + +local bt +do + local SUCCESS = 0 + local FAILURE = 1 + local RUNNING = 2 + + local function SEQUENCE(nodes) + return function(...) + for _, node in nodes do + local status = node(...) + if status == FAILURE or status == RUNNING then + return status + end + end + return SUCCESS + end + end + local function FALLBACK(nodes) + return function(...) + for _, node in nodes do + local status = node(...) + if status == SUCCESS or status == RUNNING then + return status + end + end + return FAILURE + end + end + bt = { + SEQUENCE = SEQUENCE, + FALLBACK = FALLBACK, + RUNNING = RUNNING + } +end + +local function interval(s) + local pin + + local function throttle() + if not pin then + pin = os.clock() + end + + local elapsed = os.clock() - pin > s + if elapsed then + pin = os.clock() + end + + return elapsed + end + return throttle +end + +return { + Scheduler = Scheduler, + ChangeTracker = ChangeTracker, + interval = interval, + BehaviorTree = bt +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..68b04aa --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +# Examples + +This folder contains code examples for the Luau/Typescript APIs. + +## Run with Luau +To run the examples with Luau, run the following commands from the root of the repository: + +```sh +cd examples/luau +luau path/to/file.luau +``` diff --git a/examples/luau/entities/basics.luau b/examples/luau/entities/basics.luau new file mode 100644 index 0000000..cfebf6d --- /dev/null +++ b/examples/luau/entities/basics.luau @@ -0,0 +1,45 @@ +local jecs = require("@jecs") +local world = jecs.World.new() + +local Position = world:component() +local Walking = world:component() +local Name = world:component() + +-- Create an entity with name Bob +local bob = world:entity() + +-- The set operation finds or creates a component, and sets it. +world:set(bob, Position, Vector3.new(10, 20, 30)) +-- Name the entity Bob +world:set(bob, Name, "Bob") +-- The add operation adds a component without setting a value. This is +-- useful for tags, or when adding a component with its default value. +world:add(bob, Walking) + +-- Get the value for the Position component +local pos = world:get(bob, Position) +print(`\{{pos.X}, {pos.Y}, {pos.Z}\}`) + +-- Overwrite the value of the Position component +world:set(bob, Position, Vector3.new(40, 50, 60)) + + +local alice = world:entity() +-- Create another named entity +world:set(alice, Name, "Alice") +world:set(alice, Position, Vector3.new(10, 20, 30)) +world:add(alice, Walking) + + +-- Remove tag +world:remove(alice, Walking) + +-- Iterate all entities with Position +for entity, p in world:query(Position) do + print(`{entity}: \{{p.X}, {p.Y}, {p.Z}\}`) +end + +-- Output: +-- {10, 20, 30} +-- Alice: {10, 20, 30} +-- Bob: {40, 50, 60} diff --git a/examples/luau/entities/hierarchy.luau b/examples/luau/entities/hierarchy.luau new file mode 100644 index 0000000..d872509 --- /dev/null +++ b/examples/luau/entities/hierarchy.luau @@ -0,0 +1,125 @@ +local jecs = require("@jecs") +local pair = jecs.pair +local ChildOf = jecs.ChildOf +local world = jecs.World.new() + +local Name = world:component() +local Position = world:component() +local Star = world:component() +local Planet = world:component() +local Moon = world:component() + +local Vector3 +do + Vector3 = {} + Vector3.__index = Vector3 + + function Vector3.new(x, y, z) + x = x or 0 + y = y or 0 + z = z or 0 + return setmetatable({ X = x, Y = y, Z = z }, Vector3) + end + + function Vector3.__add(left, right) + return Vector3.new( + left.X + right.X, + left.Y + right.Y, + left.Z + right.Z + ) + end + + function Vector3.__mul(left, right) + if typeof(right) == "number" then + return Vector3.new( + left.X * right, + left.Y * right, + left.Z * right + ) + end + return Vector3.new( + left.X * right.X, + left.Y * right.Y, + left.Z * right.Z + ) + end + + Vector3.one = Vector3.new(1, 1, 1) + Vector3.zero = Vector3.new() +end + +local function path(entity) + local str = world:get(entity, Name) + local parent + while true do + parent = world:parent(entity) + if not parent then + break + end + entity = parent + str = world:get(parent, Name) .. "/" .. str + end + return str +end + +local function iterate(entity, parent) + local p = world:get(entity, Position) + local actual = p + parent + print(path(entity)) + print(`\{{actual.X}, {actual.Y}, {actual.Z}}`) + + for child in world:query(pair(ChildOf, entity)) do + --print(world:get(child, Name)) + iterate(child, actual) + end +end + +local sun = world:entity() +world:add(sun, Star) +world:set(sun, Position, Vector3.one) +world:set(sun, Name, "Sun") +do + local earth = world:entity() + world:set(earth, Name, "Earth") + world:add(earth, pair(ChildOf, sun)) + world:add(earth, Planet) + world:set(earth, Position, Vector3.one * 3) + + do + local moon = world:entity() + world:set(moon, Name, "Moon") + world:add(moon, pair(ChildOf, earth)) + world:add(moon, Moon) + world:set(moon, Position, Vector3.one * 0.1) + + print(`Child of Earth? {world:has(moon, pair(ChildOf, earth))}`) + end + + local venus = world:entity() + world:set(venus, Name, "Venus") + world:add(venus, pair(ChildOf, sun)) + world:add(venus, Planet) + world:set(venus, Position, Vector3.one * 2) + + local mercury = world:entity() + world:set(mercury, Name, "Mercury") + world:add(mercury, pair(ChildOf, sun)) + world:add(mercury, Planet) + world:set(mercury, Position, Vector3.one) + + + iterate(sun, Vector3.zero) +end + +-- Output: +-- Child of Earth? true +-- Sun +-- {1, 1, 1} +-- Sun/Mercury +-- {2, 2, 2} +-- Sun/Venus +-- {3, 3, 3} +-- Sun/Earth +-- {4, 4, 4} +-- Sun/Earth/Moon +-- {4.1, 4.1, 4.1} diff --git a/examples/luau/queries/basics.luau b/examples/luau/queries/basics.luau new file mode 100644 index 0000000..8dd9a35 --- /dev/null +++ b/examples/luau/queries/basics.luau @@ -0,0 +1,75 @@ +local jecs = require("@jecs") +local world = jecs.World.new() + +local Position = world:component() +local Velocity = world:component() +local Name = world:component() + +local Vector3 +do + Vector3 = {} + Vector3.__index = Vector3 + + function Vector3.new(x, y, z) + x = x or 0 + y = y or 0 + z = z or 0 + return setmetatable({ X = x, Y = y, Z = z }, Vector3) + end + + function Vector3.__add(left, right) + return Vector3.new( + left.X + right.X, + left.Y + right.Y, + left.Z + right.Z + ) + end + + function Vector3.__mul(left, right) + if typeof(right) == "number" then + return Vector3.new( + left.X * right, + left.Y * right, + left.Z * right + ) + end + return Vector3.new( + left.X * right.X, + left.Y * right.Y, + left.Z * right.Z + ) + end + + Vector3.one = Vector3.new(1, 1, 1) + Vector3.zero = Vector3.new() +end + +-- Create a few test entities for a Position, Velocity query +local e1 = world:entity() +world:set(e1, Name, "e1") +world:set(e1, Position, Vector3.new(10, 20, 30)) +world:set(e1, Velocity, Vector3.new(1, 2, 3)) + +local e2 = world:entity() +world:set(e2, Name, "e2") +world:set(e2, Position, Vector3.new(10, 20, 30)) +world:set(e2, Velocity, Vector3.new(4, 5, 6)) + +-- This entity will not match as it does not have Position, Velocity +local e3 = world:entity() +world:set(e3, Name, "e3") +world:set(e3, Position, Vector3.new(10, 20, 30)) + +-- Create an uncached query for Position, Velocity. +for entity, p, v in world:query(Position, Velocity) do + -- Iterate entities matching the query + p.X += v.X + p.Y += v.Y + p.Z += v.Z + + print(`{world:get(entity, Name)}: \{{p.X}, {p.Y}, {p.Z}}`) +end + +-- Output: +-- e2: {14, 25, 36} +-- e1: {11, 22, 33} diff --git a/examples/luau/queries/changetracking.luau b/examples/luau/queries/changetracking.luau new file mode 100644 index 0000000..20ed888 --- /dev/null +++ b/examples/luau/queries/changetracking.luau @@ -0,0 +1,242 @@ +local jecs = require("@jecs") + +type World = jecs.WorldShim + +type Tracker = { track: (world: World, fn: (changes: { + added: () -> () -> (number, T), + removed: () -> () -> number, + changed: () -> () -> (number, T, T) + }) -> ()) -> () +} + +local function diff(a, b) + local size = 0 + for k, v in a do + if b[k] ~= v then + return true + end + size += 1 + end + for k, v in b do + size -= 1 + end + + if size ~= 0 then + return true + end + + return false +end + +type Entity = number & { __nominal_type_dont_use: T } + +local function ChangeTracker(world, T: Entity): Tracker + local PreviousT = jecs.pair(jecs.Rest, T) + local add = {} + local added + local removed + local is_trivial + + local function changes_added() + added = true + local q = world:query(T):without(PreviousT):drain() + return function() + local id, data = q.next() + if not id then + return nil + end + + is_trivial = typeof(data) ~= "table" + + add[id] = data + + return id, data + end + end + + local function changes_changed() + local q = world:query(T, PreviousT):drain() + + return function() + local id, new, old = q.next() + while true do + if not id then + return nil + end + + if not is_trivial then + if diff(new, old) then + break + end + elseif new ~= old then + break + end + + id, new, old = q.next() + end + + add[id] = new + + return id, old, new + end + end + + local function changes_removed() + removed = true + + local q = world:query(PreviousT):without(T):drain() + return function() + local id = q.next() + if id then + world:remove(id, PreviousT) + end + return id + end + end + + local changes = { + added = changes_added, + changed = changes_changed, + removed = changes_removed, + } + + local function track(fn) + added = false + removed = false + + fn(changes) + + if not added then + for _ in changes_added() do + end + end + + if not removed then + for _ in changes_removed() do + end + end + + for e, data in add do + world:set(e, PreviousT, if is_trivial then data else table.clone(data)) + end + end + + local tracker = { track = track } + + return tracker +end + +local Vector3 +do + Vector3 = {} + Vector3.__index = Vector3 + + function Vector3.new(x, y, z) + x = x or 0 + y = y or 0 + z = z or 0 + return setmetatable({ X = x, Y = y, Z = z }, Vector3) + end + + function Vector3.__add(left, right) + return Vector3.new( + left.X + right.X, + left.Y + right.Y, + left.Z + right.Z + ) + end + + function Vector3.__mul(left, right) + if typeof(right) == "number" then + return Vector3.new( + left.X * right, + left.Y * right, + left.Z * right + ) + end + return Vector3.new( + left.X * right.X, + left.Y * right.Y, + left.Z * right.Z + ) + end + + Vector3.one = Vector3.new(1, 1, 1) + Vector3.zero = Vector3.new() +end + +local world = jecs.World.new() +local Name = world:component() + +local function named(ctr, name) + local e = ctr(world) + world:set(e, Name, name) + return e +end +local function name(e) + return world:get(e, Name) +end + +local Position = named(world.component, "Position") + +-- Create the ChangeTracker with the component type to track +local PositionTracker = ChangeTracker(world, Position) + +local e1 = named(world.entity, "e1") +world:set(e1, Position, Vector3.new(10, 20, 30)) + +local e2 = named(world.entity, "e2") +world:set(e2, Position, Vector3.new(10, 20, 30)) + +PositionTracker.track(function(changes) + -- You can iterate over different types of changes: Added, Changed, Removed + + -- added queries for every entity with a new Position component + for e, p in changes.added() do + print(`Added {e}: \{{p.X}, {p.Y}, {p.Z}}`) + end + + -- changed queries for entities who's changed their data since + -- last was it tracked + for _ in changes.changed() do + print([[This won't print because it is the first time + we are tracking the Position component]]) + end + + -- removed queries for entities who's removed their Position component + -- since last it was tracked + for _ in changes.removed() do + print([[This won't print because it is the first time + we are tracking the Position component]]) + end +end) + +world:set(e1, Position, Vector3.new(1, 1, 2) * 999) + +PositionTracker.track(function(changes) + for e, p in changes.added() do + print([[This won't never print no Position component was added + since last time we tracked]]) + end + + for e, old, new in changes.changed() do + print(`{name(e)}'s Position changed from \{{old.X}, {old.Y}, {old.Z}\} to \{{new.X}, {new.Y}, {new.Z}\}`) + end + + -- If you don't call e.g. changes.removed() then it will automatically drain its iterator and stage their changes. + -- This ensures you will not have any off-by-one frame errors. +end) + +world:remove(e2, Position) + +PositionTracker.track(function(changes) + for e in changes.removed() do + print(`Position was removed from {name(e)}`) + end +end) + +-- Output: +-- Added 265: {10, 20, 30} +-- Added 264: {10, 20, 30} +-- e1's Position changed from {10, 20, 30} to {999, 999, 1998} +-- Position was removed from e2 diff --git a/examples/luau/queries/wildcards.luau b/examples/luau/queries/wildcards.luau new file mode 100644 index 0000000..87bef3d --- /dev/null +++ b/examples/luau/queries/wildcards.luau @@ -0,0 +1,37 @@ +local jecs = require("@jecs") +local pair = jecs.pair +local world = jecs.World.new() +local Name = world:component() + +local function named(ctr, name) + local e = ctr(world) + world:set(e, Name, name) + return e +end +local function name(e) + return world:get(e, Name) +end + +local Eats = world:component() +local Apples = named(world.entity, "Apples") +local Oranges = named(world.entity, "Oranges") + +local bob = named(world.entity, "Bob") +world:set(bob, pair(Eats, Apples), 10) + +local alice = named(world.entity, "Alice") +world:set(alice, pair(Eats, Oranges), 5) + +-- Aliasing the wildcard to symbols improves readability and ease of writing +local __ = jecs.Wildcard + +-- Create a query that matches edible components +for entity, amount in world:query(pair(Eats, __)) do + -- Iterate the query + local food = world:target(entity, Eats) + print(`{name(entity)} eats {amount} {name(food)}`) +end + +-- Output: +-- Alice eats 5 Oranges +-- Bob eats 10 Apples diff --git a/test/btree.luau b/test/btree.luau new file mode 100644 index 0000000..e6f1957 --- /dev/null +++ b/test/btree.luau @@ -0,0 +1,152 @@ +-- original author @centauri +local bt +do + + local FAILURE = 0 + local SUCCESS = 1 + local RUNNING = 2 + + local function SEQUENCE(nodes) + return function(...) + for _, node in nodes do + local status = node(...) + if status == FAILURE or status == RUNNING then + return status + end + end + return SUCCESS + end + end + local function FALLBACK(nodes) + return function(...) + for _, node in nodes do + local status = node(...) + if status == SUCCESS or status == RUNNING then + return status + end + end + return FAILURE + end + end + bt = { + SEQUENCE = SEQUENCE, + FALLBACK = FALLBACK, + RUNNING = RUNNING, + SUCCESS = SUCCESS, + FAILURE = FAILURE, + } +end + +local SEQUENCE, FALLBACK = bt.SEQUENCE, bt.FALLBACK +local RUNNING, SUCCESS, FAILURE = bt.FAILURE, bt.SUCCESS, bt.FAILURE + +local btree = FALLBACK { + SEQUENCE { + function() + return 1 + end, + + function() + return 0 + end + }, + SEQUENCE { + function() + print(3) + local start = os.clock() + local now = os.clock() + while os.clock() - now < 4 do + print("yielding") + coroutine.yield() + end + return 0 + end + }, + function() + return 1 + end +} + +function wait(seconds) + local start = os.clock() + while os.clock() - start < seconds do end + return os.clock() - start +end + +local function panic(str) + -- We don't want to interrupt the loop when we error + coroutine.resume(coroutine.create(function() error(str) end)) +end + +local jecs = require("@jecs") +local world = jecs.World.new() + +local function Scheduler(world, ...) + local systems = { ... } + local systemsNames = {} + local N = #systems + local system + local dt + + for i, module in systems do + local sys = if typeof(module) == "function" then module else require(module) + systems[i] = sys + local file, line = debug.info(2, "sl") + systemsNames[sys] = `{file}->::{line}::->{debug.info(sys, "n")}` + end + + local function run() + local name = systemsNames[system] + + --debug.profilebegin(name) + --debug.setmemorycategory(name) + system(world, dt) + --debug.profileend() + end + + local function loop(sinceLastFrame) + --debug.profilebegin("loop()") + local start = os.clock() + for i = N, 1, -1 do + system = systems[i] + + dt = sinceLastFrame + + local didNotYield, why = xpcall(function() + for _ in run do end + end, debug.traceback) + + if didNotYield then + continue + end + + if string.find(why, "thread is not yieldable") then + N -= 1 + local name = table.remove(systems, i) + panic("Not allowed to yield in the systems." + .. "\n" + .. `System: {name} has been ejected` + ) + else + panic(why) + end + end + + --debug.profileend() + --debug.resetmemorycategory() + return os.clock() - start + end + + return loop +end + +local co = coroutine.create(btree) +local function ai(world, dt) + coroutine.resume(co) +end + +local loop = Scheduler(world, ai) + +while wait(0.2) do + print("frame time: ", loop(0.2)) +end