Fix desired id being overriden

This commit is contained in:
Ukendio 2026-01-02 06:32:59 +01:00
parent 0d3f1bd3aa
commit 7f3946736b
3 changed files with 333 additions and 55 deletions

View file

@ -1034,8 +1034,8 @@ local function world_range(world: world, range_begin: number, range_end: number?
dense = 0 dense = 0
} :: record } :: record
end end
entity_index.max_id = range_begin - 1 entity_index.max_id = range_begin
entity_index.alive_count = range_begin - 1 entity_index.alive_count = range_begin
end end
end end
@ -3302,62 +3302,51 @@ local function world_new(DEBUG: boolean?)
if r then if r then
local dense = r.dense local dense = r.dense
if not dense or r.dense == 0 then -- If dense == 0, this is a pre-populated entry from world:range()
r.dense = index -- Just add the entity to the end of the dense array
dense = index if dense == 0 then
local e_swap = eindex_dense_array[dense]
local r_swap = entity_index_try_get_any(e_swap) :: record
r_swap.dense = dense
alive_count += 1 alive_count += 1
entity_index.alive_count = alive_count entity_index.alive_count = alive_count
r.dense = alive_count r.dense = alive_count
eindex_dense_array[dense] = e_swap
eindex_dense_array[alive_count] = entity eindex_dense_array[alive_count] = entity
return entity return entity
end end
local any = eindex_dense_array[dense] -- If dense > 0, check if there's an existing entity at that position
if any ~= entity then local existing_entity = eindex_dense_array[dense]
if alive_count <= dense then if existing_entity and existing_entity ~= entity then
local e_swap = eindex_dense_array[dense]
local r_swap = entity_index_try_get_any(e_swap) :: record
r_swap.dense = dense
alive_count += 1 alive_count += 1
entity_index.alive_count = alive_count entity_index.alive_count = alive_count
r.dense = alive_count r.dense = alive_count
eindex_dense_array[dense] = e_swap
eindex_dense_array[alive_count] = entity eindex_dense_array[alive_count] = entity
end return entity
end end
return entity return entity
else else
for i = entity_index.max_id + 1, index do local max_id = entity_index.max_id
eindex_sparse_array[i] = { dense = i } :: record
eindex_dense_array[i] = i if index > max_id then
-- Pre-populate all intermediate IDs to keep sparse_array as an array
for i = max_id + 1, index - 1 do
if not eindex_sparse_array[i] then
-- NOTE(marcus): We have to do this check to see if
-- they exist first because world:range() may have
-- pre-populated some slots already.
end
eindex_sparse_array[i] = { dense = 0 } :: record
end end
entity_index.max_id = index entity_index.max_id = index
end
local e_swap = eindex_dense_array[alive_count]
local r_swap = eindex_sparse_array[alive_count]
r_swap.dense = index
alive_count += 1 alive_count += 1
entity_index.alive_count = alive_count entity_index.alive_count = alive_count
r = eindex_sparse_array[index]
r.dense = alive_count
eindex_sparse_array[index] = r
eindex_dense_array[index] = e_swap
eindex_dense_array[alive_count] = entity eindex_dense_array[alive_count] = entity
r = { dense = alive_count } :: record
eindex_sparse_array[index] = r
return entity return entity
end end
end end
@ -3390,7 +3379,6 @@ local function world_new(DEBUG: boolean?)
end end
local function world_delete(world: world, entity: i53) local function world_delete(world: world, entity: i53)
DEBUG_DELETING_ENTITY = entity
local record = entity_index_try_get_unsafe(entity) local record = entity_index_try_get_unsafe(entity)
if not record then if not record then
return return
@ -3744,7 +3732,7 @@ local function world_new(DEBUG: boolean?)
local function world_delete_checked(world: world, entity: i53) local function world_delete_checked(world: world, entity: i53)
DEBUG_DELETING_ENTITY = entity DEBUG_DELETING_ENTITY = entity
DEBUG_IS_INVALID_ENTITY(entity) DEBUG_IS_INVALID_ENTITY(entity)
canonical_world_delete(world, entity, id) canonical_world_delete(world, entity)
DEBUG_DELETING_ENTITY = nil DEBUG_DELETING_ENTITY = nil
end end
world_delete = world_delete_checked world_delete = world_delete_checked

