Observers should handle non queried pairs in :with
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled

This commit is contained in:
Ukendio 2025-09-29 23:02:54 +02:00
parent 9514fb758a
commit 698854d11b
2 changed files with 114 additions and 22 deletions

View file

@ -6,7 +6,6 @@ type Query<T...> = jecs.Query<T...>
type Id<T=any> = jecs.Id<T>
type Entity<T> = jecs.Entity<T>
export type Iter<T...> = (Observer<T...>) -> () -> (jecs.Entity, T...)
@ -22,7 +21,7 @@ export type Monitor<T...> = {
local function observers_new<T...>(
query: Query<T...>,
callback: ((Entity<nil>) -> ())
callback: (jecs.Entity) -> ()
): Observer<T...>
query:cached()
@ -51,10 +50,10 @@ local function observers_new<T...>(
archetypes[archetype.id] = true
end
local function emplaced<T, a>(
entity: jecs.Entity<T>,
local function emplaced<a>(
entity: jecs.Entity,
id: jecs.Id<a>,
value: a?
value: a
)
local r = entity_index.sparse_array[jecs.ECS_ID(entity)]
@ -69,13 +68,27 @@ local function observers_new<T...>(
for _, term in terms do
if jecs.IS_PAIR(term) then
term = jecs.ECS_PAIR_FIRST(term)
local rel = jecs.ECS_PAIR_FIRST(term)
local tgt = jecs.ECS_PAIR_SECOND(term)
local wc = tgt == jecs.w
local onadded = world:added(rel, function(entity, id)
if not wc and id ~= term then
return
end
local r = jecs.record(world, entity)
if archetypes[r.archetype.id] then
callback(entity)
end
end)
table.insert(cleanup, onadded)
else
local onadded = world:added(term, emplaced)
local onchanged = world:changed(term, emplaced)
table.insert(cleanup, onadded)
table.insert(cleanup, onchanged)
end
end
local without = query.filter_without
if without then
@ -138,7 +151,7 @@ local function observers_new<T...>(
disconnect = disconnect,
}
return (observer :: any) :: Observer<T...>
return observer
end
local function monitors_new<T...>(query: Query<T...>): Monitor<T...>
@ -169,10 +182,10 @@ local function monitors_new<T...>(query: Query<T...>): Monitor<T...>
local callback_added: ((jecs.Entity) -> ())?
local callback_removed: ((jecs.Entity) -> ())?
local function emplaced<T, a>(
entity: jecs.Entity<T>,
local function emplaced<a>(
entity: jecs.Entity,
id: jecs.Id<a>,
value: a?
value: a
)
if callback_added == nil then
return
@ -203,13 +216,50 @@ local function monitors_new<T...>(query: Query<T...>): Monitor<T...>
for _, term in terms do
if jecs.IS_PAIR(term) then
term = jecs.ECS_PAIR_FIRST(term)
local rel = jecs.ECS_PAIR_FIRST(term)
local tgt = jecs.ECS_PAIR_SECOND(term)
local wc = tgt == jecs.w
local onadded = world:added(rel, function(entity, id)
if callback_added == nil then
return
end
if not wc and id ~= term then
return
end
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any) :: jecs.Record
local archetype = r.archetype
if archetypes[archetype.id] then
callback_added(entity)
end
end)
local onremoved = world:removed(rel, function(entity, id)
if callback_removed == nil then
return
end
if not wc and id ~= term then
return
end
local r = jecs.record(world, entity)
if not archetypes[r.archetype.id] then
return
end
callback_removed(entity)
end)
table.insert(cleanup, onadded)
table.insert(cleanup, onremoved)
else
local onadded = world:added(term, emplaced)
local onremoved = world:removed(term, removed)
table.insert(cleanup, onadded)
table.insert(cleanup, onremoved)
end
end
local without = query.filter_without
if without then
@ -311,13 +361,13 @@ local function monitors_new<T...>(query: Query<T...>): Monitor<T...>
callback_removed = callback
end
local observer = {
local monitor = {
disconnect = disconnect,
added = monitor_added,
removed = monitor_removed
}
} :: Monitor<T...>
return (observer :: any) :: Monitor<T...>
return monitor
end
return {

View file

@ -7,6 +7,26 @@ local ob = require("@addons/ob")
TEST("addons/ob::observer", function()
local world = jecs.world()
do CASE [[should not invoke callbacks with a related but non-queried pair that
while the entity still matches against the query]]
local A = world:component()
local B = world:component()
local C = world:component()
local c = 1
ob.observer(world:query(jecs.pair(A, B)), function()
c+=1
end)
local e = world:entity()
CHECK(c==1)
world:add(e, jecs.pair(A, B))
CHECK(c==2)
world:add(e, jecs.pair(A, C))
CHECK(c==2)
end
do CASE "should match against archetypes that were already created"
local A = world:component()
@ -203,6 +223,28 @@ end)
TEST("addons/ob::monitor", function()
local world = jecs.world()
do CASE [[should not invoke callbacks with a related but non-queried pair that
while the entity still matches against the query]]
local A = world:component()
local B = world:component()
local C = world:component()
local c = 1
local monitor = ob.monitor(world:query(jecs.pair(A, B)))
monitor.added(function()
c += 1
end)
local e = world:entity()
CHECK(c==1)
world:add(e, jecs.pair(A, B))
CHECK(c==2)
world:add(e, jecs.pair(A, C))
CHECK(c==2)
end
do CASE "should match against archetypes that were already created"
local A = world:component()