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 Id<T=any> = jecs.Id<T>
type Entity<T> = jecs.Entity<T>
export type Iter<T...> = (Observer<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...>( local function observers_new<T...>(
query: Query<T...>, query: Query<T...>,
callback: ((Entity<nil>) -> ()) callback: (jecs.Entity) -> ()
): Observer<T...> ): Observer<T...>
query:cached() query:cached()
@ -51,10 +50,10 @@ local function observers_new<T...>(
archetypes[archetype.id] = true archetypes[archetype.id] = true
end end
local function emplaced<T, a>( local function emplaced<a>(
entity: jecs.Entity<T>, entity: jecs.Entity,
id: jecs.Id<a>, id: jecs.Id<a>,
value: a? value: a
) )
local r = entity_index.sparse_array[jecs.ECS_ID(entity)] local r = entity_index.sparse_array[jecs.ECS_ID(entity)]
@ -69,13 +68,27 @@ local function observers_new<T...>(
for _, term in terms do for _, term in terms do
if jecs.IS_PAIR(term) then 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 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 onadded = world:added(term, emplaced)
local onchanged = world:changed(term, emplaced) local onchanged = world:changed(term, emplaced)
table.insert(cleanup, onadded) table.insert(cleanup, onadded)
table.insert(cleanup, onchanged) table.insert(cleanup, onchanged)
end end
end
local without = query.filter_without local without = query.filter_without
if without then if without then
@ -138,7 +151,7 @@ local function observers_new<T...>(
disconnect = disconnect, disconnect = disconnect,
} }
return (observer :: any) :: Observer<T...> return observer
end end
local function monitors_new<T...>(query: Query<T...>): Monitor<T...> 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_added: ((jecs.Entity) -> ())?
local callback_removed: ((jecs.Entity) -> ())? local callback_removed: ((jecs.Entity) -> ())?
local function emplaced<T, a>( local function emplaced<a>(
entity: jecs.Entity<T>, entity: jecs.Entity,
id: jecs.Id<a>, id: jecs.Id<a>,
value: a? value: a
) )
if callback_added == nil then if callback_added == nil then
return return
@ -203,13 +216,50 @@ local function monitors_new<T...>(query: Query<T...>): Monitor<T...>
for _, term in terms do for _, term in terms do
if jecs.IS_PAIR(term) then 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 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 onadded = world:added(term, emplaced)
local onremoved = world:removed(term, removed) local onremoved = world:removed(term, removed)
table.insert(cleanup, onadded) table.insert(cleanup, onadded)
table.insert(cleanup, onremoved) table.insert(cleanup, onremoved)
end end
end
local without = query.filter_without local without = query.filter_without
if without then if without then
@ -311,13 +361,13 @@ local function monitors_new<T...>(query: Query<T...>): Monitor<T...>
callback_removed = callback callback_removed = callback
end end
local observer = { local monitor = {
disconnect = disconnect, disconnect = disconnect,
added = monitor_added, added = monitor_added,
removed = monitor_removed removed = monitor_removed
} } :: Monitor<T...>
return (observer :: any) :: Monitor<T...> return monitor
end end
return { return {

View file

@ -7,6 +7,26 @@ local ob = require("@addons/ob")
TEST("addons/ob::observer", function() TEST("addons/ob::observer", function()
local world = jecs.world() 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" do CASE "should match against archetypes that were already created"
local A = world:component() local A = world:component()
@ -203,6 +223,28 @@ end)
TEST("addons/ob::monitor", function() TEST("addons/ob::monitor", function()
local world = jecs.world() 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" do CASE "should match against archetypes that were already created"
local A = world:component() local A = world:component()