BIN
test.rbxl Executable file

Binary file not shown.

View file

@ -77,7 +77,7 @@ TEST("reproduce idr_t nil archetype bug", function()
-- Removing this, but keeping Animator on the other 2 causes it to stop happening -- Removing this, but keeping Animator on the other 2 causes it to stop happening
world:set(trackEntity1, cts.VelocitizeAnimationWeight, 0) world:set(trackEntity1, cts.VelocitizeAnimationWeight, 0)
local q = world:query(jecs.pair(jecs.ChildOf, jecs.w)):cached() local q = world:query(jecs.pair(jecs.ChildOf, __)):cached()
world:delete(root) world:delete(root)
@ -88,7 +88,7 @@ TEST("reproduce idr_t nil archetype bug", function()
print(`bugged entity found: {entityId}`) print(`bugged entity found: {entityId}`)
print(`original parent: {info.parentId}`) print(`original parent: {info.parentId}`)
print(`current parent: {i}`) print(`current parent: {i}`)
print(`batch = {batch}, i = {i}`) print(`batch = {batchSize}, i = {i}`)
print("==========================================") print("==========================================")
trackedEntities[entityId] = nil trackedEntities[entityId] = nil
end end
@ -917,9 +917,6 @@ TEST("world:delete()", function()
CHECK(destroyed) CHECK(destroyed)
CHECK(not world:contains(child)) CHECK(not world:contains(child))
end end
if true then
return
end
do CASE "Should delete children in different archetypes if they have the same parent" do CASE "Should delete children in different archetypes if they have the same parent"
local world = jecs.world() local world = jecs.world()
@ -1536,12 +1533,10 @@ TEST("world:range()", function()
do CASE "spawn entity under min range" do CASE "spawn entity under min range"
local world = jecs.world() local world = jecs.world()
world:range(400, 1000) world:range(400, 1000)
CHECK(world.entity_index.alive_count == 399)
local e = world:entity(300) local e = world:entity(300)
CHECK(world.entity_index.alive_count == 400)
local e1 = world:entity(300) local e1 = world:entity(300)
CHECK(world.entity_index.alive_count == 400) CHECK(world:contains(e))
CHECK(e) CHECK(world:contains(e1))
end end
do CASE "entity ID reuse works correctly across different world ranges" do CASE "entity ID reuse works correctly across different world ranges"
local base = jecs.world() local base = jecs.world()
@ -1588,6 +1583,7 @@ TEST("world:range()", function()
CHECK(bar == bar_mirror) CHECK(bar == bar_mirror)
end end
print("<do CASE \"delete outside partitioned range\">")
do CASE "delete outside partitioned range" do CASE "delete outside partitioned range"
local server = jecs.world() local server = jecs.world()
local client = jecs.world() local client = jecs.world()
@ -1605,8 +1601,25 @@ TEST("world:range()", function()
CHECK(client:get(e2, A)) CHECK(client:get(e2, A))
client:delete(e2) client:delete(e2)
print("<client.entity_index.max_id>")
print(client.entity_index.max_id)
print("</client.entity_index.max_id>")
print("<client.entity_index.alive_count>")
print(client.entity_index.alive_count)
print("</client.entity_index.alive_count>")
print("</client.entity_index.dense_array[client.entity_index.alive_count]>")
print(client.entity_index.dense_array[client.entity_index.alive_count])
print("</client.entity_index.dense_array[client.entity_index.alive_count]>")
print("<client:entity()>")
local e3 = client:entity() local e3 = client:entity()
CHECK(ECS_ID(e3) == 1000) print("</client:entity()>")
print("<ECS_ID(e3) == 1001>")
CHECK(ECS_ID(e3) == 1001)
print("</ECS_ID(e3) == 1001>")
local e1v1 = server:entity() local e1v1 = server:entity()
local e4 = client:entity(e1v1) local e4 = client:entity(e1v1)
@ -1616,6 +1629,281 @@ TEST("world:range()", function()
CHECK(client:contains(e4)) CHECK(client:contains(e4))
end end
print("</do CASE \"delete outside partitioned range\">")
do CASE "desired id within range does not overwrite existing entities"
local world = jecs.world()
world:range(1000, 2000)
local ctype = world:component()
local e1 = world:entity(1000)
local e2 = world:entity(1001)
local e3 = world:entity(1002)
world:set(e1, ctype, "entity1")
world:set(e2, ctype, "entity2")
world:set(e3, ctype, "entity3")
local e4 = world:entity(1500)
world:set(e4, ctype, "entity4")
CHECK(world:contains(e1))
CHECK(world:contains(e2))
CHECK(world:contains(e3))
CHECK(world:contains(e4))
CHECK(world:get(e1, ctype) == "entity1")
CHECK(world:get(e2, ctype) == "entity2")
CHECK(world:get(e3, ctype) == "entity3")
CHECK(world:get(e4, ctype) == "entity4")
end
do CASE "creating entity with range+offset does not conflict with existing"
local world = jecs.world()
world:range(500, 1000)
local ctype = world:component()
local base = world:entity(500)
world:set(base, ctype, "base")
local offset1 = world:entity(501)
world:set(offset1, ctype, "offset1")
local offset2 = world:entity(750)
world:set(offset2, ctype, "offset2")
CHECK(world:contains(base))
CHECK(world:contains(offset1))
CHECK(world:contains(offset2))
CHECK(world:get(base, ctype) == "base")
CHECK(world:get(offset1, ctype) == "offset1")
CHECK(world:get(offset2, ctype) == "offset2")
end
do CASE "creating entities in reverse order within range"
local world = jecs.world()
world:range(100, 200)
local ctype = world:component()
local e3 = world:entity(150)
local e2 = world:entity(120)
local e1 = world:entity(110)
world:set(e1, ctype, 1)
world:set(e2, ctype, 2)
world:set(e3, ctype, 3)
-- All should exist independently
CHECK(world:contains(e1))
CHECK(world:contains(e2))
CHECK(world:contains(e3))
CHECK(world:get(e1, ctype) == 1)
CHECK(world:get(e2, ctype) == 2)
CHECK(world:get(e3, ctype) == 3)
end
do CASE "creating entity with desired ID after range pre-population"
local world = jecs.world()
world:range(400, 1000)
local ctype = world:component()
-- Range pre-populates 1-399 with dense=0
-- Create an entity at a pre-populated position
local e1 = world:entity(300)
world:set(e1, ctype, "prepop")
-- Create another entity at a different pre-populated position
local e2 = world:entity(250)
world:set(e2, ctype, "prepop2")
-- Create entity within the range
local e3 = world:entity(500)
world:set(e3, ctype, "inrange")
-- All should work correctly
CHECK(world:contains(e1))
CHECK(world:contains(e2))
CHECK(world:contains(e3))
CHECK(world:get(e1, ctype) == "prepop")
CHECK(world:get(e2, ctype) == "prepop2")
CHECK(world:get(e3, ctype) == "inrange")
end
do CASE "creating same entity twice returns existing entity"
local world = jecs.world()
world:range(1000, 2000)
local ctype = world:component()
-- Create entity
local e1 = world:entity(1500)
world:set(e1, ctype, "original")
-- Try to create it again
local e2 = world:entity(1500)
-- Should return the same entity
CHECK(e1 == e2)
CHECK(world:get(e1, ctype) == "original")
CHECK(world:get(e2, ctype) == "original")
-- Should only be one entity
local count = 0
for _ in world:query(ctype) do
count += 1
end
CHECK(count == 1)
end
do CASE "creating entities with gaps in range does not affect existing"
local world = jecs.world()
world:range(200, 300)
local ctype = world:component()
-- Create entities with gaps
local e1 = world:entity(200)
local e2 = world:entity(250)
local e3 = world:entity(299)
world:set(e1, ctype, 1)
world:set(e2, ctype, 2)
world:set(e3, ctype, 3)
-- Create entity in the gap
local e4 = world:entity(225)
world:set(e4, ctype, 4)
-- All should still exist
CHECK(world:contains(e1))
CHECK(world:contains(e2))
CHECK(world:contains(e3))
CHECK(world:contains(e4))
CHECK(world:get(e1, ctype) == 1)
CHECK(world:get(e2, ctype) == 2)
CHECK(world:get(e3, ctype) == 3)
CHECK(world:get(e4, ctype) == 4)
end
do CASE "creating many entities with desired IDs in range"
local world = jecs.world()
world:range(1000, 2000)
local ctype = world:component()
local entities = {}
-- Create 50 entities with specific IDs
for i = 1, 50 do
local id = 1000 + (i * 10)
local e = world:entity(id)
world:set(e, ctype, i)
entities[i] = e
end
-- Verify all exist and have correct values
for i = 1, 50 do
CHECK(world:contains(entities[i]))
CHECK(world:get(entities[i], ctype) == i)
end
-- Create more entities in between
for i = 1, 49 do
local id = 1000 + (i * 10) + 5
local e = world:entity(id)
world:set(e, ctype, i + 100)
end
-- Original entities should still be intact
for i = 1, 50 do
CHECK(world:contains(entities[i]))
CHECK(world:get(entities[i], ctype) == i)
end
end
do CASE "sparse_array remains optimized with non-contiguous entity IDs"
local world = jecs.world()
local ctype = world:component()
-- Create entities with large gaps to test sparse_array optimization
-- Start with entity 100, then jump to 1000 (gap: 101-999)
local e1 = world:entity(100)
local e2 = world:entity(1000)
-- Then jump to 5000 (gap: 1001-4999)
local e3 = world:entity(5000)
-- Then jump to 10000 (gap: 5001-9999)
local e4 = world:entity(10000)
world:set(e1, ctype, 1)
world:set(e2, ctype, 2)
world:set(e3, ctype, 3)
world:set(e4, ctype, 4)
-- Verify all entities exist and have correct values
CHECK(world:contains(e1))
CHECK(world:contains(e2))
CHECK(world:contains(e3))
CHECK(world:contains(e4))
CHECK(world:get(e1, ctype) == 1)
CHECK(world:get(e2, ctype) == 2)
CHECK(world:get(e3, ctype) == 3)
CHECK(world:get(e4, ctype) == 4)
-- Verify intermediate IDs are pre-populated (they should exist but not be alive)
-- These are in the gaps between created entities
local sparse_array = world.entity_index.sparse_array
CHECK(sparse_array[500] ~= nil) -- Between 100 and 1000, should be pre-populated
CHECK(sparse_array[500].dense == 0) -- But not alive (dense=0)
CHECK(sparse_array[2500] ~= nil) -- Between 1000 and 5000, should be pre-populated
CHECK(sparse_array[2500].dense == 0)
CHECK(sparse_array[7500] ~= nil) -- Between 5000 and 10000, should be pre-populated
CHECK(sparse_array[7500].dense == 0)
-- Verify max_id was updated correctly
CHECK(world.entity_index.max_id == 10000)
end
do CASE "desired id does not overwrite old entity id"
local world = jecs.world()
local ctype = world:component()
local id = world:entity()
print("<local e = world:entity(id + 1)>")
local e = world:entity((id::any) + 1)
print("</local e = world:entity(id + 1)>")
CHECK(world:contains(id))
CHECK(world:contains(e))
-- also make sure that they don't share the same record
world:set(id, ctype, 1)
world:set(e, ctype, 2)
CHECK(world:get(id, ctype) == 1)
CHECK(world:get(e, ctype) == 2)
end
do CASE "creating ids with a higher key first"
local world = jecs.world()
local ctype = world:component()
local e = world:entity(1000)
local id = world:entity(999)
CHECK(world:contains(id))
CHECK(world:contains(e))
-- also make sure that they don't share the same record
world:set(id, ctype, 1)
world:set(e, ctype, 2)
CHECK(world:get(id, ctype) == 1)
CHECK(world:get(e, ctype) == 2)
end
do CASE "under range start" do CASE "under range start"
local world = jecs.world() local world = jecs.world()
@ -1689,7 +1977,7 @@ TEST("world:entity()", function()
local ctype = world:component() local ctype = world:component()
local id = world:entity() local id = world:entity()
local e = world:entity(id + 1) local e = world:entity((id::any) + 1)
CHECK(world:contains(id)) CHECK(world:contains(id))
CHECK(world:contains(e)) CHECK(world:contains(e))
@ -1793,6 +2081,8 @@ TEST("world:entity()", function()
CHECK(ECS_ID(e) == pin) CHECK(ECS_ID(e) == pin)
CHECK(ECS_GENERATION(e) == 0) CHECK(ECS_GENERATION(e) == 0)
end end
end) end)
TEST("world:has()", function() TEST("world:has()", function()