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
} :: record
end
entity_index.max_id = range_begin - 1
entity_index.alive_count = range_begin - 1
entity_index.max_id = range_begin
entity_index.alive_count = range_begin
end
end
@ -3302,62 +3302,51 @@ local function world_new(DEBUG: boolean?)
if r then
local dense = r.dense
if not dense or r.dense == 0 then
r.dense = index
dense = index
local e_swap = eindex_dense_array[dense]
local r_swap = entity_index_try_get_any(e_swap) :: record
r_swap.dense = dense
-- If dense == 0, this is a pre-populated entry from world:range()
-- Just add the entity to the end of the dense array
if dense == 0 then
alive_count += 1
entity_index.alive_count = alive_count
r.dense = alive_count
eindex_dense_array[dense] = e_swap
eindex_dense_array[alive_count] = entity
return entity
end
local any = eindex_dense_array[dense]
if any ~= entity then
if alive_count <= dense 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
entity_index.alive_count = alive_count
r.dense = alive_count
eindex_dense_array[dense] = e_swap
eindex_dense_array[alive_count] = entity
end
-- If dense > 0, check if there's an existing entity at that position
local existing_entity = eindex_dense_array[dense]
if existing_entity and existing_entity ~= entity then
alive_count += 1
entity_index.alive_count = alive_count
r.dense = alive_count
eindex_dense_array[alive_count] = entity
return entity
end
return entity
else
for i = entity_index.max_id + 1, index do
eindex_sparse_array[i] = { dense = i } :: record
eindex_dense_array[i] = i
end
entity_index.max_id = index
local max_id = entity_index.max_id
local e_swap = eindex_dense_array[alive_count]
local r_swap = eindex_sparse_array[alive_count]
r_swap.dense = index
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
entity_index.max_id = index
end
alive_count += 1
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
r = { dense = alive_count } :: record
eindex_sparse_array[index] = r
return entity
end
end
@ -3390,7 +3379,6 @@ local function world_new(DEBUG: boolean?)
end
local function world_delete(world: world, entity: i53)
DEBUG_DELETING_ENTITY = entity
local record = entity_index_try_get_unsafe(entity)
if not record then
return
@ -3744,7 +3732,7 @@ local function world_new(DEBUG: boolean?)
local function world_delete_checked(world: world, entity: i53)
DEBUG_DELETING_ENTITY = entity
DEBUG_IS_INVALID_ENTITY(entity)
canonical_world_delete(world, entity, id)
canonical_world_delete(world, entity)
DEBUG_DELETING_ENTITY = nil
end
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
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)
@ -88,7 +88,7 @@ TEST("reproduce idr_t nil archetype bug", function()
print(`bugged entity found: {entityId}`)
print(`original parent: {info.parentId}`)
print(`current parent: {i}`)
print(`batch = {batch}, i = {i}`)
print(`batch = {batchSize}, i = {i}`)
print("==========================================")
trackedEntities[entityId] = nil
end
@ -917,9 +917,6 @@ TEST("world:delete()", function()
CHECK(destroyed)
CHECK(not world:contains(child))
end
if true then
return
end
do CASE "Should delete children in different archetypes if they have the same parent"
local world = jecs.world()
@ -1536,12 +1533,10 @@ TEST("world:range()", function()
do CASE "spawn entity under min range"
local world = jecs.world()
world:range(400, 1000)
CHECK(world.entity_index.alive_count == 399)
local e = world:entity(300)
CHECK(world.entity_index.alive_count == 400)
local e1 = world:entity(300)
CHECK(world.entity_index.alive_count == 400)
CHECK(e)
CHECK(world:contains(e))
CHECK(world:contains(e1))
end
do CASE "entity ID reuse works correctly across different world ranges"
local base = jecs.world()
@ -1588,6 +1583,7 @@ TEST("world:range()", function()
CHECK(bar == bar_mirror)
end
print("<do CASE \"delete outside partitioned range\">")
do CASE "delete outside partitioned range"
local server = jecs.world()
local client = jecs.world()
@ -1605,8 +1601,25 @@ TEST("world:range()", function()
CHECK(client:get(e2, A))
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()
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 e4 = client:entity(e1v1)
@ -1616,6 +1629,281 @@ TEST("world:range()", function()
CHECK(client:contains(e4))
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"
local world = jecs.world()
@ -1689,7 +1977,7 @@ TEST("world:entity()", function()
local ctype = world:component()
local id = world:entity()
local e = world:entity(id + 1)
local e = world:entity((id::any) + 1)
CHECK(world:contains(id))
CHECK(world:contains(e))
@ -1793,6 +2081,8 @@ TEST("world:entity()", function()
CHECK(ECS_ID(e) == pin)
CHECK(ECS_GENERATION(e) == 0)
end
end)
TEST("world:has()", function()