Change Observers to support cleanups and :with/without
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run

This commit is contained in:
Ukendio 2025-09-15 16:42:17 +02:00
parent 3dacb2af80
commit 456713c2d5
2 changed files with 142 additions and 88 deletions

View file

@ -10,22 +10,16 @@ type Entity<T> = jecs.Entity<T>
export type Iter<T...> = (Observer<T...>) -> () -> (jecs.Entity, T...) export type Iter<T...> = (Observer<T...>) -> () -> (jecs.Entity, T...)
export type Observer<T...> = typeof(setmetatable( export type Observer<T...> = {
{} :: { disconnect: (Observer<T...>) -> (),
iter: Iter<T...>, added: ((jecs.Entity) -> ()) -> (),
entities: { Entity<nil> }, removed: ((jecs.Entity) -> ()) -> ()
disconnect: (Observer<T...>) -> () }
},
{} :: {
__iter: Iter<T...>,
}
))
local function observers_new<T...>( local function observers_new<T...>(
query: Query<T...>, query: Query<T...>,
callback: ((Entity<nil>, Id<any>, value: any?) -> ())? callback: ((Entity<nil>) -> ())
): Observer<T...> ): Observer<T...>
query:cached() query:cached()
local world = (query :: Query<T...> & { world: World }).world local world = (query :: Query<T...> & { world: World }).world
@ -47,8 +41,6 @@ local function observers_new<T...>(
end end
local entity_index = world.entity_index :: any local entity_index = world.entity_index :: any
local i = 0
local entities = {}
local function emplaced<T, a>( local function emplaced<T, a>(
entity: jecs.Entity<T>, entity: jecs.Entity<T>,
@ -60,20 +52,40 @@ local function observers_new<T...>(
local archetype = r.archetype local archetype = r.archetype
if archetypes[archetype.id] then if archetypes[archetype.id] then
i += 1 callback(entity)
entities[i] = entity
if callback ~= nil then
callback(entity, id, value)
end
end end
end end
local cleanup = {}
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) term = jecs.ECS_PAIR_FIRST(term)
end end
world:added(term, emplaced) local onadded = world:added(term, emplaced)
world:changed(term, emplaced) local onchanged = world:changed(term, emplaced)
table.insert(cleanup, onadded)
table.insert(cleanup, onchanged)
end
local without = query.filter_without
if without then
for _, term in without do
if jecs.IS_PAIR(term) then
term = jecs.ECS_PAIR_FIRST(term)
end
local onremoved = world:removed(term, function(entity, id)
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback(entity)
end
end
end)
table.insert(cleanup, onremoved)
end
end end
local function disconnect() local function disconnect()
@ -86,44 +98,28 @@ local function observers_new<T...>(
observers_on_delete, observers_on_delete,
observer_on_delete observer_on_delete
)) ))
end
local function iter() table.clear(archetypes)
local row = i
return function() for _, disconnect in cleanup do
if row == 0 then disconnect()
i = 0
table.clear(entities)
end
local entity = entities[row]
row -= 1
return entity
end end
end end
local observer = { local observer = {
disconnect = disconnect, disconnect = disconnect,
entities = entities,
__iter = iter,
iter = iter
} }
setmetatable(observer, observer)
return (observer :: any) :: Observer<T...> return (observer :: any) :: Observer<T...>
end end
local function monitors_new<T...>( local function monitors_new<T...>(query: Query<T...>): Observer<T...>
query: Query<T...>,
callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
): Observer<T...>
query:cached() query:cached()
local world = (query :: Query<T...> & { world: World }).world local world = (query :: Query<T...> & { world: World }).world
local archetypes = {} local archetypes = {}
local terms = query.ids local terms = query.filter_with :: { jecs.Id<any> }
local first = terms[1] local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first] local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
@ -137,45 +133,75 @@ local function monitors_new<T...>(
archetypes[archetype.id] = nil archetypes[archetype.id] = nil
end end
local entity_index = world.entity_index :: any local entity_index = world.entity_index :: any
local i = 0
local entities = {} local callback_added: ((jecs.Entity) -> ())?
local callback_removed: ((jecs.Entity) -> ())?
local function emplaced<T, a>( local function emplaced<T, a>(
entity: jecs.Entity<T>, entity: jecs.Entity<T>,
id: jecs.Id<a>, id: jecs.Id<a>,
value: a? value: a?
) )
if callback_added == nil then
return
end
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
local archetype = r.archetype local archetype = r.archetype
if archetypes[archetype.id] then if archetypes[archetype.id] then
i += 1 callback_added(entity)
entities[i] = entity
if callback ~= nil then
callback(entity, jecs.OnAdd)
end
end end
end end
local function removed(entity: jecs.Entity, component: jecs.Id) local function removed(entity: jecs.Entity, component: jecs.Id)
if callback_removed == nil then
return
end
local r = jecs.record(world, entity) local r = jecs.record(world, entity)
if not archetypes[r.archetype.id] then if not archetypes[r.archetype.id] then
return return
end end
local EcsOnRemove = jecs.OnRemove :: jecs.Id callback_removed(entity)
if callback ~= nil then
callback(entity, EcsOnRemove)
end
end end
local cleanup = {}
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) term = jecs.ECS_PAIR_FIRST(term)
end end
world:added(term, emplaced) local onadded = world:added(term, emplaced)
world:removed(term, removed) local onremoved = world:removed(term, removed)
table.insert(cleanup, onadded)
table.insert(cleanup, onremoved)
end
local without = query.filter_without
if without then
for _, term in without do
if jecs.IS_PAIR(term) then
term = jecs.ECS_PAIR_FIRST(term)
end
local onadded = world:added(term, removed)
local onremoved = world:removed(term, function(entity, id)
if callback_added == nil then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback_added(entity)
end
end
end)
table.insert(cleanup, onadded)
table.insert(cleanup, onremoved)
end
end end
local function disconnect() local function disconnect()
@ -188,30 +214,28 @@ local function monitors_new<T...>(
observers_on_delete, observers_on_delete,
observer_on_delete observer_on_delete
)) ))
table.clear(archetypes)
for _, disconnect in cleanup do
disconnect()
end
end end
local function iter() local function monitor_added(callback)
local row = i callback_added = callback
return function()
if row == 0 then
i = 0
table.clear(entities)
end
local entity = entities[row]
row -= 1
return entity
end end
local function monitor_removed(callback)
callback_removed = callback
end end
local observer = { local observer = {
disconnect = disconnect, disconnect = disconnect,
entities = entities, added = monitor_added,
__iter = iter, removed = monitor_removed
iter = iter
} }
setmetatable(observer, observer)
return (observer :: any) :: Observer<T...> return (observer :: any) :: Observer<T...>
end end

