Merge branch 'main' into docs/studio

This commit is contained in:
Marcus 2026-01-02 06:40:43 +01:00 committed by GitHub
commit fb178980a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1426 additions and 812 deletions

View file

@ -16,8 +16,8 @@ local jecs = require("@jecs")
--[[
There are two ways to create tags:
1. Using jecs.tag() - preregister a tag entity which will be allocated when you create the world.
2. Using world:entity() - creates a regular entity id
1. Using jecs.tag() to preregister a tag entity which will be allocated when you create the world.
2. Using world:entity() to create a regular entity id
The first method is the "proper" way to create tags but it hinges upon that
you do remember to create the world after declaring all of your

View file

@ -79,6 +79,6 @@ end
-- Cached query (faster for repeated use)
local cached_query = world:query(Position, Velocity):cached()
for entity, pos, vel in cached_query do
-- Process entities - this is faster for repeated iterations
-- Process entities. This is faster for repeated iterations.
end

View file

@ -17,8 +17,9 @@ world:set(Transform, jecs.OnAdd, function(entity, id, data)
print(`Transform added to entity {entity}`)
end)
world:set(Transform, jecs.OnRemove, function(entity, id)
world:set(Transform, jecs.OnRemove, function(entity, id, delete)
-- A transform component id has been removed from entity
-- delete is true if the entity is being deleted, false/nil otherwise
print(`Transform removed from entity {entity}`)
end)
@ -38,3 +39,66 @@ end)
When an entity graph contains cycles, order is undefined. This includes cycles
that can be formed using different relationships.
]]
--[[
Structural changes in OnRemove hooks
You can call world:add, world:remove, and world:set inside OnRemove hooks.
That's fine. But there's a catch.
When an entity is being deleted, all of its components get removed. Each
removal triggers the OnRemove hook. If you try to make structural changes
to the entity during deletion, like removing more components or adding new
ones, you're fighting against the deletion process itself. The entity is
going to lose all its components anyway, so what's the point?
This creates a conflict. On one hand, you might want to clean up related
components when a specific component is removed. On the other hand, during
deletion, you don't want to do that because the entity is already being
torn down. So you need a way to tell the difference.
The solution is the delete boolean. Every OnRemove hook receives it as the
third parameter. It's true when the entity is being deleted, and false
(or nil) when you're just removing a single component normally.
So you check it. If delete is true, you bail out early. If it's false,
you do your cleanup. Simple.
Here's what it looks like in practice:
]]
local Health = world:component()
local Dead = world:component()
world:set(Health, jecs.OnRemove, function(entity, id, delete)
if delete then
-- Entity is being deleted, don't try to clean up
return
end
-- Normal removal, do cleanup
world:remove(entity, Dead)
end)
--[[
The ob.luau module uses this pattern extensively. When you're building
observers or monitors that track component removals, you need to distinguish
between "component removed" and "entity deleted" because they mean different
things for your tracking logic.
Now, about the DEBUG flag. If you create a world with DEBUG enabled:
local world = jecs.world(true)
Then the world will actively prevent you from calling world:add, world:remove,
or world:set inside OnRemove hooks when delete is true. It throws an error
that tells you exactly what went wrong. This is useful during development
to catch cases where you forgot to check the delete flag.
But here's the important part: even with DEBUG enabled, you're still allowed
to call these functions when delete is false. The DEBUG mode only prevents
structural changes during deletion, not during normal component removal.
So the pattern is always the same: check delete, bail if true, proceed if false.
The DEBUG flag just makes sure you don't forget to do the check.
]]

View file

