diff --git a/addons/ob.luau b/addons/ob.luau index efc0630..eb7f0e4 100755 --- a/addons/ob.luau +++ b/addons/ob.luau @@ -10,22 +10,16 @@ type Entity = jecs.Entity export type Iter = (Observer) -> () -> (jecs.Entity, T...) -export type Observer = typeof(setmetatable( - {} :: { - iter: Iter, - entities: { Entity }, - disconnect: (Observer) -> () - }, - {} :: { - __iter: Iter, - } -)) +export type Observer = { + disconnect: (Observer) -> (), + added: ((jecs.Entity) -> ()) -> (), + removed: ((jecs.Entity) -> ()) -> () +} local function observers_new( query: Query, - callback: ((Entity, Id, value: any?) -> ())? + callback: ((Entity) -> ()) ): Observer - query:cached() local world = (query :: Query & { world: World }).world @@ -47,8 +41,6 @@ local function observers_new( end local entity_index = world.entity_index :: any - local i = 0 - local entities = {} local function emplaced( entity: jecs.Entity, @@ -60,70 +52,74 @@ local function observers_new( 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 function disconnect() - table.remove(observers_on_create, table.find( - observers_on_create, - observer_on_create - )) + 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 - table.remove(observers_on_delete, table.find( - observers_on_delete, - observer_on_delete + local function disconnect() + table.remove(observers_on_create, table.find( + observers_on_create, + observer_on_create + )) + + table.remove(observers_on_delete, table.find( + 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 - end - end - - local observer = { - disconnect = disconnect, - entities = entities, - __iter = iter, - iter = iter + local observer = { + disconnect = disconnect, } - setmetatable(observer, observer) - return (observer :: any) :: Observer end -local function monitors_new( - query: Query, - callback: ((Entity, Id, value: any?) -> ())? -): Observer - +local function monitors_new(query: Query): Observer query:cached() local world = (query :: Query & { world: World }).world local archetypes = {} - local terms = query.ids + local terms = query.filter_with :: { jecs.Id } local first = terms[1] local observers_on_create = world.observable[jecs.ArchetypeCreate][first] @@ -137,47 +133,77 @@ local function monitors_new( 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( entity: jecs.Entity, id: jecs.Id, 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() table.remove(observers_on_create, table.find( observers_on_create, @@ -188,30 +214,28 @@ local function monitors_new( 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 - end + 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 end diff --git a/test/addons/observers.luau b/test/addons/ob.luau similarity index 74% rename from test/addons/observers.luau rename to test/addons/ob.luau index ae307e4..aab623a 100755 --- a/test/addons/observers.luau +++ b/test/addons/ob.luau @@ -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)