Add query:fini and query:archetypes(override) and changes to OB

This commit is contained in:
Ukendio 2026-02-16 01:45:41 +01:00
parent d4a7f1d86c
commit 30597ed389
6 changed files with 530 additions and 235 deletions

View file

@ -47,6 +47,8 @@ end
-- local tgt = tonumber(tokens[2]) :: jecs.Entity
-- rel = ecs_ensure_entity(world, rel)
--
-- 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
-- tgt = ecs_ensure_entity(world, tgt)
-- return jecs.pair(rel, tgt)

View file

@ -1,81 +1,81 @@
--[[
Signals let you subscribe to component add, change, and remove events with
multiple listeners per component. Unlike hooks (see 110_hooks.luau), which
allow only one OnAdd, OnChange, and OnRemove per component, signals support
any number of subscribers and each subscription returns an unsubscribe
function so you can clean up when you no longer need to listen.
Use signals when you need several independent systems to react to the same
component lifecycle events, or when you want to subscribe and unsubscribe
dynamically (e.g. a UI that only cares while it's mounted).
]]
local jecs = require("@jecs")
local world = jecs.world()
local Position = world:component() :: jecs.Id<{ x: number, y: number }>
--[[
world:added(component, fn)
Subscribe to "component added" events. Your callback is invoked with:
(entity, id, value, oldarchetype) whenever the component is added to an entity.
Returns a function; call it to unsubscribe.
]]
local unsub_added = world:added(Position, function(entity, id, value, oldarchetype)
print(`Position added to entity {entity}: ({value.x}, {value.y})`)
end)
--[[
world:changed(component, fn)
Subscribe to "component changed" events. Your callback is invoked with:
(entity, id, value, oldarchetype) whenever the component's value is updated
on an entity (e.g. via world:set).
Returns a function; call it to unsubscribe.
]]
local unsub_changed = world:changed(Position, function(entity, id, value, oldarchetype)
print(`Position changed on entity {entity}: ({value.x}, {value.y})`)
end)
--[[
world:removed(component, fn)
Subscribe to "component removed" events. Your callback is invoked with:
(entity, id, delete?) when the component is removed. The third argument
`delete` is true when the entity is being deleted, false or nil when
only the component was removed (same semantics as OnRemove in 110_hooks).
Returns a function; call it to unsubscribe.
]]
local unsub_removed = world:removed(Position, function(entity, id, delete)
if delete then
print(`Entity {entity} deleted (had Position)`)
else
print(`Position removed from entity {entity}`)
end
end)
local e = world:entity()
world:set(e, Position, { x = 10, y = 20 }) -- added
world:set(e, Position, { x = 30, y = 40 }) -- changed
world:remove(e, Position) -- removed
world:added(Position, function(entity)
print("Second listener: Position added")
end)
world:set(e, Position, { x = 0, y = 0 }) -- Multiple listeners are all invoked
-- Unsubscribe when you no longer need to listen
unsub_added()
unsub_changed()
unsub_removed()
world:set(e, Position, { x = 1, y = 1 })
world:remove(e, Position)
--[[
Signals let you subscribe to component add, change, and remove events with
multiple listeners per component. Unlike hooks (see 110_hooks.luau), which
allow only one OnAdd, OnChange, and OnRemove per component, signals support
any number of subscribers and each subscription returns an unsubscribe
function so you can clean up when you no longer need to listen.
Use signals when you need several independent systems to react to the same
component lifecycle events, or when you want to subscribe and unsubscribe
dynamically (e.g. a UI that only cares while it's mounted).
]]
local jecs = require("@jecs")
local world = jecs.world()
local Position = world:component() :: jecs.Id<{ x: number, y: number }>
--[[
world:added(component, fn)
Subscribe to "component added" events. Your callback is invoked with:
(entity, id, value, oldarchetype) whenever the component is added to an entity.
Returns a function; call it to unsubscribe.
]]
local unsub_added = world:added(Position, function(entity, id, value, oldarchetype)
print(`Position added to entity {entity}: ({value.x}, {value.y})`)
end)
--[[
world:changed(component, fn)
Subscribe to "component changed" events. Your callback is invoked with:
(entity, id, value, oldarchetype) whenever the component's value is updated
on an entity (e.g. via world:set).
Returns a function; call it to unsubscribe.
]]
local unsub_changed = world:changed(Position, function(entity, id, value, oldarchetype)
print(`Position changed on entity {entity}: ({value.x}, {value.y})`)
end)
--[[
world:removed(component, fn)
Subscribe to "component removed" events. Your callback is invoked with:
(entity, id, delete?) when the component is removed. The third argument
`delete` is true when the entity is being deleted, false or nil when
only the component was removed (same semantics as OnRemove in 110_hooks).
Returns a function; call it to unsubscribe.
]]
local unsub_removed = world:removed(Position, function(entity, id, delete)
if delete then
print(`Entity {entity} deleted (had Position)`)
else
print(`Position removed from entity {entity}`)
end
end)
local e = world:entity()
world:set(e, Position, { x = 10, y = 20 }) -- added
world:set(e, Position, { x = 30, y = 40 }) -- changed
world:remove(e, Position) -- removed
world:added(Position, function(entity)
print("Second listener: Position added")
end)
world:set(e, Position, { x = 0, y = 0 }) -- Multiple listeners are all invoked
-- Unsubscribe when you no longer need to listen
unsub_added()
unsub_changed()
unsub_removed()
world:set(e, Position, { x = 1, y = 1 })
world:remove(e, Position)

View file

@ -3,8 +3,17 @@ local jecs = require("@jecs")
type World = jecs.World
type Id<T=any> = jecs.Id<T>
type Id<T=any> = jecs.Id<any>
local function duplicate(query: jecs.Query<...any>): jecs.CachedQuery<...any>
local world = (query :: jecs.Query<any> & { world: World }).world
local dup = world:query()
dup.filter_with = table.clone(query.filter_with)
if query.filter_without then
dup.filter_without = query.filter_without
end
return dup:cached()
end
export type Observer = {
disconnect: () -> (),
@ -20,7 +29,7 @@ local function observers_new(
query: jecs.Query<...any>,
callback: (jecs.Entity) -> ()
): Observer
local cachedquery = query:cached()
local cachedquery = duplicate(query)
local world = (cachedquery :: jecs.Query<any> & { world: World }).world
callback = callback
@ -134,7 +143,7 @@ local function observers_new(
end
local function monitors_new(query: jecs.Query<...any>): Monitor
local cachedquery = query:cached()
local cachedquery = duplicate(query)
local world = (cachedquery :: jecs.Query<...any> & { world: World }).world :: jecs.World
@ -151,16 +160,10 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
local callback_added: ((jecs.Entity) -> ())?
local callback_removed: ((jecs.Entity) -> ())?
-- NOTE(marcus): Track the last (entity, old archetype) pair we processed to detect bulk operations.
-- During bulk_insert from ROOT_ARCHETYPE, the entity is moved to the target archetype first,
-- then all on_add callbacks fire sequentially with the same oldarchetype for the same entity.
-- We track both entity and old archetype to distinguish between:
-- 1. Same entity, same old archetype (bulk operation - skip)
-- 2. Different entity, same old archetype (separate operation - don't skip)
local last_old_archetype: jecs.Archetype? = nil
local last_entity: jecs.Entity? = nil
local function emplaced<a>(
local function emplaced<a>(
entity: jecs.Entity,
id: jecs.Id<a>,
value: a,
@ -173,10 +176,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any) :: jecs.Record
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
-- NOTE(marcus): Skip if we've seen this exact (entity, old archetype) combination before
-- AND this component is in the query's terms. This detects bulk operations where
-- the same entity transitions with multiple components, while allowing different
-- entities to trigger even if they share the same old archetype.
if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then
return
end
@ -185,52 +184,31 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
last_entity = entity
callback_added(entity)
else
-- NOTE(marcus): Clear tracking when we see a different transition pattern
last_old_archetype = nil
last_entity = nil
end
end
-- Track which entity we've already processed for deletion to avoid duplicate callbacks
-- during bulk deletion where multiple components are removed with delete=true
local last_deleted_entity: jecs.Entity? = nil
local function removed(entity: jecs.Entity, component: jecs.Component, delete:boolean?)
if callback_removed == nil then
return
end
local r = jecs.record(world, entity)
if not r then return end
local src = r.archetype
if not src then return end
if not archetypes[src.id] then return end
if delete then
-- Deletion is a bulk removal - all components are removed with delete=true
-- We should only trigger the callback once per entity, not once per component
if last_deleted_entity == entity then
return
end
local r = jecs.record(world, entity)
if r and r.archetype and archetypes[r.archetype.id] then
-- Entity was in the monitor before deletion
last_deleted_entity = entity
-- Clear tracking when entity is deleted
last_old_archetype = nil
last_entity = nil
callback_removed(entity)
end
if last_entity == entity and last_old_archetype == src then
return
end
local r = jecs.record(world, entity)
local src = r.archetype
local dst = jecs.archetype_traverse_remove(world, component, src)
if not archetypes[dst.id] then
-- Clear tracking when entity leaves the monitor to allow re-entry
last_old_archetype = nil
last_entity = nil
last_deleted_entity = nil
callback_removed(entity)
end
last_entity = entity
last_old_archetype = src
callback_removed(entity)
end
local cleanup = {}
@ -254,8 +232,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
entity_index, entity :: any) :: jecs.Record
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
-- NOTE(marcus): Skip if we've seen this exact (entity, old archetype) combination before
-- AND this component is in the query's terms.
if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then
return
end
@ -264,7 +240,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
last_entity = entity
callback_added(entity)
else
-- Clear tracking when we see a different transition pattern
last_old_archetype = nil
last_entity = nil
end
@ -314,11 +289,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
return
end
-- NOTE(marcus): This check that it was presently in
-- the query but distinctively leaves is important as
-- sometimes it could be too eager to report that it
-- removed a component even though the entity is not
-- apart of the monitor
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
last_old_archetype = nil
callback_removed(entity)
@ -364,10 +334,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
return
end
-- NOTE(marcus): Sometimes OnAdd listeners for excluded
-- terms are too eager to report that it is leaving the
-- monitor even though the entity is not apart of it
-- already.
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
callback_removed(entity)
end
@ -386,11 +352,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
end
local dst = jecs.archetype_traverse_remove(world, id, archetype)
-- NOTE(marcus): Inversely with the opposite operation, you
-- only need to check if it is going to enter the query once
-- because world:remove already stipulates that it is
-- idempotent so that this hook won't be invoked if it is
-- was already removed.
if archetypes[dst.id] then
callback_added(entity)
end

View file

@ -80,15 +80,17 @@ export type Id2<First, Second=nil> = ecs_id_t<First, Second>
export type Item<T...> = (self: Query<T...>) -> (Entity, T...)
export type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
export type CachedIter<T...> = (query: CachedQuery<T...>) -> () -> (Entity, T...)
export type Cached_Query_Iter<T...> = (query: Cached_Query<T...>) -> () -> (Entity, T...)
type TypePack<T...> = (T...) -> never
export type CachedQuery<T...> = typeof(setmetatable(
export type Cached_Query<T...> = typeof(setmetatable(
{} :: {
iter: CachedIter<T...>,
archetypes: (CachedQuery<T...>) -> { Archetype },
has: (CachedQuery<T...>, Entity) -> boolean,
iter: Cached_Query_Iter<T...>,
archetypes: (Cached_Query<T...>) -> { Archetype },
has: (Cached_Query<T...>, Entity) -> boolean,
fini: (Cached_Query<T...>) -> (),
ids: { Id<any> },
filter_with: { Id<any> }?,
filter_without: { Id<any> }?,
@ -96,7 +98,7 @@ export type CachedQuery<T...> = typeof(setmetatable(
-- world: World
},
{} :: {
__iter: CachedIter<T...>,
__iter: Cached_Query_Iter<T...>,
})
)
@ -106,7 +108,7 @@ export type Query<T...> = typeof(setmetatable(
with: ((Query<T...>, ...Component) -> Query<T...>),
without: ((Query<T...>, ...Component) -> Query<T...>),
archetypes: (Query<T...>) -> { Archetype },
cached: (Query<T...>) -> CachedQuery<T...>,
cached: (Query<T...>) -> Cached_Query<T...>,
has: (Query<T...>, Entity) -> boolean,
ids: { Id<any> },
filter_with: { Id<any> }?,
@ -1240,9 +1242,9 @@ end
local function NOOP() end
local function query_archetypes(query: query)
local function query_archetypes(query: query, override: boolean?)
local compatible_archetypes = query.compatible_archetypes
if not compatible_archetypes then
if not compatible_archetypes or override then
compatible_archetypes = {}
query.compatible_archetypes = compatible_archetypes
@ -1747,80 +1749,80 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
col4_u = col4
col5_u = col5
col6_u = col6
col7_u = col7
end
local row = i_u
i_u -= 1
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row]
end
else
local output = {}
local ids_len = #ids_u
function world_query_iter_next(): any
local entities = entities_u
local e = entities[i_u]
local col0 = col0_u
local col1 = col1_u
local col2 = col2_u
local col3 = col3_u
local col4 = col4_u
local col5 = col5_u
local col6 = col6_u
local col7 = col7_u
local ids = ids_u
local columns_map = columns_map_u
while e == nil do
last_archetype_u += 1
local compatible_archetypes = compatible_archetypes_u
local archetype = compatible_archetypes[last_archetype_u]
archetype_u = archetype
if not archetype then
return nil
col7_u = col7
end
entities = archetype.entities
i_u = #entities
if i_u == 0 then
continue
local row = i_u
i_u -= 1
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row]
end
else
local output = {}
local ids_len = #ids_u
function world_query_iter_next(): any
local entities = entities_u
local e = entities[i_u]
local col0 = col0_u
local col1 = col1_u
local col2 = col2_u
local col3 = col3_u
local col4 = col4_u
local col5 = col5_u
local col6 = col6_u
local col7 = col7_u
local ids = ids_u
local columns_map = columns_map_u
while e == nil do
last_archetype_u += 1
local compatible_archetypes = compatible_archetypes_u
local archetype = compatible_archetypes[last_archetype_u]
archetype_u = archetype
if not archetype then
return nil
end
entities = archetype.entities
i_u = #entities
if i_u == 0 then
continue
end
e = entities[i_u]
entities_u = entities
columns_map = archetype.columns_map
columns_map_u = columns_map
col0 = columns_map[id0]
col1 = columns_map[id1]
col2 = columns_map[id2]
col3 = columns_map[id3]
col4 = columns_map[id4]
col5 = columns_map[id5]
col6 = columns_map[id6]
col7 = columns_map[id7]
col0_u = col0
col1_u = col1
col2_u = col2
col3_u = col3
col4_u = col4
col5_u = col5
col6_u = col6
col7_u = col7
end
e = entities[i_u]
entities_u = entities
columns_map = archetype.columns_map
columns_map_u = columns_map
col0 = columns_map[id0]
col1 = columns_map[id1]
col2 = columns_map[id2]
col3 = columns_map[id3]
col4 = columns_map[id4]
col5 = columns_map[id5]
col6 = columns_map[id6]
col7 = columns_map[id7]
col0_u = col0
col1_u = col1
col2_u = col2
col3_u = col3
col4_u = col4
col5_u = col5
col6_u = col6
col7_u = col7
local row = i_u
i_u -= 1
for i = 9, ids_len do
output[i - 8] = columns_map[ids[i]::any][row]
end
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row], unpack(output)
end
local row = i_u
i_u -= 1
for i = 9, ids_len do
output[i - 8] = columns_map[ids[i]::any][row]
end
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row], unpack(output)
end
end
query.next = world_query_iter_next
return world_query_iter_next
query.next = world_query_iter_next
return world_query_iter_next
end
local function query_iter(query): () -> (number, ...any)
@ -1917,7 +1919,7 @@ local function query_cached(query: QueryInner)
table.insert(query_cache_on_create, observer_for_create)
table.insert(query_cache_on_delete, observer_for_delete)
local function cached_query_iter()
local function cached_query_iter()
last_archetype_u = 1
local compatible_archetypes = compatible_archetypes_u
archetype_u = compatible_archetypes[last_archetype_u]
@ -2416,7 +2418,7 @@ local function query_cached(query: QueryInner)
local eindex = world.entity_index :: entityindex
local function cached_query_has(entity): boolean
local function cached_query_has(_, entity): boolean
local r = entity_index_try_get_fast(eindex, entity)
if not r then
return false
@ -2429,12 +2431,31 @@ local function query_cached(query: QueryInner)
return archetypes_map[entityarchetype.id] ~= nil
end
local function cached_query_fini()
local create_pos = table.find(query_cache_on_create, observer_for_create)
if create_pos then
table.remove(query_cache_on_create, create_pos)
end
local delete_pos = table.find(query_cache_on_delete, observer_for_delete)
if delete_pos then
table.remove(query_cache_on_delete, delete_pos)
end
compatible_archetypes_u = nil
-- NOTE(marcus): Maybe we have to be even more aggressive with cleaning
-- things up to ensure it the memory is free`d. But since most of it are
-- references we cannot be sure that someone is holding onto them making
-- it implausible to free the memory anyways
end
local cached_query = query :: any
cached_query.archetypes = query_archetypes
cached_query.__iter = cached_query_iter
cached_query.iter = cached_query_iter
cached_query.has = cached_query_has
cached_query.fini = cached_query_fini
setmetatable(cached_query, cached_query)
return cached_query
end

View file

@ -3,7 +3,7 @@ local testkit = require("@modules/testkit")
local test = testkit.test()
local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
local FOCUS = test.FOCUS
local ob = require("@modules/ob")
local ob = require("@modules/OB/module")
TEST("modules/ob::observer", function()
local world = jecs.world()
@ -292,6 +292,36 @@ end)
TEST("modules/ob::monitor", function()
local world = jecs.world()
do CASE "same query can be used for multiple monitors without error (monitors use own cached proxy)"
local A = world:component()
local q = world:query(A)
local c1, c2 = 0, 0
ob.monitor(q).added(function() c1 += 1 end)
ob.monitor(q).added(function() c2 += 1 end)
local e = world:entity()
world:add(e, A)
CHECK(c1 == 1)
CHECK(c2 == 1)
end
do CASE [[Monitor should only report removed entities if it was previously
apart of it]]
local A = world:component()
local B = world:component()
local C = world:component()
local count = 0
ob.monitor(world:query(A, C)).removed(function()
count += 1
end)
local e = world:entity()
jecs.bulk_insert(world, e, {A, B}, {0,0})
CHECK(count==0)
world:remove(e, A)
CHECK(count==0)
end
do CASE [[should not invoke monitor.added callback multiple times in a bulk_move
]]
local A = world:component()
@ -609,6 +639,74 @@ TEST("modules/ob::monitor", function()
CHECK(r==2)
end
-- Without-clause pair onremoved path: guards must stay or these fail.
-- Removing "if archetypes[dst.id]" would make this fail: we must only report added when dst matches the query.
do CASE "without(pair): removing one excluded pair only fires added when entity actually enters (dst matches query)"
local A = world:component()
local B = world:component()
local e1 = world:entity()
local e2 = world:entity()
local added_count = 0
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, e1), jecs.pair(B, e2)))
monitor.added(function()
added_count += 1
end)
local e = world:entity()
world:add(e, jecs.pair(B, e1))
world:add(e, jecs.pair(B, e2))
world:add(e, A)
CHECK(added_count == 0)
world:remove(e, jecs.pair(B, e1))
CHECK(added_count == 0)
world:remove(e, jecs.pair(B, e2))
CHECK(added_count == 1)
end
-- Removing "if delete then return" would make this fail: must not report added when removal is due to delete.
do CASE "without(pair): must not report added when pair is removed due to entity delete"
local A = world:component()
local B = world:component()
local e1 = world:entity()
local added_count = 0
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, e1)))
monitor.added(function()
added_count += 1
end)
local e = world:entity()
world:add(e, A)
CHECK(added_count == 1)
world:add(e, jecs.pair(B, e1))
world:delete(e)
CHECK(added_count == 1)
end
-- Removing "if not wc and id ~= term then return" would make this fail: must only react to removal of the excluded term, not other pairs.
do CASE "without(pair): must not report added when a different pair (same relation) is removed"
local A = world:component()
local B = world:component()
local e1 = world:entity()
local e2 = world:entity()
local added_count = 0
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, e1)))
monitor.added(function()
added_count += 1
end)
local e = world:entity()
world:add(e, A)
CHECK(added_count == 1)
world:add(e, jecs.pair(B, e2))
world:remove(e, jecs.pair(B, e2))
CHECK(added_count == 1)
end
do CASE "Should enter monitor at query:without(pair(R, *)) when adding pair(R, t1)"
local A = world:component()
local B = world:component()
@ -792,7 +890,206 @@ TEST("modules/ob::monitor", function()
CHECK(c == 2)
end
if true then return end
do CASE "entity about to leave (remove queried pair) reports to removed exactly once"
local A = world:component()
local e1 = world:entity()
local monitor = ob.monitor(world:query(jecs.pair(A, e1)))
local removed_count = 0
monitor.removed(function()
removed_count += 1
end)
local e = world:entity()
world:add(e, jecs.pair(A, e1))
world:remove(e, jecs.pair(A, e1))
CHECK(removed_count == 1)
end
do CASE "removing non-queried pair (same relation, other target) must not report to removed"
local A = world:component()
local e1 = world:entity()
local e2 = world:entity()
local monitor = ob.monitor(world:query(jecs.pair(A, e1)))
local removed_count = 0
monitor.removed(function()
removed_count += 1
end)
local e = world:entity()
world:add(e, jecs.pair(A, e2))
world:remove(e, jecs.pair(A, e2))
CHECK(removed_count == 0)
end
do CASE "about-to-leave reports to removed exactly once per exit; second removal (already out) must not report"
local A = world:component()
local e1 = world:entity()
local e2 = world:entity()
local monitor = ob.monitor(world:query(jecs.pair(A, e1), jecs.pair(A, e2)))
local removed_count = 0
monitor.removed(function()
removed_count += 1
end)
local e = world:entity()
world:add(e, jecs.pair(A, e1))
world:add(e, jecs.pair(A, e2))
world:remove(e, jecs.pair(A, e1))
CHECK(removed_count == 1)
world:remove(e, jecs.pair(A, e2))
CHECK(removed_count == 1)
end
do CASE "pair term leave then re-enter gives one removed (about to leave) then one added (has entered)"
local A = world:component()
local e1 = world:entity()
local monitor = ob.monitor(world:query(jecs.pair(A, e1)))
local added_count = 0
local removed_count = 0
monitor.added(function()
added_count += 1
end)
monitor.removed(function()
removed_count += 1
end)
local e = world:entity()
world:add(e, jecs.pair(A, e1))
world:remove(e, jecs.pair(A, e1))
world:add(e, jecs.pair(A, e1))
CHECK(added_count == 2)
CHECK(removed_count == 1)
end
do CASE "bulk_insert causes added exactly once (entity has entered monitor)"
local A = world:component()
local B = world:component()
local C = world:component()
local monitor = ob.monitor(world:query(A, B, C))
local added_count = 0
monitor.added(function()
added_count += 1
end)
local e = world:entity()
jecs.bulk_insert(world, e, { A, B, C }, { 1, 2, 3 })
CHECK(added_count == 1)
end
do CASE "bulk_insert of multiple entities each reports to added once (each has entered)"
local A = world:component()
local B = world:component()
local C = world:component()
local monitor = ob.monitor(world:query(A, B, C))
local added_count = 0
monitor.added(function()
added_count += 1
end)
local e1 = world:entity()
local e2 = world:entity()
local e3 = world:entity()
jecs.bulk_insert(world, e1, { A, B, C }, { 1, 2, 3 })
jecs.bulk_insert(world, e2, { A, B, C }, { 4, 5, 6 })
jecs.bulk_insert(world, e3, { A, B, C }, { 7, 8, 9 })
CHECK(added_count == 3)
end
do CASE "bulk_remove causes removed exactly once (entity about to leave monitor)"
local A = world:component()
local B = world:component()
local C = world:component()
local monitor = ob.monitor(world:query(A, B, C))
local added_count = 0
local removed_count = 0
monitor.added(function()
added_count += 1
end)
monitor.removed(function()
removed_count += 1
end)
local e = world:entity()
jecs.bulk_insert(world, e, { A, B, C }, { 1, 2, 3 })
CHECK(added_count == 1)
CHECK(removed_count == 0)
jecs.bulk_remove(world, e, { A, B, C })
CHECK(removed_count == 1)
CHECK(not world:has(e, A))
CHECK(not world:has(e, B))
CHECK(not world:has(e, C))
end
do CASE "bulk_remove of multiple entities each reports to removed once (each about to leave)"
local A = world:component()
local B = world:component()
local C = world:component()
local monitor = ob.monitor(world:query(A, B, C))
local removed_count = 0
monitor.removed(function()
removed_count += 1
end)
local e1 = world:entity()
local e2 = world:entity()
jecs.bulk_insert(world, e1, { A, B, C }, { 1, 2, 3 })
jecs.bulk_insert(world, e2, { A, B, C }, { 4, 5, 6 })
jecs.bulk_remove(world, e1, { A, B, C })
CHECK(removed_count == 1)
jecs.bulk_remove(world, e2, { A, B, C })
CHECK(removed_count == 2)
end
do CASE "bulk_remove only of query terms still reports to removed exactly once (entity about to leave)"
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local monitor = ob.monitor(world:query(A, B, C))
local removed_count = 0
monitor.removed(function()
removed_count += 1
end)
local e = world:entity()
jecs.bulk_insert(world, e, { A, B, C, D }, { 1, 2, 3, 4 })
jecs.bulk_remove(world, e, { A, B, C })
CHECK(removed_count == 1)
CHECK(world:has(e, D))
end
do CASE "bulk_insert then bulk_remove gives added once (entered) and removed once (about to leave)"
local A = world:component()
local B = world:component()
local C = world:component()
local monitor = ob.monitor(world:query(A, B, C))
local added_count = 0
local removed_count = 0
monitor.added(function()
added_count += 1
end)
monitor.removed(function()
removed_count += 1
end)
local e = world:entity()
jecs.bulk_insert(world, e, { A, B, C }, { 1, 2, 3 })
jecs.bulk_remove(world, e, { A, B, C })
CHECK(added_count == 1)
CHECK(removed_count == 1)
end
do CASE "monitor with pair should handle remove and re-add correctly"
local A = world:component()
@ -888,7 +1185,6 @@ TEST("modules/ob::monitor", function()
local B = world:component()
local e1 = world:entity()
local e2 = world:entity()
local e3 = world:entity()
local monitor = ob.monitor(world:query(jecs.pair(A, e1), jecs.pair(A, e2)))
local c = 0
@ -898,15 +1194,15 @@ TEST("modules/ob::monitor", function()
local e = world:entity()
world:add(e, jecs.pair(A, e1))
CHECK(c == 1)
CHECK(c == 0)
world:remove(e, jecs.pair(A, e1))
world:add(e, jecs.pair(A, e2))
CHECK(c == 2)
CHECK(c == 0)
world:remove(e, jecs.pair(A, e2))
world:add(e, jecs.pair(A, e1))
CHECK(c == 3)
CHECK(c == 0)
world:add(e, jecs.pair(A, e2))
CHECK(c == 4)
CHECK(c == 1)
end
do CASE "monitor with pair should handle set operations correctly"
@ -929,12 +1225,13 @@ TEST("modules/ob::monitor", function()
CHECK(c == 1)
world:set(e, jecs.pair(A, e1), false)
CHECK(c == 1)
CHECK(r == 0)
world:set(e, jecs.pair(A, e1), true)
CHECK(c == 2)
CHECK(c == 1)
world:remove(e, jecs.pair(A, e1))
CHECK(r == 1)
world:set(e, jecs.pair(A, e1), true)
CHECK(c == 3)
CHECK(c == 2)
end
do CASE "monitor with pair query should handle non-matching pairs"

View file

@ -2146,6 +2146,20 @@ TEST("world:query()", function()
CHECK(not world:has(e1, B))
end
end
do CASE "query:archetypes(override) should create new archetypes list"
local world = jecs.world()
local A = world:component()
local B = world:component()
local q = world:query(A, B)
local e = world:entity()
world:set(e, A, false)
world:set(e, B, true)
CHECK(q:archetypes() == q:archetypes())
CHECK(q:archetypes() ~= q:archetypes(true))
end
do CASE "cached"
local world = jecs.world()