@ -83,6 +83,9 @@ local function observers_new(
local tgt = jecs.ECS_PAIR_SECOND(term)
local wc = tgt == jecs.w
local onremoved = world:removed(rel, function(entity, id, delete: boolean?)
if delete then
return
end
if not wc and id ~= term then
return
end
@ -98,7 +101,10 @@ local function observers_new(
table.insert(cleanup, onremoved)
else
local onremoved = world:removed(term, function(entity, id)
local onremoved = world:removed(term, function(entity, id, delete: boolean?)
if delete then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then

View file

@ -1,5 +1,5 @@
[tools]
wally = "upliftgames/wally@0.3.2"
rojo = "rojo-rbx/rojo@7.4.4"
luau = "luau-lang/luau@0.701"
rojo = "rojo-rbx/rojo@7.7.0-rc.1"
luau = "luau-lang/luau@0.703.0"
zune = "scythe-technology/zune@0.5.1"

2
src/jecs.d.ts vendored
View file

@ -121,7 +121,7 @@ export class World {
/**
* Creates a new World.
*/
private constructor();
private constructor(DEBUG?: boolean);
/**
* Enforces a check for entities to be created within a desired range.

File diff suppressed because it is too large Load diff

BIN
test.rbxl Executable file

Binary file not shown.

View file

@ -12,16 +12,16 @@
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"Lib": {
"$path": "../src/jecs.luau"
"$path": "../../src/jecs.luau"
},
"benches": {
"$path": "benches"
"$path": "visual"
},
"mirror": {
"$path": "mirror.luau"
"$path": "../../src/mirror.luau"
},
"DevPackages": {
"$path": "benches/visual/DevPackages"
"$path": "visual/DevPackages"
}
}
}

View file

@ -27,48 +27,11 @@ do
G: jecs.Id,
H: jecs.Id
)
BENCH("1 component", function()
for _ in world:query(A) do
end
end)
BENCH("2 component", function()
for _ in world:query(B, A) do
end
end)
BENCH("4 component", function()
for _ in world:query(D, C, B, A) do
end
end)
BENCH("8 component", function()
for _ in world:query(H, G, F, E, D, C, B, A) do
end
end)
local e = world:entity()
world:set(e, A, true)
world:set(e, B, true)
world:set(e, C, true)
world:set(e, D, true)
world:set(e, E, true)
world:set(e, F, true)
world:set(e, G, true)
world:set(e, H, true)
BENCH("Update Data", function()
for _ = 1, 100 do
world:set(e, A, false)
world:set(e, B, false)
world:set(e, C, false)
world:set(e, D, false)
world:set(e, E, false)
world:set(e, F, false)
world:set(e, G, false)
world:set(e, H, false)
end
end)
end
local D1 = ecs:component()
@ -86,8 +49,9 @@ do
local added = 0
local archetypes = {}
for i = 1, 2 ^ 16 - 2 do
for i = 1, 2 ^ 12 - 2 do
local entity = ecs:entity()
ecs:add(entity, entity)
local combination = ""
@ -153,48 +117,10 @@ do
G: jecs.Id,
H: jecs.Id
)
BENCH("1 component", function()
for _ in world:query(A) do
end
end)
BENCH("2 component", function()
for _ in world:query(B, A) do
end
end)
BENCH("4 component", function()
for _ in world:query(D, C, B, A) do
end
end)
BENCH("8 component", function()
for _ in world:query(H, G, F, E, D, C, B, A) do
end
end)
local e = world:entity()
world:set(e, A, true)
world:set(e, B, true)
world:set(e, C, true)
world:set(e, D, true)
world:set(e, E, true)
world:set(e, F, true)
world:set(e, G, true)
world:set(e, H, true)
BENCH("Update Data", function()
for _ = 1, 100 do
world:set(e, A, false)
world:set(e, B, false)
world:set(e, C, false)
world:set(e, D, false)
world:set(e, E, false)
world:set(e, F, false)
world:set(e, G, false)
world:set(e, H, false)
end
end)
end
local D1 = ecs:component()
@ -207,13 +133,14 @@ do
local D8 = ecs:component()
local function flip()
return math.random() >= 0.15
return math.random() >= 0.5
end
local added = 0
local archetypes = {}
for i = 1, 2 ^ 16 - 2 do
for i = 1, 2 ^ 12 - 2 do
local entity = ecs:entity()
ecs:add(entity, entity)
local combination = ""
@ -246,11 +173,9 @@ do
ecs:set(entity, D8, { value = true })
end
if #combination == 7 then
added += 1
if flip() then
ecs:set(entity, D1, { value = true })
end
archetypes[combination] = true
end
local a = 0

View file

@ -50,85 +50,54 @@ local E8 = mcs:component()
local registry2 = ecr.registry()
local function flip()
return math.random() >= 0.25
return math.random() >= 0.5
end
local N = 2 ^ 16 - 2
local N = 2 ^ 12- 2
local archetypes = {}
local hm = 0
for i = 1, N do
local id = registry2.create()
local combination = ""
local n = newWorld:spawn()
local entity = ecs:entity()
local m = mcs:entity()
if flip() then
ecs:add(entity, entity)
mcs:add(m, m)
end
if flip() then
registry2:set(id, B1, { value = true })
ecs:set(entity, D1, { value = true })
newWorld:insert(n, A1({ value = true }))
mcs:set(m, E1, { value = 2 })
ecs:set(entity, D1, {value = true})
end
if flip() then
combination ..= "B"
registry2:set(id, B2, { value = true })
ecs:set(entity, D2, { value = true })
mcs:set(m, E2, { value = 2 })
newWorld:insert(n, A2({ value = true }))
end
if flip() then
combination ..= "C"
registry2:set(id, B3, { value = true })
ecs:set(entity, D3, { value = true })
mcs:set(m, E3, { value = 2 })
newWorld:insert(n, A3({ value = true }))
end
if flip() then
combination ..= "D"
registry2:set(id, B4, { value = true })
ecs:set(entity, D4, { value = true })
mcs:set(m, E4, { value = 2 })
newWorld:insert(n, A4({ value = true }))
end
if flip() then
combination ..= "E"
registry2:set(id, B5, { value = true })
ecs:set(entity, D5, { value = true })
mcs:set(m, E5, { value = 2 })
newWorld:insert(n, A5({ value = true }))
end
if flip() then
combination ..= "F"
registry2:set(id, B6, { value = true })
ecs:set(entity, D6, { value = true })
mcs:set(m, E6, { value = 2 })
newWorld:insert(n, A6({ value = true }))
end
if flip() then
combination ..= "G"
registry2:set(id, B7, { value = true })
ecs:set(entity, D7, { value = true })
mcs:set(m, E7, { value = 2 })
newWorld:insert(n, A7({ value = true }))
end
if flip() then
combination ..= "H"
registry2:set(id, B8, { value = true })
newWorld:insert(n, A8({ value = true }))
ecs:set(entity, D8, { value = true })
mcs:set(m, E8, { value = 2 })
end
if combination:find("BCDF") then
if not archetypes[combination] then
print(combination)
end
hm += 1
end
archetypes[combination] = true
end
print("TEST", hm)
@ -140,30 +109,38 @@ end
print(count)
local mq = mcs:query(E1, E2, E3, E4)
local jq = ecs:query(D1, D2, D3, D4)
return {
ParameterGenerator = function()
return
end,
Functions = {
Matter = function()
for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do
end
end,
ECR = function()
for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do
end
end,
-- Mirror = function()
-- for entityId, firstComponent in mcs:query(E2, E4, E6, E8) do
-- Matter = function()
-- for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do
-- end
-- end,
Jecs = function()
for entityId, firstComponent in ecs:query(D2, D4, D6, D8) do
-- ECR = function()
-- for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do
-- end
-- end,
Mirror = function()
for i = 1, 10 do
for entityId, firstComponent in mq:iter() do
end
end
end,
Jecs = function()
for i = 1, 10 do
for entityId, firstComponent in jq:iter() do
end
end
end,
},
}

View file

@ -24,83 +24,8 @@ type Id<T=unknown> = jecs.Id<T>
local entity_visualiser = require("@modules/entity_visualiser")
local dwi = entity_visualiser.stringify
TEST("optimize idr_r removal", function()
local pair = jecs.pair
local world = jecs.world()
local rel = world:component()
local A = world:component()
local B = world:component()
local t1 = world:entity()
local t2 = world:entity()
local entities = {} :: { jecs.Entity }
for i = 1, 10 do
local e1 = world:entity()
local e2 = world:entity()
world:set(e1, A, true)
world:set(e2, A, true)
world:add(e1, pair(B, t1))
world:add(e1, pair(B, t2))
world:add(e2, pair(B, t1))
world:add(e2, pair(B, t2))
table.insert(entities, e1)
table.insert(entities, e2)
end
local e1 = world:entity()
local e2 = world:entity()
table.insert(entities, e1)
table.insert(entities, e2)
world:set(e1, A, true)
world:set(e2, A, true)
world:add(e1, pair(B, t1))
world:add(e1, pair(B, t2))
world:add(e2, pair(B, t1))
world:add(e2, pair(B, t2))
BENCH("delete B", function()
world:delete(B)
end)
for _, e in entities do
CHECK(world:has(e, A))
CHECK(not world:target(e, B))
CHECK(not world:target(e, B))
end
end)
TEST("deleting t1's archetype before invoking its onremove hooks", function()
local pair = jecs.pair
local world = jecs.world()
local rel = world:component()
local t1 = world:entity()
local t2 = world:entity()
--[[
weirdly enough if i do this (only when adding childof relation after adding (rel, t2) to t1) it does not error. Probably a red herring
world:add(t2, pair(rel, t1))
world:add(t1, pair(rel, t2))
world:add(t2, pair(jecs.ChildOf, t1))
--]]
-- this causes world:delete to error
world:add(t2, pair(jecs.ChildOf, t1))
world:add(t1, pair(rel, t2))
world:delete(t1)
end)
TEST("reproduce idr_t nil archetype bug", function()
local world = jecs.world()
local world = jecs.world(true)
local cts = {
Humanoid = world:component(),
@ -121,46 +46,58 @@ TEST("reproduce idr_t nil archetype bug", function()
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)
CHECK_EXPECT_ERR(function()
world:remove(entity, cts.Humanoid)
end)
end)
local batches = 10
local batchSize = 20
local batchSize = 200
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))
for i = 1, batchSize do
-- 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 }
local root = world:entity()
-- world:add(root, jecs.pair(jecs.ChildOf, char))
-- 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 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))
world:set(trackEntity1, jecs.Name, "trackEntity1v"..i)
trackedEntities[trackEntity1] = { parentId = root }
-- Removing this, but keeping Animator on the other 2 causes it to stop happening
world:set(trackEntity1, cts.VelocitizeAnimationWeight, 0)
-- 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))
world:set(trackEntity2, jecs.Name, "trackEntity2v"..i)
trackedEntities[trackEntity2] = { parentId = 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:delete(entityId)
end
end
end
end
-- 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, __)):cached()
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(`current parent: {i}`)
print(`batch = {batchSize}, i = {i}`)
print("==========================================")
trackedEntities[entityId] = nil
end
end
for entity in q do
local parent = world:parent(entity) :: jecs.Entity
CHECK(world:parent(entity) == nil)
end
end
end)
TEST("Ensure archetype edges get cleaned", function()
@ -980,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()
@ -1599,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()
@ -1651,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()
@ -1668,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)
@ -1679,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()
@ -1746,6 +1971,45 @@ TEST("world:entity()", function()
local e2 = world:entity(399)
CHECK(world:contains(e2))
end
do CASE "desired id does not overwrite old entity id"
local world = jecs.world()
local ctype = world:component()
local id = world:entity()
local e = world:entity((id::any) + 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
local N = 2^8
do CASE "unique IDs"
@ -1817,6 +2081,8 @@ TEST("world:entity()", function()
CHECK(ECS_ID(e) == pin)
CHECK(ECS_GENERATION(e) == 0)
end
end)
TEST("world:has()", function()