From 917c951d55282709b307724214e8ae917ea71659 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 29 Aug 2025 17:13:13 +0200 Subject: [PATCH] Remove eagerly --- jecs.luau | 127 +++++++++++++++++++++--------------------------- test/tests.luau | 99 ++++++++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 73 deletions(-) diff --git a/jecs.luau b/jecs.luau index 5bfce15..94c2e35 100755 --- a/jecs.luau +++ b/jecs.luau @@ -54,6 +54,8 @@ export type Query = typeof(setmetatable( archetypes: (self: Query) -> { Archetype }, cached: (self: Query) -> Query, ids: { Id }, + filter_with: { Id }?, + filter_without: { Id }? -- 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>, - added: (World, Entity, (e: Entity, id: Id, value: T) -> ()) -> () -> (), + added: (World, Entity, (e: Entity, id: Id, value: T, oldarchetype: Archetype) -> ()) -> () -> (), removed: (World, Entity, (e: Entity, id: Id) -> ()) -> () -> (), - changed: (World, Entity, (e: Entity, id: Id, value: T) -> ()) -> () -> (), + changed: (World, Entity, (e: Entity, id: Id, 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: ((entity: Entity, id: Entity, value: T?) -> ())?, - on_change: ((entity: Entity, id: Entity, value: T) -> ())?, + on_add: ((entity: Entity, id: Entity, value: T, oldarchetype: Archetype) -> ())?, + on_change: ((entity: Entity, id: Entity, value: T, oldArchetype: Archetype) -> ())?, on_remove: ((entity: Entity, id: Entity) -> ())?, } export type ComponentIndex = Map @@ -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 = (e: i53, id: i53, value: T?) -> () + type Listener = (e: i53, id: i53, value: T, oldarchetype: archetype) -> () world.added = function(_: world, component: i53, fn: Listener) 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 } do - listener(entity, id, value) + listener(entity, id, value, oldarchetype) end end local existing_hook = world_get(world, component, EcsOnAdd) :: Listener @@ -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 } do - listener(entity, id, value) + listener(entity, id, value, oldarchetype) end end local existing_hook = world_get(world, component, EcsOnChange) :: Listener @@ -2686,7 +2680,7 @@ local function world_new() listeners = {} signals.removed[component] = listeners local function on_remove(entity, id) - for _, listener in listeners :: { Listener } 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,53 +2961,41 @@ 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 idr_t_archetype = archetypes[archetype_id] - local entities = idr_t_archetype.entities + 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 - 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) - + break + else 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) - end - end - - inner_entity_move(entity_index, e, r, to) + local child = entities[i] + world_remove(world, child, id) end end end + end - for archetype_id in cr.records do - archetype_destroy(world, archetypes[archetype_id]) - end + for archetype_id in archetype_ids do + archetype_destroy(world, archetypes[archetype_id]) end end @@ -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 diff --git a/test/tests.luau b/test/tests.luau index bced58d..0fe0de2 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -24,6 +24,104 @@ type Id = jecs.Id 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)