From fd6883cfeda7c5e8cf3264ece927b35932eb6d25 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 7 May 2025 19:21:12 +0200 Subject: [PATCH] Allow pre existing hooks for observer --- addons/observers.luau | 135 ++++++++---------- .../systems/players_added.luau | 10 +- test/addons/observers.luau | 27 +++- test/tests.luau | 24 ++++ wally.toml | 2 +- 5 files changed, 108 insertions(+), 90 deletions(-) diff --git a/addons/observers.luau b/addons/observers.luau index 5f3f1f5..ef85d85 100644 --- a/addons/observers.luau +++ b/addons/observers.luau @@ -6,9 +6,9 @@ type Observer = { } export type PatchedWorld = jecs.World & { - added: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id, value: any) -> ()) -> (), - removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (), - changed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (), + added: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (), + removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> () -> (), + changed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (), observer: (PatchedWorld, Observer) -> (), monitor: (PatchedWorld, Observer) -> (), } @@ -70,6 +70,7 @@ local function join(world, component) sparse_array[entity] = nil dense_array[max_id] = nil values[max_id] = nil + max_id -= 1 end) world:changed(component, function(entity, id, value) @@ -89,62 +90,6 @@ local function join(world, component) end end -local function query_changed(world, component) - assert(jecs.IS_PAIR(component) == false) - local callerid = debug.info(2, "sl") - - local tracker = world.trackers[callerid] - if not tracker then - local records = {} - local connections = {} - tracker = { - records = records, - connections = connections - } - world.trackers[callerid] = tracker - - table.insert(connections, world:added(component, function(entity, id, v) - tracker[entity] = { - new = v - } - end)) - table.insert(connections, world:changed(component, function(entity, id, v) - local record = tracker[entity] - record.old = record.new - record.new = v - end)) - - table.insert(connections, world:removed(component, function(entity, id) - local record = tracker[entity] - record.old = record.new - record.new = nil - end)) - end - - local entity = nil - local record = nil - return function() - entity, record = next(tracker, entity) - if entity == nil then - return - end - return entity, record - end -end - -local function spy_on_world_delete(world) - local world_delete = world.delete - world.delete = function(world, entity) - world_delete(world, entity) - for _, tracker in world.trackers do - tracker.records[entity] = nil - for _, connection in tracker.connections do - connection() - end - end - end -end - local function monitors_new(world, description) local query = description.query local callback = description.callback @@ -192,18 +137,23 @@ local function monitors_new(world, description) end end -local function observers_add(world: jecs.World & { [string]: any }): PatchedWorld +local function observers_add(world: jecs.World): PatchedWorld type Signal = { [jecs.Entity]: { (...any) -> () } } + + local world_mut = world :: jecs.World & {[string]: any} + local signals = { added = {} :: Signal, emplaced = {} :: Signal, removed = {} :: Signal } - world.added = function(_, component, fn) + world_mut.added = function( + _: jecs.World, + component: jecs.Id, + fn: (e: jecs.Entity, id: jecs.Id, value: T) -> () + ) local listeners = signals.added[component] - local component_index = world.component_index :: jecs.ComponentIndex - assert(component_index[component] == nil, "You cannot use hooks on components you intend to use this signal with") if not listeners then listeners = {} signals.added[component] = listeners @@ -213,7 +163,16 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl listener(entity, id, value) end end - world:set(component, jecs.OnAdd, on_add) + local idr = world.component_index[component] + if idr then + local idr_hook_existing = idr.hooks.on_add + if idr_hook_existing then + table.insert(listeners, idr_hook_existing) + end + idr.hooks.on_add = on_add :: any + else + world:set(component, jecs.OnAdd, on_add) + end end table.insert(listeners, fn) return function() @@ -224,10 +183,12 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl end end - world.changed = function(_, component, fn) + world_mut.changed = function( + _: jecs.World, + component: jecs.Id, + fn: (e: jecs.Entity, id: jecs.Id, value: T) -> () + ) local listeners = signals.emplaced[component] - local component_index = world.component_index :: jecs.ComponentIndex - assert(component_index[component] == nil, "You cannot use hooks on components you intend to use this signal with") if not listeners then listeners = {} signals.emplaced[component] = listeners @@ -236,7 +197,16 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl listener(entity, id, value) end end - world:set(component, jecs.OnChange, on_change) + local idr = world.component_index[component] + if idr then + local idr_hook_existing = idr.hooks.on_change + if idr_hook_existing then + table.insert(listeners, idr_hook_existing) + end + idr.hooks.on_change = on_change :: any + else + world:set(component, jecs.OnChange, on_change) + end end table.insert(listeners, fn) return function() @@ -247,10 +217,12 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl end end - world.removed = function(_, component, fn) + world_mut.removed = function( + _: jecs.World, + component: jecs.Id, + fn: (e: jecs.Entity, id: jecs.Id) -> () + ) local listeners = signals.removed[component] - local component_index = world.component_index :: jecs.ComponentIndex - assert(component_index[component] == nil, "You cannot use hooks on components you intend to use this signal with") if not listeners then listeners = {} signals.removed[component] = listeners @@ -259,7 +231,16 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl listener(entity, id, value) end end - world:set(component, jecs.OnRemove, on_remove) + local idr = world.component_index[component] + if idr then + local idr_hook_existing = idr.hooks.on_remove + if idr_hook_existing then + table.insert(listeners, idr_hook_existing) + end + idr.hooks.on_remove = on_remove :: any + else + world:set(component, jecs.OnRemove, on_remove) + end end table.insert(listeners, fn) return function() @@ -270,15 +251,15 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl end end - world.signals = signals + world_mut.signals = signals - world.observer = observers_new + world_mut.observer = observers_new - world.monitor = monitors_new + world_mut.monitor = monitors_new - world.trackers = {} + world_mut.trackers = {} - return world :: PatchedWorld + return world_mut :: PatchedWorld end return observers_add diff --git a/demo/src/ServerScriptService/systems/players_added.luau b/demo/src/ServerScriptService/systems/players_added.luau index 703c98d..46b1ab1 100644 --- a/demo/src/ServerScriptService/systems/players_added.luau +++ b/demo/src/ServerScriptService/systems/players_added.luau @@ -1,7 +1,6 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local collect = require(ReplicatedStorage.collect) -local types = require(ReplicatedStorage.types) -local ct = require(ReplicatedStorage.components) +local collect = require("../../ReplicatedStorage/collect") +local types = require("../../ReplicatedStorage/types") +local ct = require("../../ReplicatedStorage/components") local Players = game:GetService("Players") local player_added = collect(Players.PlayerAdded) @@ -13,7 +12,8 @@ return function(world: types.World, dt: number) for entity, player in world:query(ct.Player):without(ct.Renderable) do local character = player.Character - if character and character.Parent ~= nil then + if character then + if not character.Parent then world:set(entity, ct.Renderable, character) end end diff --git a/test/addons/observers.luau b/test/addons/observers.luau index 26461c7..1d5a99e 100644 --- a/test/addons/observers.luau +++ b/test/addons/observers.luau @@ -8,6 +8,24 @@ local observers_add = require("@addons/observers") TEST("addons/observers", function() local world = observers_add(jecs.world()) + do CASE "Should not override hook" + local A = world:component() + + local count = 0 + local function counter() + count += 1 + end + + world:set(A, jecs.OnAdd, counter) + world:set(world:entity(), A, true) + CHECK(count == 1) + world:added(A, counter) + world:set(world:entity(), A, true) + + CHECK(count == 3) + print(count) + end + do CASE "Ensure ordering between signals and observers" local A = world:component() local B = world:component() @@ -24,17 +42,12 @@ TEST("addons/observers", function() world:added(A, counter) world:added(A, counter) - world:removed(A, counter) - local e = world:entity() world:add(e, A) CHECK(count == 2) world:add(e, B) CHECK(count == 3) - - world:remove(e, A) - CHECK(count == 4) end do CASE "Rematch entities in observers" @@ -87,10 +100,10 @@ TEST("addons/observers", function() local A = world:component() local callcount = 0 - world:added(A, function(entity) + world:added(A, function(entity) callcount += 1 end) - world:added(A, function(entity) + world:added(A, function(entity) callcount += 1 end) diff --git a/test/tests.luau b/test/tests.luau index c87c7b6..61c089d 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -25,6 +25,30 @@ local entity_visualiser = require("@tools/entity_visualiser") local lifetime_tracker_add = require("@tools/lifetime_tracker") local dwi = entity_visualiser.stringify +TEST("repro", function() + local Model = jecs.component() + local Relation = jecs.component() + local Relation2 = jecs.component() + + local world = jecs.world() + + local e1 = world:entity() + world:set(e1, Model, 2) + + local e2 = world:entity() + world:set(e2, Model, 2) + world:set(e2, jecs.pair(Relation, e1), 5) + + local e3 = world:entity() + world:set(e3, Model, 2) + world:set(e3, jecs.pair(Relation, e1), 5) + + world:delete(e1) + + for _ in world:query(Model) do end + jecs.ECS_META_RESET() +end) + TEST("world:add()", function() do CASE "idempotent" local world = jecs.world() diff --git a/wally.toml b/wally.toml index 125c85b..af3bb5d 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "ukendio/jecs" -version = "0.5.5" +version = "0.6.0-rc.1" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" license = "MIT"