mirror of
https://github.com/Ukendio/jecs.git
synced 2026-02-04 15:15:21 +00:00
Merge branch 'main' into docs/studio
This commit is contained in:
commit
fb178980a9
12 changed files with 1426 additions and 812 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
]]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
2
src/jecs.d.ts
vendored
|
|
@ -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.
|
||||
|
|
|
|||
1402
src/jecs.luau
1402
src/jecs.luau
File diff suppressed because it is too large
Load diff
BIN
test.rbxl
Executable file
BIN
test.rbxl
Executable file
Binary file not shown.
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
448
test/tests.luau
448
test/tests.luau
|
|
@ -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,44 +46,56 @@ 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))
|
||||
-- world:add(root, jecs.pair(jecs.ChildOf, char))
|
||||
|
||||
-- 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 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 }
|
||||
|
||||
-- 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(`batch = {batch}, i = {i}`)
|
||||
print(`current parent: {i}`)
|
||||
print(`batch = {batchSize}, i = {i}`)
|
||||
print("==========================================")
|
||||
trackedEntities[entityId] = nil
|
||||
world:delete(entityId)
|
||||
end
|
||||
end
|
||||
for entity in q do
|
||||
local parent = world:parent(entity) :: jecs.Entity
|
||||
CHECK(world:parent(entity) == nil)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue