diff --git a/src/jecs.luau b/src/jecs.luau
index 3cae329..5a98984 100755
--- a/src/jecs.luau
+++ b/src/jecs.luau
@@ -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
diff --git a/test.rbxl b/test.rbxl
new file mode 100755
index 0000000..372eb6b
Binary files /dev/null and b/test.rbxl differ
diff --git a/test/tests.luau b/test/tests.luau
index 50a6b11..5cb2ab0 100755
--- a/test/tests.luau
+++ b/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"
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("")
+ print(client.entity_index.max_id)
+ print("")
+
+ print("")
+ print(client.entity_index.alive_count)
+ print("")
+
+ print("")
+ print(client.entity_index.dense_array[client.entity_index.alive_count])
+ print("")
+ print("")
local e3 = client:entity()
- CHECK(ECS_ID(e3) == 1000)
+ print("")
+ print("")
+ CHECK(ECS_ID(e3) == 1001)
+ print("")
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 "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::any) + 1)
+ print("")
+
+ 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()