From 4db44476a978c86e1be5bc82c286b3aa4123741d Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 9 Dec 2025 20:34:05 +0100 Subject: [PATCH] Only delete archetypes when completely invalidated --- src/jecs.luau | 29 ++++++++++----------- test/tests.luau | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/jecs.luau b/src/jecs.luau index 4e4b266..25dec1c 100755 --- a/src/jecs.luau +++ b/src/jecs.luau @@ -3084,17 +3084,6 @@ local function world_new() local archetype = record.archetype - if archetype then - for _, id in archetype.types do - local idr = component_index[id] - local on_remove = idr.on_remove - if on_remove then - on_remove(entity, id, true) - end - end - archetype_delete(world, record.archetype, record.row) - end - local component_index = world.component_index local archetypes = world.archetypes local tgt = ECS_PAIR(EcsWildcard, entity) @@ -3163,6 +3152,7 @@ local function world_new() local idr_t_archetype = archetypes[archetype_id] local idr_t_types = idr_t_archetype.types local entities = idr_t_archetype.entities + local will_delete_archetype = false for _, id in idr_t_types do if not ECS_IS_PAIR(id) then @@ -3173,6 +3163,7 @@ local function world_new() if object ~= entity then continue end + will_delete_archetype = true local id_record = component_index[id] local flags = id_record.flags local flags_delete_mask = bit32.btest(flags, ECS_ID_DELETE) @@ -3197,10 +3188,10 @@ local function world_new() end end end - end - for archetype_id in archetype_ids do - archetype_destroy(world, archetypes[archetype_id]) + if will_delete_archetype then + archetype_destroy(world, idr_t_archetype) + end end end @@ -3252,6 +3243,16 @@ local function world_new() end end + if archetype then + for _, id in archetype.types do + local cr = component_index[id] + local on_remove = cr.on_remove + if on_remove then + on_remove(entity, id, true) + end + end + archetype_delete(world, record.archetype, record.row) + end local dense = record.dense local i_swap = entity_index.alive_count diff --git a/test/tests.luau b/test/tests.luau index a552b93..13d8c60 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -24,6 +24,73 @@ type Id = jecs.Id local entity_visualiser = require("@modules/entity_visualiser") local dwi = entity_visualiser.stringify +FOCUS() +TEST("reproduce idr_t nil archetype bug", function() + local world = jecs.world() + + local cts = { + Humanoid = world:component(), + Animator = world:component(), + VelocitizeAnimationWeight = world:component(), + } + + local char = world:entity() + + -- REMOVING ONE OF THESE THESE OFFSETS i BY +1 + world:set(char, cts.Humanoid, 0) + world:set(char, cts.Animator, 0) + -- + + world:added(cts.Humanoid, function() end) -- REMOVING THIS OFFSETS i BY +1 TOO + world:removed(cts.Animator, function(entity, id) + local r = jecs.record(world, entity) + local src = r.archetype + + --REMOVING THIS jecs.archetype_traverse_remove CALL STOPS IT FROM HAPPENING + local dst = src and jecs.archetype_traverse_remove(world, id, src) + end) + + local batches = 10 + local batchSize = 20 + + local trackedEntities: { [number]: { parentId: number? } } = {} + + for batch = 1, batches do + for i = 1, batchSize do + local root = world:entity() + world:add(root, jecs.pair(jecs.ChildOf, char)) + + -- Removing animator from trackEntity1 causes it to stop happening + local trackEntity1 = world:entity() + world:set(trackEntity1, cts.Animator, 0) + world:add(trackEntity1, jecs.pair(jecs.ChildOf, root)) + trackedEntities[trackEntity1] = { parentId = root } + + -- Removing animator from trackEntity2 causes it to happen less frequently + local trackEntity2 = world:entity() + world:set(trackEntity2, cts.Animator, 0) + world:add(trackEntity2, jecs.pair(jecs.ChildOf, root)) + trackedEntities[trackEntity2] = { parentId = root } + + -- Removing this, but keeping Animator on the other 2 causes it to stop happening + world:set(trackEntity1, cts.VelocitizeAnimationWeight, 0) + + world:delete(root) + + for entityId, info in trackedEntities do + if world:contains(entityId) and not world:parent(entityId :: any) then + print(`bugged entity found: {entityId}`) + print(`original parent: {info.parentId}`) + print(`batch = {batch}, i = {i}`) + print("==========================================") + trackedEntities[entityId] = nil + world:deletee(entityId) + end + end + end + end +end) + TEST("Ensure archetype edges get cleaned", function() local A = jecs.component() local B = jecs.component()