mirror of
https://github.com/Ukendio/jecs.git
synced 2026-03-18 00:44:32 +00:00
Add query:fini and query:archetypes(override) and changes to OB
This commit is contained in:
parent
d4a7f1d86c
commit
30597ed389
6 changed files with 530 additions and 235 deletions
|
|
@ -47,6 +47,8 @@ end
|
|||
-- local tgt = tonumber(tokens[2]) :: jecs.Entity
|
||||
|
||||
-- rel = ecs_ensure_entity(world, rel)
|
||||
--
|
||||
-- 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
|
||||
-- tgt = ecs_ensure_entity(world, tgt)
|
||||
|
||||
-- return jecs.pair(rel, tgt)
|
||||
|
|
|
|||
|
|
@ -1,81 +1,81 @@
|
|||
--[[
|
||||
Signals let you subscribe to component add, change, and remove events with
|
||||
multiple listeners per component. Unlike hooks (see 110_hooks.luau), which
|
||||
allow only one OnAdd, OnChange, and OnRemove per component, signals support
|
||||
any number of subscribers and each subscription returns an unsubscribe
|
||||
function so you can clean up when you no longer need to listen.
|
||||
|
||||
Use signals when you need several independent systems to react to the same
|
||||
component lifecycle events, or when you want to subscribe and unsubscribe
|
||||
dynamically (e.g. a UI that only cares while it's mounted).
|
||||
]]
|
||||
|
||||
local jecs = require("@jecs")
|
||||
local world = jecs.world()
|
||||
|
||||
local Position = world:component() :: jecs.Id<{ x: number, y: number }>
|
||||
|
||||
--[[
|
||||
world:added(component, fn)
|
||||
|
||||
Subscribe to "component added" events. Your callback is invoked with:
|
||||
(entity, id, value, oldarchetype) whenever the component is added to an entity.
|
||||
|
||||
Returns a function; call it to unsubscribe.
|
||||
]]
|
||||
|
||||
local unsub_added = world:added(Position, function(entity, id, value, oldarchetype)
|
||||
print(`Position added to entity {entity}: ({value.x}, {value.y})`)
|
||||
end)
|
||||
|
||||
--[[
|
||||
world:changed(component, fn)
|
||||
|
||||
Subscribe to "component changed" events. Your callback is invoked with:
|
||||
(entity, id, value, oldarchetype) whenever the component's value is updated
|
||||
on an entity (e.g. via world:set).
|
||||
|
||||
Returns a function; call it to unsubscribe.
|
||||
]]
|
||||
|
||||
local unsub_changed = world:changed(Position, function(entity, id, value, oldarchetype)
|
||||
print(`Position changed on entity {entity}: ({value.x}, {value.y})`)
|
||||
end)
|
||||
|
||||
--[[
|
||||
world:removed(component, fn)
|
||||
|
||||
Subscribe to "component removed" events. Your callback is invoked with:
|
||||
(entity, id, delete?) when the component is removed. The third argument
|
||||
`delete` is true when the entity is being deleted, false or nil when
|
||||
only the component was removed (same semantics as OnRemove in 110_hooks).
|
||||
|
||||
Returns a function; call it to unsubscribe.
|
||||
]]
|
||||
|
||||
local unsub_removed = world:removed(Position, function(entity, id, delete)
|
||||
if delete then
|
||||
print(`Entity {entity} deleted (had Position)`)
|
||||
else
|
||||
print(`Position removed from entity {entity}`)
|
||||
end
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:set(e, Position, { x = 10, y = 20 }) -- added
|
||||
world:set(e, Position, { x = 30, y = 40 }) -- changed
|
||||
world:remove(e, Position) -- removed
|
||||
|
||||
world:added(Position, function(entity)
|
||||
print("Second listener: Position added")
|
||||
end)
|
||||
|
||||
world:set(e, Position, { x = 0, y = 0 }) -- Multiple listeners are all invoked
|
||||
|
||||
-- Unsubscribe when you no longer need to listen
|
||||
unsub_added()
|
||||
unsub_changed()
|
||||
unsub_removed()
|
||||
|
||||
world:set(e, Position, { x = 1, y = 1 })
|
||||
world:remove(e, Position)
|
||||
--[[
|
||||
Signals let you subscribe to component add, change, and remove events with
|
||||
multiple listeners per component. Unlike hooks (see 110_hooks.luau), which
|
||||
allow only one OnAdd, OnChange, and OnRemove per component, signals support
|
||||
any number of subscribers and each subscription returns an unsubscribe
|
||||
function so you can clean up when you no longer need to listen.
|
||||
|
||||
Use signals when you need several independent systems to react to the same
|
||||
component lifecycle events, or when you want to subscribe and unsubscribe
|
||||
dynamically (e.g. a UI that only cares while it's mounted).
|
||||
]]
|
||||
|
||||
local jecs = require("@jecs")
|
||||
local world = jecs.world()
|
||||
|
||||
local Position = world:component() :: jecs.Id<{ x: number, y: number }>
|
||||
|
||||
--[[
|
||||
world:added(component, fn)
|
||||
|
||||
Subscribe to "component added" events. Your callback is invoked with:
|
||||
(entity, id, value, oldarchetype) whenever the component is added to an entity.
|
||||
|
||||
Returns a function; call it to unsubscribe.
|
||||
]]
|
||||
|
||||
local unsub_added = world:added(Position, function(entity, id, value, oldarchetype)
|
||||
print(`Position added to entity {entity}: ({value.x}, {value.y})`)
|
||||
end)
|
||||
|
||||
--[[
|
||||
world:changed(component, fn)
|
||||
|
||||
Subscribe to "component changed" events. Your callback is invoked with:
|
||||
(entity, id, value, oldarchetype) whenever the component's value is updated
|
||||
on an entity (e.g. via world:set).
|
||||
|
||||
Returns a function; call it to unsubscribe.
|
||||
]]
|
||||
|
||||
local unsub_changed = world:changed(Position, function(entity, id, value, oldarchetype)
|
||||
print(`Position changed on entity {entity}: ({value.x}, {value.y})`)
|
||||
end)
|
||||
|
||||
--[[
|
||||
world:removed(component, fn)
|
||||
|
||||
Subscribe to "component removed" events. Your callback is invoked with:
|
||||
(entity, id, delete?) when the component is removed. The third argument
|
||||
`delete` is true when the entity is being deleted, false or nil when
|
||||
only the component was removed (same semantics as OnRemove in 110_hooks).
|
||||
|
||||
Returns a function; call it to unsubscribe.
|
||||
]]
|
||||
|
||||
local unsub_removed = world:removed(Position, function(entity, id, delete)
|
||||
if delete then
|
||||
print(`Entity {entity} deleted (had Position)`)
|
||||
else
|
||||
print(`Position removed from entity {entity}`)
|
||||
end
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:set(e, Position, { x = 10, y = 20 }) -- added
|
||||
world:set(e, Position, { x = 30, y = 40 }) -- changed
|
||||
world:remove(e, Position) -- removed
|
||||
|
||||
world:added(Position, function(entity)
|
||||
print("Second listener: Position added")
|
||||
end)
|
||||
|
||||
world:set(e, Position, { x = 0, y = 0 }) -- Multiple listeners are all invoked
|
||||
|
||||
-- Unsubscribe when you no longer need to listen
|
||||
unsub_added()
|
||||
unsub_changed()
|
||||
unsub_removed()
|
||||
|
||||
world:set(e, Position, { x = 1, y = 1 })
|
||||
world:remove(e, Position)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,17 @@ local jecs = require("@jecs")
|
|||
|
||||
type World = jecs.World
|
||||
|
||||
type Id<T=any> = jecs.Id<T>
|
||||
type Id<T=any> = jecs.Id<any>
|
||||
|
||||
local function duplicate(query: jecs.Query<...any>): jecs.CachedQuery<...any>
|
||||
local world = (query :: jecs.Query<any> & { world: World }).world
|
||||
local dup = world:query()
|
||||
dup.filter_with = table.clone(query.filter_with)
|
||||
if query.filter_without then
|
||||
dup.filter_without = query.filter_without
|
||||
end
|
||||
return dup:cached()
|
||||
end
|
||||
|
||||
export type Observer = {
|
||||
disconnect: () -> (),
|
||||
|
|
@ -20,7 +29,7 @@ local function observers_new(
|
|||
query: jecs.Query<...any>,
|
||||
callback: (jecs.Entity) -> ()
|
||||
): Observer
|
||||
local cachedquery = query:cached()
|
||||
local cachedquery = duplicate(query)
|
||||
|
||||
local world = (cachedquery :: jecs.Query<any> & { world: World }).world
|
||||
callback = callback
|
||||
|
|
@ -134,7 +143,7 @@ local function observers_new(
|
|||
end
|
||||
|
||||
local function monitors_new(query: jecs.Query<...any>): Monitor
|
||||
local cachedquery = query:cached()
|
||||
local cachedquery = duplicate(query)
|
||||
|
||||
local world = (cachedquery :: jecs.Query<...any> & { world: World }).world :: jecs.World
|
||||
|
||||
|
|
@ -151,16 +160,10 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
local callback_added: ((jecs.Entity) -> ())?
|
||||
local callback_removed: ((jecs.Entity) -> ())?
|
||||
|
||||
-- NOTE(marcus): Track the last (entity, old archetype) pair we processed to detect bulk operations.
|
||||
-- During bulk_insert from ROOT_ARCHETYPE, the entity is moved to the target archetype first,
|
||||
-- then all on_add callbacks fire sequentially with the same oldarchetype for the same entity.
|
||||
-- We track both entity and old archetype to distinguish between:
|
||||
-- 1. Same entity, same old archetype (bulk operation - skip)
|
||||
-- 2. Different entity, same old archetype (separate operation - don't skip)
|
||||
local last_old_archetype: jecs.Archetype? = nil
|
||||
local last_entity: jecs.Entity? = nil
|
||||
|
||||
local function emplaced<a>(
|
||||
local function emplaced<a>(
|
||||
entity: jecs.Entity,
|
||||
id: jecs.Id<a>,
|
||||
value: a,
|
||||
|
|
@ -173,10 +176,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
local r = jecs.entity_index_try_get_fast(
|
||||
entity_index, entity :: any) :: jecs.Record
|
||||
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
|
||||
-- NOTE(marcus): Skip if we've seen this exact (entity, old archetype) combination before
|
||||
-- AND this component is in the query's terms. This detects bulk operations where
|
||||
-- the same entity transitions with multiple components, while allowing different
|
||||
-- entities to trigger even if they share the same old archetype.
|
||||
if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then
|
||||
return
|
||||
end
|
||||
|
|
@ -185,52 +184,31 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
last_entity = entity
|
||||
callback_added(entity)
|
||||
else
|
||||
-- NOTE(marcus): Clear tracking when we see a different transition pattern
|
||||
last_old_archetype = nil
|
||||
last_entity = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Track which entity we've already processed for deletion to avoid duplicate callbacks
|
||||
-- during bulk deletion where multiple components are removed with delete=true
|
||||
local last_deleted_entity: jecs.Entity? = nil
|
||||
|
||||
local function removed(entity: jecs.Entity, component: jecs.Component, delete:boolean?)
|
||||
if callback_removed == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local r = jecs.record(world, entity)
|
||||
if not r then return end
|
||||
|
||||
local src = r.archetype
|
||||
if not src then return end
|
||||
|
||||
if not archetypes[src.id] then return end
|
||||
|
||||
if delete then
|
||||
-- Deletion is a bulk removal - all components are removed with delete=true
|
||||
-- We should only trigger the callback once per entity, not once per component
|
||||
if last_deleted_entity == entity then
|
||||
return
|
||||
end
|
||||
|
||||
local r = jecs.record(world, entity)
|
||||
if r and r.archetype and archetypes[r.archetype.id] then
|
||||
-- Entity was in the monitor before deletion
|
||||
last_deleted_entity = entity
|
||||
-- Clear tracking when entity is deleted
|
||||
last_old_archetype = nil
|
||||
last_entity = nil
|
||||
callback_removed(entity)
|
||||
end
|
||||
if last_entity == entity and last_old_archetype == src then
|
||||
return
|
||||
end
|
||||
|
||||
local r = jecs.record(world, entity)
|
||||
local src = r.archetype
|
||||
|
||||
local dst = jecs.archetype_traverse_remove(world, component, src)
|
||||
|
||||
if not archetypes[dst.id] then
|
||||
-- Clear tracking when entity leaves the monitor to allow re-entry
|
||||
last_old_archetype = nil
|
||||
last_entity = nil
|
||||
last_deleted_entity = nil
|
||||
callback_removed(entity)
|
||||
end
|
||||
last_entity = entity
|
||||
last_old_archetype = src
|
||||
callback_removed(entity)
|
||||
end
|
||||
|
||||
local cleanup = {}
|
||||
|
|
@ -254,8 +232,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
entity_index, entity :: any) :: jecs.Record
|
||||
|
||||
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
|
||||
-- NOTE(marcus): Skip if we've seen this exact (entity, old archetype) combination before
|
||||
-- AND this component is in the query's terms.
|
||||
if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then
|
||||
return
|
||||
end
|
||||
|
|
@ -264,7 +240,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
last_entity = entity
|
||||
callback_added(entity)
|
||||
else
|
||||
-- Clear tracking when we see a different transition pattern
|
||||
last_old_archetype = nil
|
||||
last_entity = nil
|
||||
end
|
||||
|
|
@ -314,11 +289,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
return
|
||||
end
|
||||
|
||||
-- NOTE(marcus): This check that it was presently in
|
||||
-- the query but distinctively leaves is important as
|
||||
-- sometimes it could be too eager to report that it
|
||||
-- removed a component even though the entity is not
|
||||
-- apart of the monitor
|
||||
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
|
||||
last_old_archetype = nil
|
||||
callback_removed(entity)
|
||||
|
|
@ -364,10 +334,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
return
|
||||
end
|
||||
|
||||
-- NOTE(marcus): Sometimes OnAdd listeners for excluded
|
||||
-- terms are too eager to report that it is leaving the
|
||||
-- monitor even though the entity is not apart of it
|
||||
-- already.
|
||||
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
|
||||
callback_removed(entity)
|
||||
end
|
||||
|
|
@ -386,11 +352,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
end
|
||||
local dst = jecs.archetype_traverse_remove(world, id, archetype)
|
||||
|
||||
-- NOTE(marcus): Inversely with the opposite operation, you
|
||||
-- only need to check if it is going to enter the query once
|
||||
-- because world:remove already stipulates that it is
|
||||
-- idempotent so that this hook won't be invoked if it is
|
||||
-- was already removed.
|
||||
if archetypes[dst.id] then
|
||||
callback_added(entity)
|
||||
end
|
||||
|
|
|
|||
183
src/jecs.luau
183
src/jecs.luau
|
|
@ -80,15 +80,17 @@ export type Id2<First, Second=nil> = ecs_id_t<First, Second>
|
|||
|
||||
export type Item<T...> = (self: Query<T...>) -> (Entity, T...)
|
||||
export type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
|
||||
export type CachedIter<T...> = (query: CachedQuery<T...>) -> () -> (Entity, T...)
|
||||
export type Cached_Query_Iter<T...> = (query: Cached_Query<T...>) -> () -> (Entity, T...)
|
||||
|
||||
type TypePack<T...> = (T...) -> never
|
||||
|
||||
export type CachedQuery<T...> = typeof(setmetatable(
|
||||
export type Cached_Query<T...> = typeof(setmetatable(
|
||||
{} :: {
|
||||
iter: CachedIter<T...>,
|
||||
archetypes: (CachedQuery<T...>) -> { Archetype },
|
||||
has: (CachedQuery<T...>, Entity) -> boolean,
|
||||
iter: Cached_Query_Iter<T...>,
|
||||
archetypes: (Cached_Query<T...>) -> { Archetype },
|
||||
has: (Cached_Query<T...>, Entity) -> boolean,
|
||||
fini: (Cached_Query<T...>) -> (),
|
||||
|
||||
ids: { Id<any> },
|
||||
filter_with: { Id<any> }?,
|
||||
filter_without: { Id<any> }?,
|
||||
|
|
@ -96,7 +98,7 @@ export type CachedQuery<T...> = typeof(setmetatable(
|
|||
-- world: World
|
||||
},
|
||||
{} :: {
|
||||
__iter: CachedIter<T...>,
|
||||
__iter: Cached_Query_Iter<T...>,
|
||||
})
|
||||
)
|
||||
|
||||
|
|
@ -106,7 +108,7 @@ export type Query<T...> = typeof(setmetatable(
|
|||
with: ((Query<T...>, ...Component) -> Query<T...>),
|
||||
without: ((Query<T...>, ...Component) -> Query<T...>),
|
||||
archetypes: (Query<T...>) -> { Archetype },
|
||||
cached: (Query<T...>) -> CachedQuery<T...>,
|
||||
cached: (Query<T...>) -> Cached_Query<T...>,
|
||||
has: (Query<T...>, Entity) -> boolean,
|
||||
ids: { Id<any> },
|
||||
filter_with: { Id<any> }?,
|
||||
|
|
@ -1240,9 +1242,9 @@ end
|
|||
|
||||
local function NOOP() end
|
||||
|
||||
local function query_archetypes(query: query)
|
||||
local function query_archetypes(query: query, override: boolean?)
|
||||
local compatible_archetypes = query.compatible_archetypes
|
||||
if not compatible_archetypes then
|
||||
if not compatible_archetypes or override then
|
||||
compatible_archetypes = {}
|
||||
query.compatible_archetypes = compatible_archetypes
|
||||
|
||||
|
|
@ -1747,80 +1749,80 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
|
|||
col4_u = col4
|
||||
col5_u = col5
|
||||
col6_u = col6
|
||||
col7_u = col7
|
||||
end
|
||||
|
||||
local row = i_u
|
||||
i_u -= 1
|
||||
|
||||
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row]
|
||||
end
|
||||
else
|
||||
local output = {}
|
||||
local ids_len = #ids_u
|
||||
function world_query_iter_next(): any
|
||||
local entities = entities_u
|
||||
local e = entities[i_u]
|
||||
local col0 = col0_u
|
||||
local col1 = col1_u
|
||||
local col2 = col2_u
|
||||
local col3 = col3_u
|
||||
local col4 = col4_u
|
||||
local col5 = col5_u
|
||||
local col6 = col6_u
|
||||
local col7 = col7_u
|
||||
local ids = ids_u
|
||||
local columns_map = columns_map_u
|
||||
|
||||
while e == nil do
|
||||
last_archetype_u += 1
|
||||
local compatible_archetypes = compatible_archetypes_u
|
||||
local archetype = compatible_archetypes[last_archetype_u]
|
||||
archetype_u = archetype
|
||||
|
||||
if not archetype then
|
||||
return nil
|
||||
col7_u = col7
|
||||
end
|
||||
entities = archetype.entities
|
||||
i_u = #entities
|
||||
if i_u == 0 then
|
||||
continue
|
||||
|
||||
local row = i_u
|
||||
i_u -= 1
|
||||
|
||||
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row]
|
||||
end
|
||||
else
|
||||
local output = {}
|
||||
local ids_len = #ids_u
|
||||
function world_query_iter_next(): any
|
||||
local entities = entities_u
|
||||
local e = entities[i_u]
|
||||
local col0 = col0_u
|
||||
local col1 = col1_u
|
||||
local col2 = col2_u
|
||||
local col3 = col3_u
|
||||
local col4 = col4_u
|
||||
local col5 = col5_u
|
||||
local col6 = col6_u
|
||||
local col7 = col7_u
|
||||
local ids = ids_u
|
||||
local columns_map = columns_map_u
|
||||
|
||||
while e == nil do
|
||||
last_archetype_u += 1
|
||||
local compatible_archetypes = compatible_archetypes_u
|
||||
local archetype = compatible_archetypes[last_archetype_u]
|
||||
archetype_u = archetype
|
||||
|
||||
if not archetype then
|
||||
return nil
|
||||
end
|
||||
entities = archetype.entities
|
||||
i_u = #entities
|
||||
if i_u == 0 then
|
||||
continue
|
||||
end
|
||||
e = entities[i_u]
|
||||
entities_u = entities
|
||||
columns_map = archetype.columns_map
|
||||
columns_map_u = columns_map
|
||||
col0 = columns_map[id0]
|
||||
col1 = columns_map[id1]
|
||||
col2 = columns_map[id2]
|
||||
col3 = columns_map[id3]
|
||||
col4 = columns_map[id4]
|
||||
col5 = columns_map[id5]
|
||||
col6 = columns_map[id6]
|
||||
col7 = columns_map[id7]
|
||||
col0_u = col0
|
||||
col1_u = col1
|
||||
col2_u = col2
|
||||
col3_u = col3
|
||||
col4_u = col4
|
||||
col5_u = col5
|
||||
col6_u = col6
|
||||
col7_u = col7
|
||||
end
|
||||
e = entities[i_u]
|
||||
entities_u = entities
|
||||
columns_map = archetype.columns_map
|
||||
columns_map_u = columns_map
|
||||
col0 = columns_map[id0]
|
||||
col1 = columns_map[id1]
|
||||
col2 = columns_map[id2]
|
||||
col3 = columns_map[id3]
|
||||
col4 = columns_map[id4]
|
||||
col5 = columns_map[id5]
|
||||
col6 = columns_map[id6]
|
||||
col7 = columns_map[id7]
|
||||
col0_u = col0
|
||||
col1_u = col1
|
||||
col2_u = col2
|
||||
col3_u = col3
|
||||
col4_u = col4
|
||||
col5_u = col5
|
||||
col6_u = col6
|
||||
col7_u = col7
|
||||
|
||||
local row = i_u
|
||||
i_u -= 1
|
||||
|
||||
for i = 9, ids_len do
|
||||
output[i - 8] = columns_map[ids[i]::any][row]
|
||||
end
|
||||
|
||||
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row], unpack(output)
|
||||
end
|
||||
|
||||
local row = i_u
|
||||
i_u -= 1
|
||||
|
||||
for i = 9, ids_len do
|
||||
output[i - 8] = columns_map[ids[i]::any][row]
|
||||
end
|
||||
|
||||
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row], unpack(output)
|
||||
end
|
||||
end
|
||||
|
||||
query.next = world_query_iter_next
|
||||
return world_query_iter_next
|
||||
|
||||
query.next = world_query_iter_next
|
||||
return world_query_iter_next
|
||||
end
|
||||
|
||||
local function query_iter(query): () -> (number, ...any)
|
||||
|
|
@ -1917,7 +1919,7 @@ local function query_cached(query: QueryInner)
|
|||
table.insert(query_cache_on_create, observer_for_create)
|
||||
table.insert(query_cache_on_delete, observer_for_delete)
|
||||
|
||||
local function cached_query_iter()
|
||||
local function cached_query_iter()
|
||||
last_archetype_u = 1
|
||||
local compatible_archetypes = compatible_archetypes_u
|
||||
archetype_u = compatible_archetypes[last_archetype_u]
|
||||
|
|
@ -2416,7 +2418,7 @@ local function query_cached(query: QueryInner)
|
|||
|
||||
local eindex = world.entity_index :: entityindex
|
||||
|
||||
local function cached_query_has(entity): boolean
|
||||
local function cached_query_has(_, entity): boolean
|
||||
local r = entity_index_try_get_fast(eindex, entity)
|
||||
if not r then
|
||||
return false
|
||||
|
|
@ -2429,12 +2431,31 @@ local function query_cached(query: QueryInner)
|
|||
|
||||
return archetypes_map[entityarchetype.id] ~= nil
|
||||
end
|
||||
|
||||
local function cached_query_fini()
|
||||
local create_pos = table.find(query_cache_on_create, observer_for_create)
|
||||
if create_pos then
|
||||
table.remove(query_cache_on_create, create_pos)
|
||||
end
|
||||
|
||||
local delete_pos = table.find(query_cache_on_delete, observer_for_delete)
|
||||
if delete_pos then
|
||||
table.remove(query_cache_on_delete, delete_pos)
|
||||
end
|
||||
|
||||
compatible_archetypes_u = nil
|
||||
-- NOTE(marcus): Maybe we have to be even more aggressive with cleaning
|
||||
-- things up to ensure it the memory is free`d. But since most of it are
|
||||
-- references we cannot be sure that someone is holding onto them making
|
||||
-- it implausible to free the memory anyways
|
||||
end
|
||||
|
||||
local cached_query = query :: any
|
||||
cached_query.archetypes = query_archetypes
|
||||
cached_query.__iter = cached_query_iter
|
||||
cached_query.iter = cached_query_iter
|
||||
cached_query.has = cached_query_has
|
||||
cached_query.fini = cached_query_fini
|
||||
setmetatable(cached_query, cached_query)
|
||||
return cached_query
|
||||
end
|
||||
|
|
|
|||
315
test/ob.luau
315
test/ob.luau
|
|
@ -3,7 +3,7 @@ local testkit = require("@modules/testkit")
|
|||
local test = testkit.test()
|
||||
local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
|
||||
local FOCUS = test.FOCUS
|
||||
local ob = require("@modules/ob")
|
||||
local ob = require("@modules/OB/module")
|
||||
|
||||
TEST("modules/ob::observer", function()
|
||||
local world = jecs.world()
|
||||
|
|
@ -292,6 +292,36 @@ end)
|
|||
TEST("modules/ob::monitor", function()
|
||||
local world = jecs.world()
|
||||
|
||||
do CASE "same query can be used for multiple monitors without error (monitors use own cached proxy)"
|
||||
local A = world:component()
|
||||
local q = world:query(A)
|
||||
local c1, c2 = 0, 0
|
||||
ob.monitor(q).added(function() c1 += 1 end)
|
||||
ob.monitor(q).added(function() c2 += 1 end)
|
||||
local e = world:entity()
|
||||
world:add(e, A)
|
||||
CHECK(c1 == 1)
|
||||
CHECK(c2 == 1)
|
||||
end
|
||||
|
||||
do CASE [[Monitor should only report removed entities if it was previously
|
||||
apart of it]]
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
|
||||
local count = 0
|
||||
ob.monitor(world:query(A, C)).removed(function()
|
||||
count += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
jecs.bulk_insert(world, e, {A, B}, {0,0})
|
||||
CHECK(count==0)
|
||||
world:remove(e, A)
|
||||
CHECK(count==0)
|
||||
end
|
||||
|
||||
do CASE [[should not invoke monitor.added callback multiple times in a bulk_move
|
||||
]]
|
||||
local A = world:component()
|
||||
|
|
@ -609,6 +639,74 @@ TEST("modules/ob::monitor", function()
|
|||
CHECK(r==2)
|
||||
end
|
||||
|
||||
-- Without-clause pair onremoved path: guards must stay or these fail.
|
||||
-- Removing "if archetypes[dst.id]" would make this fail: we must only report added when dst matches the query.
|
||||
do CASE "without(pair): removing one excluded pair only fires added when entity actually enters (dst matches query)"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
|
||||
local added_count = 0
|
||||
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, e1), jecs.pair(B, e2)))
|
||||
monitor.added(function()
|
||||
added_count += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, jecs.pair(B, e1))
|
||||
world:add(e, jecs.pair(B, e2))
|
||||
world:add(e, A)
|
||||
CHECK(added_count == 0)
|
||||
|
||||
world:remove(e, jecs.pair(B, e1))
|
||||
CHECK(added_count == 0)
|
||||
|
||||
world:remove(e, jecs.pair(B, e2))
|
||||
CHECK(added_count == 1)
|
||||
end
|
||||
|
||||
-- Removing "if delete then return" would make this fail: must not report added when removal is due to delete.
|
||||
do CASE "without(pair): must not report added when pair is removed due to entity delete"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local e1 = world:entity()
|
||||
|
||||
local added_count = 0
|
||||
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, e1)))
|
||||
monitor.added(function()
|
||||
added_count += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, A)
|
||||
CHECK(added_count == 1)
|
||||
world:add(e, jecs.pair(B, e1))
|
||||
world:delete(e)
|
||||
CHECK(added_count == 1)
|
||||
end
|
||||
|
||||
-- Removing "if not wc and id ~= term then return" would make this fail: must only react to removal of the excluded term, not other pairs.
|
||||
do CASE "without(pair): must not report added when a different pair (same relation) is removed"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
|
||||
local added_count = 0
|
||||
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, e1)))
|
||||
monitor.added(function()
|
||||
added_count += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, A)
|
||||
CHECK(added_count == 1)
|
||||
world:add(e, jecs.pair(B, e2))
|
||||
world:remove(e, jecs.pair(B, e2))
|
||||
CHECK(added_count == 1)
|
||||
end
|
||||
|
||||
do CASE "Should enter monitor at query:without(pair(R, *)) when adding pair(R, t1)"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
|
|
@ -792,7 +890,206 @@ TEST("modules/ob::monitor", function()
|
|||
CHECK(c == 2)
|
||||
end
|
||||
|
||||
if true then return end
|
||||
do CASE "entity about to leave (remove queried pair) reports to removed exactly once"
|
||||
local A = world:component()
|
||||
local e1 = world:entity()
|
||||
|
||||
local monitor = ob.monitor(world:query(jecs.pair(A, e1)))
|
||||
local removed_count = 0
|
||||
monitor.removed(function()
|
||||
removed_count += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, jecs.pair(A, e1))
|
||||
world:remove(e, jecs.pair(A, e1))
|
||||
CHECK(removed_count == 1)
|
||||
end
|
||||
|
||||
do CASE "removing non-queried pair (same relation, other target) must not report to removed"
|
||||
local A = world:component()
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
|
||||
local monitor = ob.monitor(world:query(jecs.pair(A, e1)))
|
||||
local removed_count = 0
|
||||
monitor.removed(function()
|
||||
removed_count += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, jecs.pair(A, e2))
|
||||
world:remove(e, jecs.pair(A, e2))
|
||||
CHECK(removed_count == 0)
|
||||
end
|
||||
|
||||
do CASE "about-to-leave reports to removed exactly once per exit; second removal (already out) must not report"
|
||||
local A = world:component()
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
|
||||
local monitor = ob.monitor(world:query(jecs.pair(A, e1), jecs.pair(A, e2)))
|
||||
local removed_count = 0
|
||||
monitor.removed(function()
|
||||
removed_count += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, jecs.pair(A, e1))
|
||||
world:add(e, jecs.pair(A, e2))
|
||||
world:remove(e, jecs.pair(A, e1))
|
||||
CHECK(removed_count == 1)
|
||||
world:remove(e, jecs.pair(A, e2))
|
||||
CHECK(removed_count == 1)
|
||||
end
|
||||
|
||||
do CASE "pair term leave then re-enter gives one removed (about to leave) then one added (has entered)"
|
||||
local A = world:component()
|
||||
local e1 = world:entity()
|
||||
|
||||
local monitor = ob.monitor(world:query(jecs.pair(A, e1)))
|
||||
local added_count = 0
|
||||
local removed_count = 0
|
||||
monitor.added(function()
|
||||
added_count += 1
|
||||
end)
|
||||
monitor.removed(function()
|
||||
removed_count += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, jecs.pair(A, e1))
|
||||
world:remove(e, jecs.pair(A, e1))
|
||||
world:add(e, jecs.pair(A, e1))
|
||||
CHECK(added_count == 2)
|
||||
CHECK(removed_count == 1)
|
||||
end
|
||||
|
||||
do CASE "bulk_insert causes added exactly once (entity has entered monitor)"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
|
||||
local monitor = ob.monitor(world:query(A, B, C))
|
||||
local added_count = 0
|
||||
monitor.added(function()
|
||||
added_count += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
jecs.bulk_insert(world, e, { A, B, C }, { 1, 2, 3 })
|
||||
CHECK(added_count == 1)
|
||||
end
|
||||
|
||||
do CASE "bulk_insert of multiple entities each reports to added once (each has entered)"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
|
||||
local monitor = ob.monitor(world:query(A, B, C))
|
||||
local added_count = 0
|
||||
monitor.added(function()
|
||||
added_count += 1
|
||||
end)
|
||||
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
local e3 = world:entity()
|
||||
jecs.bulk_insert(world, e1, { A, B, C }, { 1, 2, 3 })
|
||||
jecs.bulk_insert(world, e2, { A, B, C }, { 4, 5, 6 })
|
||||
jecs.bulk_insert(world, e3, { A, B, C }, { 7, 8, 9 })
|
||||
CHECK(added_count == 3)
|
||||
end
|
||||
|
||||
do CASE "bulk_remove causes removed exactly once (entity about to leave monitor)"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
|
||||
local monitor = ob.monitor(world:query(A, B, C))
|
||||
local added_count = 0
|
||||
local removed_count = 0
|
||||
monitor.added(function()
|
||||
added_count += 1
|
||||
end)
|
||||
monitor.removed(function()
|
||||
removed_count += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
jecs.bulk_insert(world, e, { A, B, C }, { 1, 2, 3 })
|
||||
CHECK(added_count == 1)
|
||||
CHECK(removed_count == 0)
|
||||
|
||||
jecs.bulk_remove(world, e, { A, B, C })
|
||||
CHECK(removed_count == 1)
|
||||
CHECK(not world:has(e, A))
|
||||
CHECK(not world:has(e, B))
|
||||
CHECK(not world:has(e, C))
|
||||
end
|
||||
|
||||
do CASE "bulk_remove of multiple entities each reports to removed once (each about to leave)"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
|
||||
local monitor = ob.monitor(world:query(A, B, C))
|
||||
local removed_count = 0
|
||||
monitor.removed(function()
|
||||
removed_count += 1
|
||||
end)
|
||||
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
jecs.bulk_insert(world, e1, { A, B, C }, { 1, 2, 3 })
|
||||
jecs.bulk_insert(world, e2, { A, B, C }, { 4, 5, 6 })
|
||||
|
||||
jecs.bulk_remove(world, e1, { A, B, C })
|
||||
CHECK(removed_count == 1)
|
||||
jecs.bulk_remove(world, e2, { A, B, C })
|
||||
CHECK(removed_count == 2)
|
||||
end
|
||||
|
||||
do CASE "bulk_remove only of query terms still reports to removed exactly once (entity about to leave)"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
local D = world:component()
|
||||
|
||||
local monitor = ob.monitor(world:query(A, B, C))
|
||||
local removed_count = 0
|
||||
monitor.removed(function()
|
||||
removed_count += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
jecs.bulk_insert(world, e, { A, B, C, D }, { 1, 2, 3, 4 })
|
||||
jecs.bulk_remove(world, e, { A, B, C })
|
||||
CHECK(removed_count == 1)
|
||||
CHECK(world:has(e, D))
|
||||
end
|
||||
|
||||
do CASE "bulk_insert then bulk_remove gives added once (entered) and removed once (about to leave)"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
|
||||
local monitor = ob.monitor(world:query(A, B, C))
|
||||
local added_count = 0
|
||||
local removed_count = 0
|
||||
monitor.added(function()
|
||||
added_count += 1
|
||||
end)
|
||||
monitor.removed(function()
|
||||
removed_count += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
jecs.bulk_insert(world, e, { A, B, C }, { 1, 2, 3 })
|
||||
jecs.bulk_remove(world, e, { A, B, C })
|
||||
CHECK(added_count == 1)
|
||||
CHECK(removed_count == 1)
|
||||
end
|
||||
|
||||
do CASE "monitor with pair should handle remove and re-add correctly"
|
||||
local A = world:component()
|
||||
|
|
@ -888,7 +1185,6 @@ TEST("modules/ob::monitor", function()
|
|||
local B = world:component()
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
local e3 = world:entity()
|
||||
|
||||
local monitor = ob.monitor(world:query(jecs.pair(A, e1), jecs.pair(A, e2)))
|
||||
local c = 0
|
||||
|
|
@ -898,15 +1194,15 @@ TEST("modules/ob::monitor", function()
|
|||
|
||||
local e = world:entity()
|
||||
world:add(e, jecs.pair(A, e1))
|
||||
CHECK(c == 1)
|
||||
CHECK(c == 0)
|
||||
world:remove(e, jecs.pair(A, e1))
|
||||
world:add(e, jecs.pair(A, e2))
|
||||
CHECK(c == 2)
|
||||
CHECK(c == 0)
|
||||
world:remove(e, jecs.pair(A, e2))
|
||||
world:add(e, jecs.pair(A, e1))
|
||||
CHECK(c == 3)
|
||||
CHECK(c == 0)
|
||||
world:add(e, jecs.pair(A, e2))
|
||||
CHECK(c == 4)
|
||||
CHECK(c == 1)
|
||||
end
|
||||
|
||||
do CASE "monitor with pair should handle set operations correctly"
|
||||
|
|
@ -929,12 +1225,13 @@ TEST("modules/ob::monitor", function()
|
|||
CHECK(c == 1)
|
||||
world:set(e, jecs.pair(A, e1), false)
|
||||
CHECK(c == 1)
|
||||
CHECK(r == 0)
|
||||
world:set(e, jecs.pair(A, e1), true)
|
||||
CHECK(c == 2)
|
||||
CHECK(c == 1)
|
||||
world:remove(e, jecs.pair(A, e1))
|
||||
CHECK(r == 1)
|
||||
world:set(e, jecs.pair(A, e1), true)
|
||||
CHECK(c == 3)
|
||||
CHECK(c == 2)
|
||||
end
|
||||
|
||||
do CASE "monitor with pair query should handle non-matching pairs"
|
||||
|
|
|
|||
|
|
@ -2146,6 +2146,20 @@ TEST("world:query()", function()
|
|||
CHECK(not world:has(e1, B))
|
||||
end
|
||||
end
|
||||
|
||||
do CASE "query:archetypes(override) should create new archetypes list"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
|
||||
local q = world:query(A, B)
|
||||
local e = world:entity()
|
||||
world:set(e, A, false)
|
||||
world:set(e, B, true)
|
||||
|
||||
CHECK(q:archetypes() == q:archetypes())
|
||||
CHECK(q:archetypes() ~= q:archetypes(true))
|
||||
end
|
||||
|
||||
do CASE "cached"
|
||||
local world = jecs.world()
|
||||
|
|
|
|||
Loading…
Reference in a new issue