Skip to content

Commit 30597ed

Browse files
committed
Add query:fini and query:archetypes(override) and changes to OB
1 parent d4a7f1d commit 30597ed

File tree

6 files changed

+531
-236
lines changed

6 files changed

+531
-236
lines changed

examples/networking/networking_recv.luau

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ end
4747
-- local tgt = tonumber(tokens[2]) :: jecs.Entity
4848

4949
-- rel = ecs_ensure_entity(world, rel)
50+
--
51+
-- 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
5052
-- tgt = ecs_ensure_entity(world, tgt)
5153

5254
-- return jecs.pair(rel, tgt)

how_to/111_signals.luau

Lines changed: 81 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,81 @@
1-
--[[
2-
Signals let you subscribe to component add, change, and remove events with
3-
multiple listeners per component. Unlike hooks (see 110_hooks.luau), which
4-
allow only one OnAdd, OnChange, and OnRemove per component, signals support
5-
any number of subscribers and each subscription returns an unsubscribe
6-
function so you can clean up when you no longer need to listen.
7-
8-
Use signals when you need several independent systems to react to the same
9-
component lifecycle events, or when you want to subscribe and unsubscribe
10-
dynamically (e.g. a UI that only cares while it's mounted).
11-
]]
12-
13-
local jecs = require("@jecs")
14-
local world = jecs.world()
15-
16-
local Position = world:component() :: jecs.Id<{ x: number, y: number }>
17-
18-
--[[
19-
world:added(component, fn)
20-
21-
Subscribe to "component added" events. Your callback is invoked with:
22-
(entity, id, value, oldarchetype) whenever the component is added to an entity.
23-
24-
Returns a function; call it to unsubscribe.
25-
]]
26-
27-
local unsub_added = world:added(Position, function(entity, id, value, oldarchetype)
28-
print(`Position added to entity {entity}: ({value.x}, {value.y})`)
29-
end)
30-
31-
--[[
32-
world:changed(component, fn)
33-
34-
Subscribe to "component changed" events. Your callback is invoked with:
35-
(entity, id, value, oldarchetype) whenever the component's value is updated
36-
on an entity (e.g. via world:set).
37-
38-
Returns a function; call it to unsubscribe.
39-
]]
40-
41-
local unsub_changed = world:changed(Position, function(entity, id, value, oldarchetype)
42-
print(`Position changed on entity {entity}: ({value.x}, {value.y})`)
43-
end)
44-
45-
--[[
46-
world:removed(component, fn)
47-
48-
Subscribe to "component removed" events. Your callback is invoked with:
49-
(entity, id, delete?) when the component is removed. The third argument
50-
`delete` is true when the entity is being deleted, false or nil when
51-
only the component was removed (same semantics as OnRemove in 110_hooks).
52-
53-
Returns a function; call it to unsubscribe.
54-
]]
55-
56-
local unsub_removed = world:removed(Position, function(entity, id, delete)
57-
if delete then
58-
print(`Entity {entity} deleted (had Position)`)
59-
else
60-
print(`Position removed from entity {entity}`)
61-
end
62-
end)
63-
64-
local e = world:entity()
65-
world:set(e, Position, { x = 10, y = 20 }) -- added
66-
world:set(e, Position, { x = 30, y = 40 }) -- changed
67-
world:remove(e, Position) -- removed
68-
69-
world:added(Position, function(entity)
70-
print("Second listener: Position added")
71-
end)
72-
73-
world:set(e, Position, { x = 0, y = 0 }) -- Multiple listeners are all invoked
74-
75-
-- Unsubscribe when you no longer need to listen
76-
unsub_added()
77-
unsub_changed()
78-
unsub_removed()
79-
80-
world:set(e, Position, { x = 1, y = 1 })
81-
world:remove(e, Position)
1+
--[[
2+
Signals let you subscribe to component add, change, and remove events with
3+
multiple listeners per component. Unlike hooks (see 110_hooks.luau), which
4+
allow only one OnAdd, OnChange, and OnRemove per component, signals support
5+
any number of subscribers and each subscription returns an unsubscribe
6+
function so you can clean up when you no longer need to listen.
7+
8+
Use signals when you need several independent systems to react to the same
9+
component lifecycle events, or when you want to subscribe and unsubscribe
10+
dynamically (e.g. a UI that only cares while it's mounted).
11+
]]
12+
13+
local jecs = require("@jecs")
14+
local world = jecs.world()
15+
16+
local Position = world:component() :: jecs.Id<{ x: number, y: number }>
17+
18+
--[[
19+
world:added(component, fn)
20+
21+
Subscribe to "component added" events. Your callback is invoked with:
22+
(entity, id, value, oldarchetype) whenever the component is added to an entity.
23+
24+
Returns a function; call it to unsubscribe.
25+
]]
26+
27+
local unsub_added = world:added(Position, function(entity, id, value, oldarchetype)
28+
print(`Position added to entity {entity}: ({value.x}, {value.y})`)
29+
end)
30+
31+
--[[
32+
world:changed(component, fn)
33+
34+
Subscribe to "component changed" events. Your callback is invoked with:
35+
(entity, id, value, oldarchetype) whenever the component's value is updated
36+
on an entity (e.g. via world:set).
37+
38+
Returns a function; call it to unsubscribe.
39+
]]
40+
41+
local unsub_changed = world:changed(Position, function(entity, id, value, oldarchetype)
42+
print(`Position changed on entity {entity}: ({value.x}, {value.y})`)
43+
end)
44+
45+
--[[
46+
world:removed(component, fn)
47+
48+
Subscribe to "component removed" events. Your callback is invoked with:
49+
(entity, id, delete?) when the component is removed. The third argument
50+
`delete` is true when the entity is being deleted, false or nil when
51+
only the component was removed (same semantics as OnRemove in 110_hooks).
52+
53+
Returns a function; call it to unsubscribe.
54+
]]
55+
56+
local unsub_removed = world:removed(Position, function(entity, id, delete)
57+
if delete then
58+
print(`Entity {entity} deleted (had Position)`)
59+
else
60+
print(`Position removed from entity {entity}`)
61+
end
62+
end)
63+
64+
local e = world:entity()
65+
world:set(e, Position, { x = 10, y = 20 }) -- added
66+
world:set(e, Position, { x = 30, y = 40 }) -- changed
67+
world:remove(e, Position) -- removed
68+
69+
world:added(Position, function(entity)
70+
print("Second listener: Position added")
71+
end)
72+
73+
world:set(e, Position, { x = 0, y = 0 }) -- Multiple listeners are all invoked
74+
75+
-- Unsubscribe when you no longer need to listen
76+
unsub_added()
77+
unsub_changed()
78+
unsub_removed()
79+
80+
world:set(e, Position, { x = 1, y = 1 })
81+
world:remove(e, Position)

modules/OB/module.luau

Lines changed: 26 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,17 @@ local jecs = require("@jecs")
33

44
type World = jecs.World
55

6-
type Id<T=any> = jecs.Id<T>
7-
6+
type Id<T=any> = jecs.Id<any>
7+
8+
local function duplicate(query: jecs.Query<...any>): jecs.CachedQuery<...any>
9+
local world = (query :: jecs.Query<any> & { world: World }).world
10+
local dup = world:query()
11+
dup.filter_with = table.clone(query.filter_with)
12+
if query.filter_without then
13+
dup.filter_without = query.filter_without
14+
end
15+
return dup:cached()
16+
end
817

918
export type Observer = {
1019
disconnect: () -> (),
@@ -20,7 +29,7 @@ local function observers_new(
2029
query: jecs.Query<...any>,
2130
callback: (jecs.Entity) -> ()
2231
): Observer
23-
local cachedquery = query:cached()
32+
local cachedquery = duplicate(query)
2433

2534
local world = (cachedquery :: jecs.Query<any> & { world: World }).world
2635
callback = callback
@@ -134,7 +143,7 @@ local function observers_new(
134143
end
135144

136145
local function monitors_new(query: jecs.Query<...any>): Monitor
137-
local cachedquery = query:cached()
146+
local cachedquery = duplicate(query)
138147

139148
local world = (cachedquery :: jecs.Query<...any> & { world: World }).world :: jecs.World
140149

@@ -151,16 +160,10 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
151160
local callback_added: ((jecs.Entity) -> ())?
152161
local callback_removed: ((jecs.Entity) -> ())?
153162

154-
-- NOTE(marcus): Track the last (entity, old archetype) pair we processed to detect bulk operations.
155-
-- During bulk_insert from ROOT_ARCHETYPE, the entity is moved to the target archetype first,
156-
-- then all on_add callbacks fire sequentially with the same oldarchetype for the same entity.
157-
-- We track both entity and old archetype to distinguish between:
158-
-- 1. Same entity, same old archetype (bulk operation - skip)
159-
-- 2. Different entity, same old archetype (separate operation - don't skip)
160163
local last_old_archetype: jecs.Archetype? = nil
161164
local last_entity: jecs.Entity? = nil
162165

163-
local function emplaced<a>(
166+
local function emplaced<a>(
164167
entity: jecs.Entity,
165168
id: jecs.Id<a>,
166169
value: a,
@@ -173,10 +176,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
173176
local r = jecs.entity_index_try_get_fast(
174177
entity_index, entity :: any) :: jecs.Record
175178
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
176-
-- NOTE(marcus): Skip if we've seen this exact (entity, old archetype) combination before
177-
-- AND this component is in the query's terms. This detects bulk operations where
178-
-- the same entity transitions with multiple components, while allowing different
179-
-- entities to trigger even if they share the same old archetype.
180179
if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then
181180
return
182181
end
@@ -185,52 +184,31 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
185184
last_entity = entity
186185
callback_added(entity)
187186
else
188-
-- NOTE(marcus): Clear tracking when we see a different transition pattern
189187
last_old_archetype = nil
190188
last_entity = nil
191189
end
192190
end
193191

194-
-- Track which entity we've already processed for deletion to avoid duplicate callbacks
195-
-- during bulk deletion where multiple components are removed with delete=true
196-
local last_deleted_entity: jecs.Entity? = nil
197-
198192
local function removed(entity: jecs.Entity, component: jecs.Component, delete:boolean?)
199193
if callback_removed == nil then
200194
return
201195
end
202-
203-
if delete then
204-
-- Deletion is a bulk removal - all components are removed with delete=true
205-
-- We should only trigger the callback once per entity, not once per component
206-
if last_deleted_entity == entity then
207-
return
208-
end
209-
210-
local r = jecs.record(world, entity)
211-
if r and r.archetype and archetypes[r.archetype.id] then
212-
-- Entity was in the monitor before deletion
213-
last_deleted_entity = entity
214-
-- Clear tracking when entity is deleted
215-
last_old_archetype = nil
216-
last_entity = nil
217-
callback_removed(entity)
218-
end
219-
return
220-
end
221-
196+
222197
local r = jecs.record(world, entity)
198+
if not r then return end
199+
223200
local src = r.archetype
201+
if not src then return end
202+
203+
if not archetypes[src.id] then return end
224204

225-
local dst = jecs.archetype_traverse_remove(world, component, src)
226-
227-
if not archetypes[dst.id] then
228-
-- Clear tracking when entity leaves the monitor to allow re-entry
229-
last_old_archetype = nil
230-
last_entity = nil
231-
last_deleted_entity = nil
232-
callback_removed(entity)
205+
if last_entity == entity and last_old_archetype == src then
206+
return
233207
end
208+
209+
last_entity = entity
210+
last_old_archetype = src
211+
callback_removed(entity)
234212
end
235213

236214
local cleanup = {}
@@ -254,8 +232,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
254232
entity_index, entity :: any) :: jecs.Record
255233

256234
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
257-
-- NOTE(marcus): Skip if we've seen this exact (entity, old archetype) combination before
258-
-- AND this component is in the query's terms.
259235
if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then
260236
return
261237
end
@@ -264,7 +240,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
264240
last_entity = entity
265241
callback_added(entity)
266242
else
267-
-- Clear tracking when we see a different transition pattern
268243
last_old_archetype = nil
269244
last_entity = nil
270245
end
@@ -314,11 +289,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
314289
return
315290
end
316291

317-
-- NOTE(marcus): This check that it was presently in
318-
-- the query but distinctively leaves is important as
319-
-- sometimes it could be too eager to report that it
320-
-- removed a component even though the entity is not
321-
-- apart of the monitor
322292
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
323293
last_old_archetype = nil
324294
callback_removed(entity)
@@ -364,10 +334,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
364334
return
365335
end
366336

367-
-- NOTE(marcus): Sometimes OnAdd listeners for excluded
368-
-- terms are too eager to report that it is leaving the
369-
-- monitor even though the entity is not apart of it
370-
-- already.
371337
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
372338
callback_removed(entity)
373339
end
@@ -386,11 +352,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
386352
end
387353
local dst = jecs.archetype_traverse_remove(world, id, archetype)
388354

389-
-- NOTE(marcus): Inversely with the opposite operation, you
390-
-- only need to check if it is going to enter the query once
391-
-- because world:remove already stipulates that it is
392-
-- idempotent so that this hook won't be invoked if it is
393-
-- was already removed.
394355
if archetypes[dst.id] then
395356
callback_added(entity)
396357
end

0 commit comments

Comments
 (0)