Remove eagerly

This commit is contained in:
Ukendio 2025-08-29 17:13:13 +02:00
parent 037035a9a1
commit 917c951d55
2 changed files with 153 additions and 73 deletions

117
jecs.luau
View file

@ -54,6 +54,8 @@ export type Query<T...> = typeof(setmetatable(
archetypes: (self: Query<T...>) -> { Archetype },
cached: (self: Query<T...>) -> Query<T...>,
ids: { Id<any> },
filter_with: { Id<any> }?,
filter_without: { Id<any> }?
-- world: World
},
{} :: {
@ -92,13 +94,14 @@ type archetype = {
}
type componentrecord = {
component: i53,
records: { [number]: number },
counts: { [i53]: number },
flags: number,
size: number,
on_add: ((entity: i53, id: i53, value: any?) -> ())?,
on_change: ((entity: i53, id: i53, value: any) -> ())?,
on_add: ((entity: i53, id: i53, value: any, oldarchetype: archetype) -> ())?,
on_change: ((entity: i53, id: i53, value: any, oldarchetype: archetype) -> ())?,
on_remove: ((entity: i53, id: i53) -> ())?,
wildcard_pairs: { [number]: componentrecord },
@ -166,9 +169,9 @@ export type World = {
observable: Map<Id, Map<Id, { Observer }>>,
added: <T>(World, Entity<T>, <e>(e: Entity<e>, id: Id<T>, value: T) -> ()) -> () -> (),
added: <T>(World, Entity<T>, <e>(e: Entity<e>, id: Id<T>, value: T, oldarchetype: Archetype) -> ()) -> () -> (),
removed: <T>(World, Entity<T>, (e: Entity, id: Id<T>) -> ()) -> () -> (),
changed: <T>(World, Entity<T>, <e>(e: Entity<e>, id: Id<T>, value: T) -> ()) -> () -> (),
changed: <T>(World, Entity<T>, <e>(e: Entity<e>, id: Id<T>, value: T, oldarchetype: Archetype) -> ()) -> () -> (),
--- Enforce a check on entities to be created within desired range
range: (self: World, range_begin: number, range_end: number?) -> (),
@ -262,8 +265,8 @@ export type ComponentRecord = {
flags: number,
size: number,
on_add: (<T>(entity: Entity, id: Entity<T>, value: T?) -> ())?,
on_change: (<T>(entity: Entity, id: Entity<T>, value: T) -> ())?,
on_add: (<T>(entity: Entity, id: Entity<T>, value: T, oldarchetype: Archetype) -> ())?,
on_change: (<T>(entity: Entity, id: Entity<T>, value: T, oldArchetype: Archetype) -> ())?,
on_remove: ((entity: Entity, id: Entity) -> ())?,
}
export type ComponentIndex = Map<Id, ComponentRecord>
@ -924,14 +927,6 @@ local function archetype_create(world: world, id_types: { i53 }, ty, prev: i53?)
idr_t.size += 1
archetype_append_to_records(idr_t, archetype_id, columns_map, t, i, column)
-- Hypothetically this should only capture leaf component records
local idr_t_wc_pairs = idr_t.wildcard_pairs
if not idr_t_wc_pairs then
idr_t_wc_pairs = {} :: {[i53]: componentrecord }
idr_t.wildcard_pairs = idr_t_wc_pairs
end
idr_t_wc_pairs[component_id] = idr
end
end
@ -2058,6 +2053,7 @@ local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values:
local dst_types = ids
local to = archetype_ensure(world, dst_types)
new_entity(entity, r, to)
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
for i, id in ids do
local value = values[i]
local cdr = component_index[id]
@ -2066,11 +2062,11 @@ local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values:
if value then
r.archetype.columns_map[id][r.row] = value
if on_add then
on_add(entity, id, value :: any)
on_add(entity, id, value, ROOT_ARCHETYPE)
end
else
if on_add then
on_add(entity, id)
on_add(entity, id, nil, ROOT_ARCHETYPE)
end
end
end
@ -2112,10 +2108,10 @@ local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values:
local on_change = idr.on_change
local hook = if set then on_change else on_add
if hook then
hook(entity, id, value :: any)
hook(entity, id, value :: any, from)
end
elseif on_add then
on_add(entity, id)
on_add(entity, id, nil, from)
end
end
end
@ -2306,7 +2302,6 @@ local function world_new()
end
local function inner_entity_move(
entity_index: entityindex,
entity: i53,
record: record,
to: archetype
@ -2380,7 +2375,7 @@ local function world_new()
-- and just set the data directly.
local on_change = idr.on_change
if on_change then
on_change(entity, id, data)
on_change(entity, id, data, src)
end
else
local to: archetype
@ -2450,7 +2445,7 @@ local function world_new()
if from then
-- If there was a previous archetype, then the entity needs to move the archetype
inner_entity_move(entity_index, entity, record, to)
inner_entity_move(entity, record, to)
else
new_entity(entity, record, to)
end
@ -2460,7 +2455,7 @@ local function world_new()
local on_add = idr.on_add
if on_add then
on_add(entity, id, data)
on_add(entity, id, data, src)
end
end
end
@ -2470,7 +2465,6 @@ local function world_new()
entity: i53,
id: i53
): ()
local entity_index = world.entity_index
local record = entity_index_try_get_unsafe(entity :: number)
if not record then
return
@ -2549,7 +2543,7 @@ local function world_new()
end
if from then
inner_entity_move(entity_index, entity, record, to)
inner_entity_move(entity, record, to)
else
if #to.types > 0 then
new_entity(entity, record, to)
@ -2559,7 +2553,7 @@ local function world_new()
local on_add = idr.on_add
if on_add then
on_add(entity, id)
on_add(entity, id, nil, src)
end
end
@ -2593,7 +2587,7 @@ local function world_new()
end
end
type Listener<T> = (e: i53, id: i53, value: T?) -> ()
type Listener<T> = (e: i53, id: i53, value: T, oldarchetype: archetype) -> ()
world.added = function<T>(_: world, component: i53, fn: Listener<T>)
local listeners = signals.added[component]
@ -2601,9 +2595,9 @@ local function world_new()
listeners = {}
signals.added[component] = listeners
local function on_add(entity, id, value)
local function on_add(entity, id, value, oldarchetype)
for _, listener in listeners :: { Listener<T> } do
listener(entity, id, value)
listener(entity, id, value, oldarchetype)
end
end
local existing_hook = world_get(world, component, EcsOnAdd) :: Listener<T>
@ -2644,9 +2638,9 @@ local function world_new()
if not listeners then
listeners = {}
signals.changed[component] = listeners
local function on_change(entity, id, value: any)
local function on_change(entity, id, value, oldarchetype)
for _, listener in listeners :: { Listener<T> } do
listener(entity, id, value)
listener(entity, id, value, oldarchetype)
end
end
local existing_hook = world_get(world, component, EcsOnChange) :: Listener<T>
@ -2686,7 +2680,7 @@ local function world_new()
listeners = {}
signals.removed[component] = listeners
local function on_remove(entity, id)
for _, listener in listeners :: { Listener<T> } do
for _, listener in listeners :: { (...any) -> () } do
listener(entity, id)
end
end
@ -2881,7 +2875,7 @@ local function world_new()
local to = archetype_traverse_remove(world, id, record.archetype)
inner_entity_move(entity_index, entity, record, to)
inner_entity_move(entity, record, to)
end
end
@ -2945,7 +2939,7 @@ local function world_new()
-- this is hypothetically not that expensive of an operation anyways
to = archetype_traverse_remove(world, entity, from)
end
inner_entity_move(entity_index, e, r, to)
inner_entity_move(e, r, to)
end
archetype_destroy(world, idr_archetype)
@ -2967,55 +2961,43 @@ local function world_new()
end
end
if idr_t then
for id, cr in idr_t.wildcard_pairs do
local flags = cr.flags
local flags_delete_mask = bit32.btest(flags, ECS_ID_DELETE)
local on_remove = cr.on_remove
if flags_delete_mask then
for archetype_id in cr.records do
local archetype_ids = idr_t.records
for archetype_id in archetype_ids do
local idr_t_archetype = archetypes[archetype_id]
local idr_t_types = idr_t_archetype.types
local entities = idr_t_archetype.entities
for _, id in idr_t_types do
if not ECS_IS_PAIR(id) then
continue
end
local object = entity_index_get_alive(
entity_index, ECS_PAIR_SECOND(id))
if object ~= entity then
continue
end
local id_record = component_index[id]
local flags = id_record.flags
local flags_delete_mask = bit32.btest(flags, ECS_ID_DELETE)
if flags_delete_mask then
for i = #entities, 1, -1 do
local child = entities[i]
world_delete(world, child)
end
end
break
else
for archetype_id in cr.records do
local idr_t_archetype = archetypes[archetype_id]
local entities = idr_t_archetype.entities
-- archetype_traverse_remove is not idempotent meaning
-- this access is actually unsafe because it can
-- incorrectly cache an edge despite a node of the
-- component id on the archetype does not exist. This
-- requires careful testing to ensure correct values are
-- being passed to the arguments.
local to = archetype_traverse_remove(world, id, idr_t_archetype)
for i = #entities, 1, -1 do
local e = entities[i]
local r = eindex_sparse_array[ECS_ID(e :: number)]
if on_remove then
on_remove(e, id)
local from = r.archetype
if from ~= idr_t_archetype then
-- unfortunately the on_remove hook allows a window where `e` can have changed archetype
-- this is hypothetically not that expensive of an operation anyways
to = archetype_traverse_remove(world, id, from)
local child = entities[i]
world_remove(world, child, id)
end
end
inner_entity_move(entity_index, e, r, to)
end
end
end
for archetype_id in cr.records do
for archetype_id in archetype_ids do
archetype_destroy(world, archetypes[archetype_id])
end
end
end
if idr_r then
local archetype_ids = idr_r.records
@ -3055,7 +3037,7 @@ local function world_new()
for i = #entities, 1, -1 do
local e = entities[i]
local r = entity_index_try_get_unsafe(e) :: record
inner_entity_move(entity_index, e, r, node)
inner_entity_move(e, r, node)
end
end
@ -3065,6 +3047,7 @@ local function world_new()
end
end
local dense = record.dense
local i_swap = entity_index.alive_count
entity_index.alive_count = i_swap - 1

View file

@ -24,6 +24,104 @@ type Id<T=unknown> = jecs.Id<T>
local entity_visualiser = require("@tools/entity_visualiser")
local dwi = entity_visualiser.stringify
SKIP()
TEST("jecs delete", function()
do CASE "delete children"
local world = jecs.world()
local Health = world:component()
local Poison = world:component()
local FriendsWith = world:component()
local e = world:entity()
world:set(e, Poison, 5)
world:set(e, Health, 50)
local children = {}
for i = 1, 10 do
local child = world:entity()
world:set(child, Poison, 9999)
world:set(child, Health, 100)
world:add(child, pair(jecs.ChildOf, e))
table.insert(children, child)
end
BENCH("delete children of entity", function()
world:delete(e)
end)
for i, child in children do
CHECK(not world:contains(child))
CHECK(not world:has(child, pair(jecs.ChildOf, e)))
CHECK(not world:has(child, Health))
end
e = world:entity()
local friends = {}
for i = 1, 10 do
local friend = world:entity()
world:set(friend, Poison, 9999)
world:set(friend, Health, 100)
world:add(friend, pair(FriendsWith, e))
for j = 1, 10 do
world:add(friend, world:component())
end
table.insert(friends, friend)
end
BENCH("remove friends of entity", function()
world:delete(e)
end)
for i, friend in friends do
CHECK(not world:has(friend, pair(FriendsWith, e)))
CHECK(world:has(friend, Health))
CHECK(world:contains(friend))
end
end
end)
TEST("pepe", function()
local world = jecs.world()
local t = world:entity()
local c = world:component()
world:add(c, t)
local component = world:component()
local lifetime = world:component()
local tag = world:entity()
local rel1 = world:entity()
local rel2 = world:entity()
local rel3 = world:entity()
local destroyed = false
world:removed(lifetime, function(e)
destroyed = true
end)
local parent = world:entity()
world:set(parent, component, "foo")
world:add(parent, jecs.pair(rel1, component))
local other1 = world:entity()
world:add(other1, tag)
world:add(other1, jecs.pair(jecs.ChildOf, parent))
world:add(other1, jecs.pair(rel1, component))
local child = world:entity()
world:set(child, lifetime, "")
world:add(child, jecs.pair(jecs.ChildOf, parent))
world:add(child, jecs.pair(rel3, parent))
world:add(child, jecs.pair(rel2, other1))
world:delete(parent)
CHECK(destroyed)
CHECK(not world:contains(child))
end)
TEST("ardi", function()
local world = jecs.world()
@ -856,7 +954,6 @@ TEST("world:delete()", function()
local world = jecs.world()
local A = world:component()
local B = world:component()
local C = world:component()
world:set(A, jecs.OnRemove, function(entity, id)
world:set(entity, B, true)
end)