mirror of
https://github.com/Ukendio/jecs.git
synced 2026-03-18 00:44:32 +00:00
Add query:fini and query:archetypes(override) and changes to OB
This commit is contained in:
parent
d4a7f1d86c
commit
30597ed389
6 changed files with 530 additions and 235 deletions
|
|
@ -47,6 +47,8 @@ end
|
||||||
-- local tgt = tonumber(tokens[2]) :: jecs.Entity
|
-- local tgt = tonumber(tokens[2]) :: jecs.Entity
|
||||||
|
|
||||||
-- rel = ecs_ensure_entity(world, rel)
|
-- rel = ecs_ensure_entity(world, rel)
|
||||||
|
--
|
||||||
|
-- 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
|
||||||
-- tgt = ecs_ensure_entity(world, tgt)
|
-- tgt = ecs_ensure_entity(world, tgt)
|
||||||
|
|
||||||
-- return jecs.pair(rel, tgt)
|
-- return jecs.pair(rel, tgt)
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,17 @@ local jecs = require("@jecs")
|
||||||
|
|
||||||
type World = jecs.World
|
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 = {
|
export type Observer = {
|
||||||
disconnect: () -> (),
|
disconnect: () -> (),
|
||||||
|
|
@ -20,7 +29,7 @@ local function observers_new(
|
||||||
query: jecs.Query<...any>,
|
query: jecs.Query<...any>,
|
||||||
callback: (jecs.Entity) -> ()
|
callback: (jecs.Entity) -> ()
|
||||||
): Observer
|
): Observer
|
||||||
local cachedquery = query:cached()
|
local cachedquery = duplicate(query)
|
||||||
|
|
||||||
local world = (cachedquery :: jecs.Query<any> & { world: World }).world
|
local world = (cachedquery :: jecs.Query<any> & { world: World }).world
|
||||||
callback = callback
|
callback = callback
|
||||||
|
|
@ -134,7 +143,7 @@ local function observers_new(
|
||||||
end
|
end
|
||||||
|
|
||||||
local function monitors_new(query: jecs.Query<...any>): Monitor
|
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
|
local world = (cachedquery :: jecs.Query<...any> & { world: World }).world :: jecs.World
|
||||||
|
|
||||||
|
|
@ -151,12 +160,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
||||||
local callback_added: ((jecs.Entity) -> ())?
|
local callback_added: ((jecs.Entity) -> ())?
|
||||||
local callback_removed: ((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_old_archetype: jecs.Archetype? = nil
|
||||||
local last_entity: jecs.Entity? = nil
|
local last_entity: jecs.Entity? = nil
|
||||||
|
|
||||||
|
|
@ -173,10 +176,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
||||||
local r = jecs.entity_index_try_get_fast(
|
local r = jecs.entity_index_try_get_fast(
|
||||||
entity_index, entity :: any) :: jecs.Record
|
entity_index, entity :: any) :: jecs.Record
|
||||||
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
|
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
|
if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
@ -185,52 +184,31 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
||||||
last_entity = entity
|
last_entity = entity
|
||||||
callback_added(entity)
|
callback_added(entity)
|
||||||
else
|
else
|
||||||
-- NOTE(marcus): Clear tracking when we see a different transition pattern
|
|
||||||
last_old_archetype = nil
|
last_old_archetype = nil
|
||||||
last_entity = nil
|
last_entity = nil
|
||||||
end
|
end
|
||||||
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?)
|
local function removed(entity: jecs.Entity, component: jecs.Component, delete:boolean?)
|
||||||
if callback_removed == nil then
|
if callback_removed == nil then
|
||||||
return
|
return
|
||||||
end
|
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)
|
local r = jecs.record(world, entity)
|
||||||
if r and r.archetype and archetypes[r.archetype.id] then
|
if not r then return end
|
||||||
-- 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
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local r = jecs.record(world, entity)
|
|
||||||
local src = r.archetype
|
local src = r.archetype
|
||||||
|
if not src then return end
|
||||||
|
|
||||||
local dst = jecs.archetype_traverse_remove(world, component, src)
|
if not archetypes[src.id] then return end
|
||||||
|
|
||||||
if not archetypes[dst.id] then
|
if last_entity == entity and last_old_archetype == src then
|
||||||
-- Clear tracking when entity leaves the monitor to allow re-entry
|
return
|
||||||
last_old_archetype = nil
|
|
||||||
last_entity = nil
|
|
||||||
last_deleted_entity = nil
|
|
||||||
callback_removed(entity)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
last_entity = entity
|
||||||
|
last_old_archetype = src
|
||||||
|
callback_removed(entity)
|
||||||
end
|
end
|
||||||
|
|
||||||
local cleanup = {}
|
local cleanup = {}
|
||||||
|
|
@ -254,8 +232,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
||||||
entity_index, entity :: any) :: jecs.Record
|
entity_index, entity :: any) :: jecs.Record
|
||||||
|
|
||||||
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
|
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
|
if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
@ -264,7 +240,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
||||||
last_entity = entity
|
last_entity = entity
|
||||||
callback_added(entity)
|
callback_added(entity)
|
||||||
else
|
else
|
||||||
-- Clear tracking when we see a different transition pattern
|
|
||||||
last_old_archetype = nil
|
last_old_archetype = nil
|
||||||
last_entity = nil
|
last_entity = nil
|
||||||
end
|
end
|
||||||
|
|
@ -314,11 +289,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
||||||
return
|
return
|
||||||
end
|
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
|
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
|
||||||
last_old_archetype = nil
|
last_old_archetype = nil
|
||||||
callback_removed(entity)
|
callback_removed(entity)
|
||||||
|
|
@ -364,10 +334,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
||||||
return
|
return
|
||||||
end
|
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
|
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
|
||||||
callback_removed(entity)
|
callback_removed(entity)
|
||||||
end
|
end
|
||||||
|
|
@ -386,11 +352,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
||||||
end
|
end
|
||||||
local dst = jecs.archetype_traverse_remove(world, id, archetype)
|
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
|
if archetypes[dst.id] then
|
||||||
callback_added(entity)
|
callback_added(entity)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -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 Item<T...> = (self: Query<T...>) -> (Entity, T...)
|
||||||
export type Iter<T...> = (query: 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
|
type TypePack<T...> = (T...) -> never
|
||||||
|
|
||||||
export type CachedQuery<T...> = typeof(setmetatable(
|
export type Cached_Query<T...> = typeof(setmetatable(
|
||||||
{} :: {
|
{} :: {
|
||||||
iter: CachedIter<T...>,
|
iter: Cached_Query_Iter<T...>,
|
||||||
archetypes: (CachedQuery<T...>) -> { Archetype },
|
archetypes: (Cached_Query<T...>) -> { Archetype },
|
||||||
has: (CachedQuery<T...>, Entity) -> boolean,
|
has: (Cached_Query<T...>, Entity) -> boolean,
|
||||||
|
fini: (Cached_Query<T...>) -> (),
|
||||||
|
|
||||||
ids: { Id<any> },
|
ids: { Id<any> },
|
||||||
filter_with: { Id<any> }?,
|
filter_with: { Id<any> }?,
|
||||||
filter_without: { Id<any> }?,
|
filter_without: { Id<any> }?,
|
||||||
|
|
@ -96,7 +98,7 @@ export type CachedQuery<T...> = typeof(setmetatable(
|
||||||
-- world: World
|
-- 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...>),
|
with: ((Query<T...>, ...Component) -> Query<T...>),
|
||||||
without: ((Query<T...>, ...Component) -> Query<T...>),
|
without: ((Query<T...>, ...Component) -> Query<T...>),
|
||||||
archetypes: (Query<T...>) -> { Archetype },
|
archetypes: (Query<T...>) -> { Archetype },
|
||||||
cached: (Query<T...>) -> CachedQuery<T...>,
|
cached: (Query<T...>) -> Cached_Query<T...>,
|
||||||
has: (Query<T...>, Entity) -> boolean,
|
has: (Query<T...>, Entity) -> boolean,
|
||||||
ids: { Id<any> },
|
ids: { Id<any> },
|
||||||
filter_with: { Id<any> }?,
|
filter_with: { Id<any> }?,
|
||||||
|
|
@ -1240,9 +1242,9 @@ end
|
||||||
|
|
||||||
local function NOOP() 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
|
local compatible_archetypes = query.compatible_archetypes
|
||||||
if not compatible_archetypes then
|
if not compatible_archetypes or override then
|
||||||
compatible_archetypes = {}
|
compatible_archetypes = {}
|
||||||
query.compatible_archetypes = compatible_archetypes
|
query.compatible_archetypes = compatible_archetypes
|
||||||
|
|
||||||
|
|
@ -1755,7 +1757,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
|
||||||
|
|
||||||
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row]
|
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row]
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
local output = {}
|
local output = {}
|
||||||
local ids_len = #ids_u
|
local ids_len = #ids_u
|
||||||
function world_query_iter_next(): any
|
function world_query_iter_next(): any
|
||||||
|
|
@ -1817,10 +1819,10 @@ else
|
||||||
|
|
||||||
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row], unpack(output)
|
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row], unpack(output)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
query.next = world_query_iter_next
|
query.next = world_query_iter_next
|
||||||
return world_query_iter_next
|
return world_query_iter_next
|
||||||
end
|
end
|
||||||
|
|
||||||
local function query_iter(query): () -> (number, ...any)
|
local function query_iter(query): () -> (number, ...any)
|
||||||
|
|
@ -2416,7 +2418,7 @@ local function query_cached(query: QueryInner)
|
||||||
|
|
||||||
local eindex = world.entity_index :: entityindex
|
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)
|
local r = entity_index_try_get_fast(eindex, entity)
|
||||||
if not r then
|
if not r then
|
||||||
return false
|
return false
|
||||||
|
|
@ -2430,11 +2432,30 @@ local function query_cached(query: QueryInner)
|
||||||
return archetypes_map[entityarchetype.id] ~= nil
|
return archetypes_map[entityarchetype.id] ~= nil
|
||||||
end
|
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
|
local cached_query = query :: any
|
||||||
cached_query.archetypes = query_archetypes
|
cached_query.archetypes = query_archetypes
|
||||||
cached_query.__iter = cached_query_iter
|
cached_query.__iter = cached_query_iter
|
||||||
cached_query.iter = cached_query_iter
|
cached_query.iter = cached_query_iter
|
||||||
cached_query.has = cached_query_has
|
cached_query.has = cached_query_has
|
||||||
|
cached_query.fini = cached_query_fini
|
||||||
setmetatable(cached_query, cached_query)
|
setmetatable(cached_query, cached_query)
|
||||||
return cached_query
|
return cached_query
|
||||||
end
|
end
|
||||||
|
|
|
||||||
315
test/ob.luau
315
test/ob.luau
|
|
@ -3,7 +3,7 @@ local testkit = require("@modules/testkit")
|
||||||
local test = testkit.test()
|
local test = testkit.test()
|
||||||
local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
|
local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
|
||||||
local FOCUS = test.FOCUS
|
local FOCUS = test.FOCUS
|
||||||
local ob = require("@modules/ob")
|
local ob = require("@modules/OB/module")
|
||||||
|
|
||||||
TEST("modules/ob::observer", function()
|
TEST("modules/ob::observer", function()
|
||||||
local world = jecs.world()
|
local world = jecs.world()
|
||||||
|
|
@ -292,6 +292,36 @@ end)
|
||||||
TEST("modules/ob::monitor", function()
|
TEST("modules/ob::monitor", function()
|
||||||
local world = jecs.world()
|
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
|
do CASE [[should not invoke monitor.added callback multiple times in a bulk_move
|
||||||
]]
|
]]
|
||||||
local A = world:component()
|
local A = world:component()
|
||||||
|
|
@ -609,6 +639,74 @@ TEST("modules/ob::monitor", function()
|
||||||
CHECK(r==2)
|
CHECK(r==2)
|
||||||
end
|
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)"
|
do CASE "Should enter monitor at query:without(pair(R, *)) when adding pair(R, t1)"
|
||||||
local A = world:component()
|
local A = world:component()
|
||||||
local B = world:component()
|
local B = world:component()
|
||||||
|
|
@ -792,7 +890,206 @@ TEST("modules/ob::monitor", function()
|
||||||
CHECK(c == 2)
|
CHECK(c == 2)
|
||||||
end
|
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"
|
do CASE "monitor with pair should handle remove and re-add correctly"
|
||||||
local A = world:component()
|
local A = world:component()
|
||||||
|
|
@ -888,7 +1185,6 @@ TEST("modules/ob::monitor", function()
|
||||||
local B = world:component()
|
local B = world:component()
|
||||||
local e1 = world:entity()
|
local e1 = world:entity()
|
||||||
local e2 = 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 monitor = ob.monitor(world:query(jecs.pair(A, e1), jecs.pair(A, e2)))
|
||||||
local c = 0
|
local c = 0
|
||||||
|
|
@ -898,15 +1194,15 @@ TEST("modules/ob::monitor", function()
|
||||||
|
|
||||||
local e = world:entity()
|
local e = world:entity()
|
||||||
world:add(e, jecs.pair(A, e1))
|
world:add(e, jecs.pair(A, e1))
|
||||||
CHECK(c == 1)
|
CHECK(c == 0)
|
||||||
world:remove(e, jecs.pair(A, e1))
|
world:remove(e, jecs.pair(A, e1))
|
||||||
world:add(e, jecs.pair(A, e2))
|
world:add(e, jecs.pair(A, e2))
|
||||||
CHECK(c == 2)
|
CHECK(c == 0)
|
||||||
world:remove(e, jecs.pair(A, e2))
|
world:remove(e, jecs.pair(A, e2))
|
||||||
world:add(e, jecs.pair(A, e1))
|
world:add(e, jecs.pair(A, e1))
|
||||||
CHECK(c == 3)
|
CHECK(c == 0)
|
||||||
world:add(e, jecs.pair(A, e2))
|
world:add(e, jecs.pair(A, e2))
|
||||||
CHECK(c == 4)
|
CHECK(c == 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "monitor with pair should handle set operations correctly"
|
do CASE "monitor with pair should handle set operations correctly"
|
||||||
|
|
@ -929,12 +1225,13 @@ TEST("modules/ob::monitor", function()
|
||||||
CHECK(c == 1)
|
CHECK(c == 1)
|
||||||
world:set(e, jecs.pair(A, e1), false)
|
world:set(e, jecs.pair(A, e1), false)
|
||||||
CHECK(c == 1)
|
CHECK(c == 1)
|
||||||
|
CHECK(r == 0)
|
||||||
world:set(e, jecs.pair(A, e1), true)
|
world:set(e, jecs.pair(A, e1), true)
|
||||||
CHECK(c == 2)
|
CHECK(c == 1)
|
||||||
world:remove(e, jecs.pair(A, e1))
|
world:remove(e, jecs.pair(A, e1))
|
||||||
CHECK(r == 1)
|
CHECK(r == 1)
|
||||||
world:set(e, jecs.pair(A, e1), true)
|
world:set(e, jecs.pair(A, e1), true)
|
||||||
CHECK(c == 3)
|
CHECK(c == 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "monitor with pair query should handle non-matching pairs"
|
do CASE "monitor with pair query should handle non-matching pairs"
|
||||||
|
|
|
||||||
|
|
@ -2147,6 +2147,20 @@ TEST("world:query()", function()
|
||||||
end
|
end
|
||||||
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"
|
do CASE "cached"
|
||||||
local world = jecs.world()
|
local world = jecs.world()
|
||||||
local Foo = world:component()
|
local Foo = world:component()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue