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 Observer<T...> = typeof(setmetatable(
{} :: {
iter: Iter<T...>,
entities: { Entity<nil> },
disconnect: (Observer<T...>) -> ()
},
{} :: {
__iter: Iter<T...>,
export type Observer<T...> = {
disconnect: (Observer<T...>) -> (),
added: ((jecs.Entity) -> ()) -> (),
removed: ((jecs.Entity) -> ()) -> ()
}
))
local function observers_new<T...>(
query: Query<T...>,
callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
callback: ((Entity<nil>) -> ())
): Observer<T...>
query:cached()
local world = (query :: Query<T...> & { world: World }).world
@ -47,8 +41,6 @@ local function observers_new<T...>(
end
local entity_index = world.entity_index :: any
local i = 0
local entities = {}
local function emplaced<T, a>(
entity: jecs.Entity<T>,
@ -60,20 +52,40 @@ local function observers_new<T...>(
local archetype = r.archetype
if archetypes[archetype.id] then
i += 1
entities[i] = entity
if callback ~= nil then
callback(entity, id, value)
end
callback(entity)
end
end
local cleanup = {}
for _, term in terms do
if jecs.IS_PAIR(term) then
term = jecs.ECS_PAIR_FIRST(term)
end
world:added(term, emplaced)
world:changed(term, emplaced)
local onadded = world:added(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
local function disconnect()
@ -86,44 +98,28 @@ local function observers_new<T...>(
observers_on_delete,
observer_on_delete
))
end
local function iter()
local row = i
return function()
if row == 0 then
i = 0
table.clear(entities)
end
local entity = entities[row]
row -= 1
return entity
table.clear(archetypes)
for _, disconnect in cleanup do
disconnect()
end
end
local observer = {
disconnect = disconnect,
entities = entities,
__iter = iter,
iter = iter
}
setmetatable(observer, observer)
return (observer :: any) :: Observer<T...>
end
local function monitors_new<T...>(
query: Query<T...>,
callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
): Observer<T...>
local function monitors_new<T...>(query: Query<T...>): Observer<T...>
query:cached()
local world = (query :: Query<T...> & { world: World }).world
local archetypes = {}
local terms = query.ids
local terms = query.filter_with :: { jecs.Id<any> }
local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
@ -137,45 +133,75 @@ local function monitors_new<T...>(
archetypes[archetype.id] = nil
end
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>(
entity: jecs.Entity<T>,
id: jecs.Id<a>,
value: a?
)
if callback_added == nil 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
i += 1
entities[i] = entity
if callback ~= nil then
callback(entity, jecs.OnAdd)
end
callback_added(entity)
end
end
local function removed(entity: jecs.Entity, component: jecs.Id)
if callback_removed == nil then
return
end
local r = jecs.record(world, entity)
if not archetypes[r.archetype.id] then
return
end
local EcsOnRemove = jecs.OnRemove :: jecs.Id
if callback ~= nil then
callback(entity, EcsOnRemove)
end
callback_removed(entity)
end
local cleanup = {}
for _, term in terms do
if jecs.IS_PAIR(term) then
term = jecs.ECS_PAIR_FIRST(term)
end
world:added(term, emplaced)
world:removed(term, removed)
local onadded = world:added(term, emplaced)
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
local function disconnect()
@ -188,30 +214,28 @@ local function monitors_new<T...>(
observers_on_delete,
observer_on_delete
))
table.clear(archetypes)
for _, disconnect in cleanup do
disconnect()
end
end
local function iter()
local row = i
return function()
if row == 0 then
i = 0
table.clear(entities)
end
local entity = entities[row]
row -= 1
return entity
local function monitor_added(callback)
callback_added = callback
end
local function monitor_removed(callback)
callback_removed = callback
end
local observer = {
disconnect = disconnect,
entities = entities,
__iter = iter,
iter = iter
added = monitor_added,
removed = monitor_removed
}
setmetatable(observer, observer)
return (observer :: any) :: Observer<T...>
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 ob = require("@addons/ob")
TEST("addons/observers", function()
TEST("addons/ob", function()
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"
local A = world:component()
local B = world:component()
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
end)
@ -24,6 +51,7 @@ TEST("addons/observers", function()
world:remove(child, jecs.pair(A, B))
CHECK(c == 3)
end
do CASE "Ensure ordering between signals and observers"
local A = world:component()
local B = world:component()
@ -78,7 +106,9 @@ TEST("addons/observers", function()
count += 1
end
ob.monitor(world:query(A), counter)
local monitor = ob.monitor(world:query(A))
monitor.added(counter)
monitor.removed(counter)
local e = world:entity()
world:set(e, A, false)