mirror of
https://github.com/Ukendio/jecs.git
synced 2026-02-04 15:15:21 +00:00
Fix desired id being overriden
This commit is contained in:
parent
0d3f1bd3aa
commit
7f3946736b
3 changed files with 333 additions and 55 deletions
|
|
@ -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
BIN
test.rbxl
Executable file
Binary file not shown.
314
test/tests.luau
314
test/tests.luau
|
|
@ -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))
|
||||
|
|
@ -1702,7 +1990,7 @@ TEST("world:entity()", function()
|
|||
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()
|
||||
|
|
@ -1793,6 +2081,8 @@ TEST("world:entity()", function()
|
|||
CHECK(ECS_ID(e) == pin)
|
||||
CHECK(ECS_GENERATION(e) == 0)
|
||||
end
|
||||
|
||||
|
||||
end)
|
||||
|
||||
TEST("world:has()", function()
|
||||
|
|
|
|||
Loading…
Reference in a new issue