View file

@ -5,15 +5,42 @@ local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
local FOCUS = test.FOCUS local FOCUS = test.FOCUS
local ob = require("@addons/ob") local ob = require("@addons/ob")
TEST("addons/observers", function() TEST("addons/ob", function()
local world = jecs.world() local world = jecs.world()
do CASE "Should support query:without()"
local A = world:component()
local B = world:component()
local c = 1
local monitor = ob.monitor(world:query(A):without(B))
monitor.added(function()
c += 1
end)
monitor.removed(function()
c += 1
end)
local child = world:entity()
world:add(child, B)
CHECK(c==1)
world:add(child, A)
CHECK(c==1)
world:remove(child, B)
CHECK(c==2)
world:remove(child, A)
CHECK(c==3)
end
do CASE "monitors should accept pairs" do CASE "monitors should accept pairs"
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
local c = 1 local c = 1
ob.monitor(world:query(jecs.pair(A, B)), function (_, event) local monitor = ob.monitor(world:query(jecs.pair(A, B)))
monitor.added(function()
c += 1
end)
monitor.removed(function()
c += 1 c += 1
end) end)
@ -24,6 +51,7 @@ TEST("addons/observers", function()
world:remove(child, jecs.pair(A, B)) world:remove(child, jecs.pair(A, B))
CHECK(c == 3) CHECK(c == 3)
end end
do CASE "Ensure ordering between signals and observers" do CASE "Ensure ordering between signals and observers"
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
@ -78,7 +106,9 @@ TEST("addons/observers", function()
count += 1 count += 1
end end
ob.monitor(world:query(A), counter) local monitor = ob.monitor(world:query(A))
monitor.added(counter)
monitor.removed(counter)
local e = world:entity() local e = world:entity()
world:set(e, A, false) world:set(e, A, false)