jecs/test/tests.luau

1952 lines
43 KiB
Text
Raw Normal View History

2024-11-16 17:07:12 +00:00
local jecs = require("@jecs")
2024-07-06 21:30:14 +00:00
local testkit = require("@testkit")
2024-08-13 18:08:58 +00:00
local BENCH, START = testkit.benchmark()
2024-05-16 22:17:53 +00:00
local __ = jecs.Wildcard
local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION
local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC
local IS_PAIR = jecs.IS_PAIR
2024-07-15 18:29:06 +00:00
local pair = jecs.pair
2024-08-10 02:55:04 +00:00
local ecs_pair_first = jecs.pair_first
local ecs_pair_second = jecs.pair_second
local entity_index_try_get_any = jecs.entity_index_try_get_any
local entity_index_get_alive = jecs.entity_index_get_alive
local entity_index_is_alive = jecs.entity_index_is_alive
local ChildOf = jecs.ChildOf
local world_new = jecs.World.new
2025-01-29 07:28:08 +00:00
local it = testkit.test()
local TEST, CASE = it.TEST, it.CASE
local CHECK, FINISH = it.CHECK, it.FINISH
local SKIP, FOCUS = it.SKIP, it.FOCUS
local CHECK_EXPECT_ERR = it.CHECK_EXPECT_ERR
local N = 2 ^ 8
type World = jecs.World
type Entity<T=nil> = jecs.Entity<T>
2025-03-12 14:29:24 +00:00
local c = {
white_underline = function(s: any)
return `\27[1;4m{s}\27[0m`
end,
white = function(s: any)
return `\27[37;1m{s}\27[0m`
end,
green = function(s: any)
return `\27[32;1m{s}\27[0m`
end,
red = function(s: any)
return `\27[31;1m{s}\27[0m`
end,
yellow = function(s: any)
return `\27[33;1m{s}\27[0m`
end,
red_highlight = function(s: any)
return `\27[41;1;30m{s}\27[0m`
end,
green_highlight = function(s: any)
return `\27[42;1;30m{s}\27[0m`
end,
gray = function(s: any)
return `\27[30;1m{s}\27[0m`
end,
}
local function pe(e)
local gen = ECS_GENERATION(e)
return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`)
end
local function pp(e)
local gen = ECS_GENERATION(e)
return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{jecs.ECS_ENTITY_T_HI(e)}`)
end
local function debug_world_inspect(world: World)
local function record(e): jecs.Record
return entity_index_try_get_any(world.entity_index, e) :: any
end
local function tbl(e)
return record(e).archetype
end
local function archetype(e)
return tbl(e).type
end
local function records(e)
return tbl(e).records
end
local function columns(e)
return tbl(e).columns
end
local function row(e)
return record(e).row
end
-- Important to order them in the order of their columns
local function tuple(e, ...)
for i, column in columns(e) do
if select(i, ...) ~= column[row(e)] then
return false
end
end
return true
end
return {
record = record,
tbl = tbl,
archetype = archetype,
records = records,
row = row,
tuple = tuple,
2024-12-26 00:06:14 +00:00
columns = columns
}
2024-07-29 23:11:22 +00:00
end
2025-03-12 14:29:24 +00:00
local dwi = debug_world_inspect
local function name(world, e)
return world:get(e, jecs.Name)
end
TEST("#adding a recycled target", function()
local world = world_new()
local R = world:component()
local e = world:entity()
local T = world:entity()
world:add(e, pair(R, T))
world:delete(T)
CHECK(not world:has(e, pair(R, T)))
local T2 = world:entity()
world:add(e, pair(R, T2))
CHECK(world:target(e, R) ~= T)
CHECK(world:target(e, R) ~= 0)
end)
2025-03-30 20:29:43 +00:00
local entity_visualizer = require("@tools/entity_visualiser")
2025-03-30 20:14:22 +00:00
2025-03-25 22:13:53 +00:00
TEST("#repro2", function()
local world = world_new()
local Lifetime = world:component() :: jecs.Id<number>
local Particle = world:entity()
local Beam = world:entity()
local entity = world:entity()
world:set(entity, pair(Lifetime, Particle), 1)
world:set(entity, pair(Lifetime, Beam), 2)
2025-03-26 02:39:04 +00:00
world:set(entity, pair(4, 5), 6) -- noise
2025-03-27 16:37:36 +00:00
-- entity_visualizer.components(world, entity)
2025-03-25 22:13:53 +00:00
for e in world:each(pair(Lifetime, __)) do
local i = 0
local nth = world:target(e, Lifetime, i)
while nth do
2025-03-27 16:37:36 +00:00
-- entity_visualizer.components(world, e)
2025-03-26 02:39:04 +00:00
2025-03-25 22:13:53 +00:00
local data = world:get(e, pair(Lifetime, nth))
2025-03-26 02:39:04 +00:00
data -= 1
if data <= 0 then
world:remove(e, pair(Lifetime, nth))
else
world:set(e, pair(Lifetime, nth), data)
end
2025-03-25 22:13:53 +00:00
i += 1
nth = world:target(e, Lifetime, i)
end
end
2025-03-26 02:39:04 +00:00
CHECK(not world:has(entity, pair(Lifetime, Particle)))
CHECK(world:get(entity, pair(Lifetime, Beam)) == 1)
end)
2025-03-30 20:29:43 +00:00
local lifetime_tracker_add = require("@tools/lifetime_tracker")
2025-03-26 02:39:04 +00:00
TEST("another", function()
local world = world_new()
2025-03-27 16:37:36 +00:00
-- world = lifetime_tracker_add(world, {padding_enabled=false})
2025-03-26 02:39:04 +00:00
local e1 = world:entity()
local e2 = world:entity()
local e3 = world:entity()
world:delete(e2)
local e2_e3 = pair(e2, e3)
CHECK(jecs.pair_first(world, e2_e3) == 0)
CHECK(jecs.pair_second(world, e2_e3) == e3)
CHECK_EXPECT_ERR(function()
world:add(e1, pair(e2, e3))
end)
2025-03-25 22:13:53 +00:00
end)
2025-03-12 14:29:24 +00:00
TEST("#repro", function()
local world = world_new()
local function getTargets(relation)
local tgts = {}
local pairwildcard = pair(relation, jecs.Wildcard)
for _, archetype in world:query(pairwildcard):archetypes() do
2025-03-24 13:31:56 +00:00
local tr = archetype.records[pairwildcard]
local count = archetype.counts[pairwildcard]
local types = archetype.types
for _, entity in archetype.entities do
for i = 0, count - 1 do
local tgt = jecs.pair_second(world, types[i + tr])
table.insert(tgts, tgt)
end
end
2025-03-12 14:29:24 +00:00
end
return tgts
end
local Attacks = world:component()
local Eats = world:component()
local function setAttacksAndEats(entity1, entity2)
world:add(entity1, pair(Attacks, entity2))
world:add(entity1, pair(Eats, entity2))
end
local e1 = world:entity()
local e2 = world:entity()
local e3 = world:entity()
setAttacksAndEats(e3, e1)
setAttacksAndEats(e3, e2)
setAttacksAndEats(e1, e2)
local d = dwi(world)
world:delete(e2)
2025-03-24 13:31:56 +00:00
local types1 = { pair(Attacks, e1), pair(Eats, e1) }
table.sort(types1)
CHECK(d.tbl(e1).type == "")
CHECK(d.tbl(e3).type == table.concat(types1, "_"))
for _, entity in getTargets(Attacks) do
CHECK(entity == e1)
end
for _, entity in getTargets(Eats) do
CHECK(entity == e1)
end
2025-03-12 14:29:24 +00:00
end)
TEST("archetype", function()
local archetype_traverse_add = jecs.archetype_traverse_add
local archetype_traverse_remove = jecs.archetype_traverse_remove
local world = world_new()
local root = world.ROOT_ARCHETYPE
local c1 = world:component()
local c2 = world:component()
local c3 = world:component()
2025-01-29 07:28:08 +00:00
local a1 = archetype_traverse_add(world, c1, nil :: any)
local a2 = archetype_traverse_remove(world, c1, a1)
CHECK(root.add[c1].to == a1)
CHECK(root == a2)
end)
2024-10-01 15:30:51 +00:00
TEST("world:cleanup()", function()
local world = world_new()
2025-03-26 03:59:11 +00:00
local A = world:component() :: jecs.Id<boolean>
local B = world:component() :: jecs.Id<boolean>
local C = world:component() :: jecs.Id<boolean>
local e1 = world:entity()
local e2 = world:entity()
local e3 = world:entity()
world:set(e1, A, true)
world:set(e2, A, true)
world:set(e2, B, true)
world:set(e3, A, true)
world:set(e3, B, true)
world:set(e3, C, true)
2025-01-29 07:28:08 +00:00
local archetype_index = world.archetype_index
2025-01-29 07:28:08 +00:00
CHECK(#archetype_index["1"].entities == 1)
CHECK(#archetype_index["1_2"].entities == 1)
CHECK(#archetype_index["1_2_3"].entities == 1)
world:delete(e1)
world:delete(e2)
world:delete(e3)
world:cleanup()
2025-01-29 07:28:08 +00:00
archetype_index = world.archetype_index
2025-01-29 07:28:08 +00:00
CHECK((archetype_index["1"] :: jecs.Archetype?) == nil)
CHECK((archetype_index["1_2"] :: jecs.Archetype?) == nil)
CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil)
local e4 = world:entity()
world:set(e4, A, true)
2025-01-29 07:28:08 +00:00
CHECK(#archetype_index["1"].entities == 1)
CHECK((archetype_index["1_2"] :: jecs.Archetype?) == nil)
CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil)
world:set(e4, B, true)
2025-01-29 07:28:08 +00:00
CHECK(#archetype_index["1"].entities == 0)
CHECK(#archetype_index["1_2"].entities == 1)
CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil)
world:set(e4, C, true)
2025-01-29 07:28:08 +00:00
CHECK(#archetype_index["1"].entities == 0)
CHECK(#archetype_index["1_2"].entities == 0)
CHECK(#archetype_index["1_2_3"].entities == 1)
2024-10-01 15:30:51 +00:00
end)
2025-03-30 20:29:43 +00:00
local pe = require("@tools/entity_visualiser").prettify
local lifetime_tracker_add = require("@tools/lifetime_tracker")
2025-03-24 13:31:56 +00:00
2024-07-29 23:11:22 +00:00
TEST("world:entity()", function()
do
CASE("unique IDs")
local world = jecs.World.new()
local set = {}
for i = 1, N do
local e = world:entity()
CHECK(not set[e])
set[e] = true
end
end
do
CASE("generations")
2024-07-29 23:11:22 +00:00
local world = jecs.World.new()
2025-01-29 07:28:08 +00:00
local e = world:entity() :: number
CHECK(ECS_ID(e) == 1 + jecs.Rest :: number)
2024-07-29 23:11:22 +00:00
CHECK(ECS_GENERATION(e) == 0) -- 0
e = ECS_GENERATION_INC(e)
CHECK(ECS_GENERATION(e) == 1) -- 1
end
2025-01-29 07:28:08 +00:00
do CASE "pairs"
2024-07-29 23:11:22 +00:00
local world = jecs.World.new()
local _e = world:entity()
local e2 = world:entity()
local e3 = world:entity()
-- Incomplete pair, must have a bit flag that notes it is a pair
CHECK(IS_PAIR(world:entity()) == false)
local p = pair(e2, e3)
CHECK(IS_PAIR(p) == true)
2025-01-29 07:28:08 +00:00
CHECK(ecs_pair_first(world, p) == e2 :: number)
CHECK(ecs_pair_second(world, p) == e3 :: number)
world:delete(e2)
local e2v2 = world:entity()
CHECK(IS_PAIR(e2v2) == false)
2024-07-29 23:11:22 +00:00
CHECK(IS_PAIR(pair(e2v2, e3)) == true)
2024-07-29 23:11:22 +00:00
end
do CASE "Recycling"
local world = world_new()
local e = world:entity()
world:delete(e)
local e1 = world:entity()
world:delete(e1)
local e2 = world:entity()
2025-01-29 07:28:08 +00:00
CHECK(ECS_ID(e2) == e :: number)
CHECK(ECS_GENERATION(e2) == 2)
CHECK(world:contains(e2))
CHECK(not world:contains(e1))
CHECK(not world:contains(e))
end
do CASE "Recycling max generation"
local world = world_new()
2025-03-26 03:59:11 +00:00
local pin = (jecs.Rest :: any) :: number + 1
for i = 1, 2^16-1 do
local e = world:entity()
world:delete(e)
end
local e = world:entity()
CHECK(ECS_ID(e) == pin)
CHECK(ECS_GENERATION(e) == 2^16-1)
world:delete(e)
e = world:entity()
CHECK(ECS_ID(e) == pin)
CHECK(ECS_GENERATION(e) == 0)
end
2024-07-29 23:11:22 +00:00
end)
TEST("world:set()", function()
2025-01-29 07:28:08 +00:00
do CASE "archetype move"
do
local world = jecs.World.new()
local d = debug_world_inspect(world)
local _1 = world:component()
local _2 = world:component()
local e = world:entity()
-- An entity starts without an archetype or row
-- should therefore not need to copy over data
CHECK(d.tbl(e) == nil)
CHECK(d.row(e) == nil)
local archetypes = #world.archetypes
-- This should create a new archetype since it is the first
-- entity to have moved there
world:set(e, _1, 1)
local oldRow = d.row(e)
local oldArchetype = d.archetype(e)
CHECK(#world.archetypes == archetypes + 1)
CHECK(oldArchetype == "1")
CHECK(d.tbl(e))
CHECK(oldRow == 1)
world:set(e, _2, 2)
CHECK(d.archetype(e) == "1_2")
-- Should have tuple of fields to the next archetype and set the component data
CHECK(d.tuple(e, 1, 2))
-- Should have moved the data from the old archetype
2025-01-29 07:28:08 +00:00
CHECK(world.archetype_index[oldArchetype].columns[_1][oldRow] == nil)
end
end
2025-01-29 07:28:08 +00:00
do CASE "pairs"
local world = jecs.World.new()
local C1 = world:component()
local C2 = world:component()
local T1 = world:entity()
local T2 = world:entity()
local e = world:entity()
world:set(e, pair(C1, C2), true)
world:set(e, pair(C1, T1), true)
world:set(e, pair(T1, C1), true)
2025-01-29 07:28:08 +00:00
CHECK_EXPECT_ERR(function()
world:set(e, pair(T1, T2), true :: any)
end)
CHECK(world:get(e, pair(C1, C2)))
CHECK(world:get(e, pair(C1, T1)))
CHECK(world:get(e, pair(T1, C1)))
CHECK(not world:get(e, pair(T1, T2)))
local e2 = world:entity()
2025-01-29 07:28:08 +00:00
CHECK_EXPECT_ERR(function()
world:set(e2, pair(jecs.ChildOf, e), true :: any)
end)
CHECK(not world:get(e2, pair(jecs.ChildOf, e)))
end
2024-07-29 23:11:22 +00:00
end)
TEST("world:remove()", function()
do
CASE("should allow remove a component that doesn't exist on entity")
2024-07-26 02:45:07 +00:00
local world = jecs.World.new()
local Health = world:component()
2024-07-26 02:45:07 +00:00
local Poison = world:component()
local id = world:entity()
do
world:remove(id, Poison)
2024-07-26 02:45:07 +00:00
CHECK(true) -- Didn't error
end
world:set(id, Health, 50)
world:remove(id, Poison)
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == 50)
end
2024-07-29 23:11:22 +00:00
end)
2024-07-29 23:11:22 +00:00
TEST("world:add()", function()
do
CASE("idempotent")
local world = jecs.World.new()
local d = debug_world_inspect(world)
local _1, _2 = world:component(), world:component()
local e = world:entity()
world:add(e, _1)
world:add(e, _2)
world:add(e, _2) -- should have 0 effects
CHECK(d.archetype(e) == "1_2")
end
do
CASE("archetype move")
do
local world = jecs.World.new()
local d = debug_world_inspect(world)
local _1 = world:component()
local e = world:entity()
-- An entity starts without an archetype or row
-- should therefore not need to copy over data
CHECK(d.tbl(e) == nil)
CHECK(d.row(e) == nil)
2024-08-11 02:03:18 +00:00
local archetypes = #world.archetypes
-- This should create a new archetype
world:add(e, _1)
CHECK(#world.archetypes == archetypes + 1)
CHECK(d.archetype(e) == "1")
CHECK(d.tbl(e))
end
end
2024-07-29 23:11:22 +00:00
end)
2024-07-29 23:11:22 +00:00
TEST("world:query()", function()
do CASE "cached"
local world = world_new()
local Foo = world:component()
local Bar = world:component()
local Baz = world:component()
local e = world:entity()
local q = world:query(Foo, Bar):without(Baz):cached()
world:set(e, Foo, true)
world:set(e, Bar, false)
local i = 0
2025-01-14 10:09:18 +00:00
local iter = 0
for _, e in q:iter() do
2025-01-14 10:09:18 +00:00
iter += 1
i=1
end
2025-01-14 10:09:18 +00:00
CHECK (iter == 1)
CHECK(i == 1)
for _, e in q:iter() do
i=2
end
CHECK(i == 2)
2025-01-29 07:28:08 +00:00
for _, e in q :: any do
i=3
end
CHECK(i == 3)
2025-01-29 07:28:08 +00:00
for _, e in q :: any do
i=4
end
CHECK(i == 4)
CHECK(#q:archetypes() == 1)
2025-01-29 07:28:08 +00:00
CHECK(not table.find(q:archetypes(), world.archetype_index[table.concat({Foo, Bar, Baz}, "_")]))
world:delete(Foo)
CHECK(#q:archetypes() == 0)
end
2025-01-29 07:28:08 +00:00
do CASE "multiple iter"
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local e = world:entity()
world:add(e, A)
world:add(e, B)
local q = world:query(A, B)
local counter = 0
for x in q:iter() do
counter += 1
end
for x in q:iter() do
counter += 1
end
CHECK(counter == 2)
end
2025-01-29 07:28:08 +00:00
do CASE "tag"
local world = jecs.World.new()
local A = world:entity()
local e = world:entity()
2025-01-29 07:28:08 +00:00
CHECK_EXPECT_ERR(function()
world:set(e, A, "test" :: any)
end)
local count = 0
for id, a in world:query(A) :: any do
count += 1
CHECK(a == nil)
end
2025-01-29 07:28:08 +00:00
CHECK(count == 1)
end
2025-01-29 07:28:08 +00:00
do CASE "pairs"
local world = jecs.World.new()
2025-03-26 03:59:11 +00:00
local C1 = world:component() :: jecs.Id<boolean>
local C2 = world:component() :: jecs.Id<boolean>
local T1 = world:entity()
local T2 = world:entity()
local e = world:entity()
2025-03-26 03:59:11 +00:00
local C1_C2 = pair(C1, C2)
world:set(e, C1_C2, true)
world:set(e, pair(C1, T1), true)
world:set(e, pair(T1, C1), true)
2025-01-29 07:28:08 +00:00
CHECK_EXPECT_ERR(function()
world:set(e, pair(T1, T2), true :: any)
end)
2025-03-26 03:59:11 +00:00
for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)):iter() do
CHECK(a == true)
CHECK(b == true)
CHECK(c == true)
CHECK(d == nil)
end
end
do
CASE("query single component")
do
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
world:set(id, A, true)
if i > 5 then
world:set(id, B, true)
end
entities[i] = id
end
2025-01-29 07:28:08 +00:00
for id in world:query(A) :: any do
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(#entities == 0)
end
do
local world = jecs.World.new() :: World
local A = world:component()
local B = world:component()
local eA = world:entity()
world:set(eA, A, true)
local eB = world:entity()
world:set(eB, B, true)
local eAB = world:entity()
world:set(eAB, A, true)
world:set(eAB, B, true)
-- Should drain the iterator
local q = world:query(A)
local i = 0
local j = 0
2025-01-29 07:28:08 +00:00
for _ in q :: any do
i += 1
end
2025-01-29 07:28:08 +00:00
for _ in q :: any do
j += 1
end
CHECK(i == 2)
CHECK(j == 0)
end
end
do
CASE("query missing component")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local C = world:component()
local e1 = world:entity()
local e2 = world:entity()
world:set(e1, A, "abc")
world:set(e2, A, "def")
world:set(e1, B, 123)
world:set(e2, B, 457)
local counter = 0
2025-01-29 07:28:08 +00:00
for _ in world:query(B, C) :: any do
counter += 1
end
CHECK(counter == 0)
end
do
CASE("query more than 8 components")
local world = jecs.World.new()
local components = {}
for i = 1, 9 do
local id = world:component()
components[i] = id
end
local e = world:entity()
for i, id in components do
world:set(e, id, 13 ^ i)
end
2025-01-29 07:28:08 +00:00
for entity, a, b, c, d, e, f, g, h, i in world:query(unpack(components)) :: any do
CHECK(a == 13 ^ 1)
CHECK(b == 13 ^ 2)
CHECK(c == 13 ^ 3)
CHECK(d == 13 ^ 4)
CHECK(e == 13 ^ 5)
CHECK(f == 13 ^ 6)
CHECK(g == 13 ^ 7)
CHECK(h == 13 ^ 8)
CHECK(i == 13 ^ 9)
end
end
do
CASE("should be able to get next results")
local world = jecs.World.new() :: World
world:component()
local A = world:component()
local B = world:component()
local eA = world:entity()
world:set(eA, A, true)
local eB = world:entity()
world:set(eB, B, true)
local eAB = world:entity()
world:set(eAB, A, true)
world:set(eAB, B, true)
local it = world:query(A):iter()
2025-01-29 07:28:08 +00:00
local e: number, data = it()
while e do
2025-01-29 07:28:08 +00:00
if e == eA :: number then
CHECK(data)
2025-01-29 07:28:08 +00:00
elseif e == eAB :: number then
CHECK(data)
else
CHECK(false)
end
e, data = it()
end
CHECK(true)
end
2025-01-29 07:28:08 +00:00
do CASE "should query all matching entities when irrelevant component is removed"
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local C = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
-- specifically put them in disorder to track regression
-- https://github.com/Ukendio/jecs/pull/15
world:set(id, B, true)
world:set(id, A, true)
if i > 5 then
world:remove(id, B)
end
entities[i] = id
end
local added = 0
2025-01-29 07:28:08 +00:00
for id in world:query(A) :: any do
added += 1
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(added == N)
end
do
CASE("should query all entities without B")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
world:set(id, A, true)
if i < 5 then
entities[i] = id
else
world:set(id, B, true)
end
end
2025-01-29 07:28:08 +00:00
for id in world:query(A):without(B) :: any do
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(#entities == 0)
end
do
CASE("should allow querying for relations")
local world = jecs.World.new()
local Eats = world:component()
local Apples = world:component()
local bob = world:entity()
2024-07-15 18:29:06 +00:00
world:set(bob, pair(Eats, Apples), true)
2025-01-29 07:28:08 +00:00
for e, bool in world:query(pair(Eats, Apples)) :: any do
CHECK(e == bob)
CHECK(bool)
end
end
do
CASE("should allow wildcards in queries")
local world = jecs.World.new()
local Eats = world:component()
local Apples = world:entity()
local bob = world:entity()
2024-07-15 18:29:06 +00:00
world:set(bob, pair(Eats, Apples), "bob eats apples")
local w = jecs.Wildcard
2025-01-29 07:28:08 +00:00
for e, data in world:query(pair(Eats, w)) :: any do
CHECK(e == bob)
CHECK(data == "bob eats apples")
end
2025-01-29 07:28:08 +00:00
for e, data in world:query(pair(w, Apples)) :: any do
CHECK(e == bob)
CHECK(data == "bob eats apples")
end
end
do
CASE("should match against multiple pairs")
local world = jecs.World.new()
local Eats = world:component()
local Apples = world:entity()
local Oranges = world:entity()
local bob = world:entity()
local alice = world:entity()
2024-07-15 18:29:06 +00:00
world:set(bob, pair(Eats, Apples), "bob eats apples")
world:set(alice, pair(Eats, Oranges), "alice eats oranges")
local w = jecs.Wildcard
local count = 0
2025-01-29 07:28:08 +00:00
for e, data in world:query(pair(Eats, w)) :: any do
count += 1
if e == bob then
CHECK(data == "bob eats apples")
else
CHECK(data == "alice eats oranges")
end
end
CHECK(count == 2)
count = 0
2025-01-29 07:28:08 +00:00
for e, data in world:query(pair(w, Apples)) :: any do
count += 1
CHECK(data == "bob eats apples")
end
CHECK(count == 1)
end
2025-01-29 07:28:08 +00:00
do CASE "should only relate alive entities"
local world = jecs.World.new()
local Eats = world:entity()
2025-01-29 07:28:08 +00:00
local Apples = world:component()
local Oranges = world:component()
local bob = world:entity()
local alice = world:entity()
world:set(bob, Apples, "apples")
2024-07-15 18:29:06 +00:00
world:set(bob, pair(Eats, Apples), "bob eats apples")
2025-01-29 07:28:08 +00:00
world:set(alice, pair(Eats, Oranges) :: Entity<string>, "alice eats oranges")
world:delete(Apples)
local Wildcard = jecs.Wildcard
local count = 0
2025-01-29 07:28:08 +00:00
for _, data in world:query(pair(Wildcard, Apples)) :: any do
count += 1
end
2024-07-15 18:29:06 +00:00
world:delete(pair(Eats, Apples))
CHECK(count == 0)
2024-07-15 18:29:06 +00:00
CHECK(world:get(bob, pair(Eats, Apples)) == nil)
2025-01-29 07:28:08 +00:00
end
do
CASE("should error when setting invalid pair")
local world = jecs.World.new()
local Eats = world:component()
local Apples = world:component()
local bob = world:entity()
world:delete(Apples)
2025-03-26 02:39:04 +00:00
CHECK_EXPECT_ERR(function()
world:set(bob, pair(Eats, Apples), "bob eats apples")
end)
end
do
CASE("should find target for ChildOf")
local world = jecs.World.new()
2024-07-03 00:46:54 +00:00
local ChildOf = jecs.ChildOf
local Name = world:component()
local bob = world:entity()
local alice = world:entity()
local sara = world:entity()
2024-07-03 00:46:54 +00:00
world:add(bob, pair(ChildOf, alice))
world:set(bob, Name, "bob")
2024-07-03 00:46:54 +00:00
world:add(sara, pair(ChildOf, alice))
world:set(sara, Name, "sara")
2025-01-29 07:28:08 +00:00
CHECK(world:parent(bob) :: number == alice :: number) -- O(1)
local count = 0
2025-01-29 07:28:08 +00:00
for _, name in world:query(Name, pair(ChildOf, alice)) :: any do
count += 1
end
CHECK(count == 2)
end
do
CASE("despawning while iterating")
local world = jecs.World.new()
2024-07-31 16:48:34 +00:00
local A = world:component()
local B = world:component()
local e1 = world:entity()
local e2 = world:entity()
world:add(e1, A)
world:add(e2, A)
world:add(e2, B)
local count = 0
2025-01-29 07:28:08 +00:00
for id in world:query(A) :: any do
world:clear(id)
2024-07-31 16:48:34 +00:00
count += 1
end
CHECK(count == 2)
end
2025-03-27 16:37:36 +00:00
do CASE("iterator invalidation")
do CASE("adding")
2024-07-29 23:11:22 +00:00
SKIP()
local world = jecs.World.new()
2024-07-29 23:11:22 +00:00
local A = world:component()
local B = world:component()
2024-07-29 23:11:22 +00:00
local e1 = world:entity()
local e2 = world:entity()
world:add(e1, A)
world:add(e2, A)
world:add(e2, B)
2024-07-06 21:30:14 +00:00
2024-07-29 23:11:22 +00:00
local count = 0
2025-01-29 07:28:08 +00:00
for id in world:query(A) :: any do
2024-07-29 23:11:22 +00:00
world:add(id, B)
2024-07-29 23:11:22 +00:00
count += 1
end
2024-07-29 23:11:22 +00:00
CHECK(count == 2)
end
2025-03-27 16:37:36 +00:00
do CASE("spawning")
local world = jecs.World.new()
2024-07-29 23:11:22 +00:00
local A = world:component()
local B = world:component()
local e1 = world:entity()
local e2 = world:entity()
world:add(e1, A)
world:add(e2, A)
world:add(e2, B)
2025-01-29 07:28:08 +00:00
for id in world:query(A) :: any do
2024-07-29 23:11:22 +00:00
local e = world:entity()
world:add(e, A)
world:add(e, B)
end
2024-08-13 18:08:58 +00:00
CHECK(true)
2024-07-29 23:11:22 +00:00
end
end
2025-03-27 16:37:36 +00:00
do CASE("should not find any entities")
local world = jecs.World.new()
local Hello = world:component()
local Bob = world:component()
local helloBob = world:entity()
world:add(helloBob, pair(Hello, Bob))
world:add(helloBob, Bob)
local withoutCount = 0
2025-01-29 07:28:08 +00:00
for _ in world:query(pair(Hello, Bob)):without(Bob) :: any do
withoutCount += 1
end
CHECK(withoutCount == 0)
end
2025-03-27 16:37:36 +00:00
do CASE("without")
-- REGRESSION TEST
local world = jecs.World.new()
local _1, _2, _3 = world:component(), world:component(), world:component()
2025-03-27 16:37:36 +00:00
local counter = 0
for e, a, b in world:query(_1, _2):without(_3) :: any do
counter += 1
end
2025-03-27 16:37:36 +00:00
CHECK(counter == 0)
end
2024-07-29 23:11:22 +00:00
end)
TEST("world:each", function()
local world = world_new()
local A = world:component()
local B = world:component()
local C = world:component()
2025-01-29 07:28:08 +00:00
local e3 = world:entity()
local e1 = world:entity()
local e2 = world:entity()
world:set(e1, A, true)
world:set(e2, A, true)
world:set(e2, B, true)
world:set(e3, A, true)
world:set(e3, B, true)
world:set(e3, C, true)
2025-01-29 07:28:08 +00:00
for entity: number in world:each(A) do
if entity == e1 :: number or entity == e2 :: number or entity == e3 :: number then
CHECK(true)
continue
end
CHECK(false)
end
end)
TEST("world:children", function()
local world = world_new()
local C = world:component()
local T = world:entity()
local e1 = world:entity()
world:set(e1, C, true)
2025-01-29 07:28:08 +00:00
local e2 = world:entity() :: number
world:add(e2, T)
world:add(e2, pair(ChildOf, e1))
2025-01-29 07:28:08 +00:00
local e3 = world:entity() :: number
world:add(e3, pair(ChildOf, e1))
local count = 0
2025-01-29 07:28:08 +00:00
for entity: number in world:children(e1) do
count += 1
if entity == e2 or entity == e3 then
CHECK(true)
continue
end
CHECK(false)
end
CHECK(count == 2)
world:remove(e2, pair(ChildOf, e1))
count = 0
for entity in world:children(e1) do
count += 1
end
CHECK(count == 1)
end)
2024-07-29 23:11:22 +00:00
TEST("world:clear()", function()
2025-03-27 16:37:36 +00:00
do CASE("should remove its components")
local world = jecs.World.new() :: World
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
2025-03-27 16:37:36 +00:00
local e = world:entity()
local e1 = world:entity()
local e2 = world:entity()
2025-03-27 16:37:36 +00:00
world:set(e, A, true)
world:set(e, B, true)
2025-03-27 16:37:36 +00:00
world:set(e1, A, true)
world:set(e1, B, true)
2025-03-27 16:37:36 +00:00
CHECK(world:get(e, A))
CHECK(world:get(e, B))
2025-03-27 16:37:36 +00:00
world:clear(A)
CHECK(world:get(e, A) == nil)
CHECK(world:get(e, B))
CHECK(world:get(e1, A) == nil)
CHECK(world:get(e1, B))
end
2025-03-28 00:53:19 +00:00
do CASE("remove cleared ID from entities")
local world = world_new()
local A = world:component()
local B = world:component()
local C = world:component()
do
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
world:set(id1, A, true)
world:set(id2, A, true)
world:set(id2, B, true)
world:set(id3, A, true)
world:set(id3, B, true)
world:set(id3, C, true)
world:clear(A)
CHECK(not world:has(id1, A))
CHECK(not world:has(id2, A))
CHECK(not world:has(id3, A))
CHECK(world:has(id2, B))
CHECK(world:has(id3, B, C))
world:clear(C)
CHECK(world:has(id2, B))
CHECK(world:has(id3, B))
CHECK(world:contains(A))
CHECK(world:contains(C))
CHECK(world:has(A, jecs.Component))
CHECK(world:has(B, jecs.Component))
end
do
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
local tgt = world:entity()
world:add(id1, pair(A, tgt))
world:add(id1, pair(B, tgt))
world:add(id1, pair(C, tgt))
world:add(id2, pair(A, tgt))
world:add(id2, pair(B, tgt))
world:add(id2, pair(C, tgt))
world:add(id3, pair(A, tgt))
world:add(id3, pair(B, tgt))
world:add(id3, pair(C, tgt))
world:clear(B)
CHECK(world:has(id1, pair(A, tgt), pair(C, tgt)))
CHECK(not world:has(id1, pair(B, tgt)))
CHECK(world:has(id2, pair(A, tgt), pair(C, tgt)))
CHECK(not world:has(id1, pair(B, tgt)))
CHECK(world:has(id3, pair(A, tgt), pair(C, tgt)))
end
end
2024-07-29 23:11:22 +00:00
end)
2024-07-29 23:11:22 +00:00
TEST("world:has()", function()
2025-03-27 16:37:36 +00:00
do CASE("should find Tag on entity")
local world = jecs.World.new()
local Tag = world:entity()
local e = world:entity()
world:add(e, Tag)
CHECK(world:has(e, Tag))
end
2024-08-11 13:03:05 +00:00
2025-03-27 16:37:36 +00:00
do CASE("should return false when missing one tag")
local world = jecs.World.new()
2024-08-11 13:03:05 +00:00
local A = world:entity()
local B = world:entity()
local C = world:entity()
local D = world:entity()
2024-08-11 13:03:05 +00:00
local e = world:entity()
world:add(e, A)
world:add(e, C)
world:add(e, D)
2024-08-11 13:03:05 +00:00
CHECK(world:has(e, A, B, C, D) == false)
end
2024-07-29 23:11:22 +00:00
end)
2024-07-23 02:44:56 +00:00
2024-07-29 23:11:22 +00:00
TEST("world:component()", function()
2025-03-27 16:37:36 +00:00
do CASE("only components should have EcsComponent trait")
local world = jecs.World.new() :: World
local A = world:component()
local e = world:entity()
CHECK(world:has(A, jecs.Component))
CHECK(not world:has(e, jecs.Component))
end
do CASE("tag")
local world = jecs.World.new() :: World
local A = world:component()
local B = world:entity()
local C = world:entity()
local e = world:entity()
world:set(e, A, "test")
world:add(e, B)
2025-01-29 07:28:08 +00:00
CHECK_EXPECT_ERR(function()
world:set(e, C, 11 :: any)
end)
CHECK(world:has(e, A))
CHECK(world:get(e, A) == "test")
CHECK(world:get(e, B) == nil)
CHECK(world:get(e, C) == nil)
end
2024-07-29 23:11:22 +00:00
end)
2024-07-23 02:44:56 +00:00
2024-07-29 23:11:22 +00:00
TEST("world:delete", function()
2025-03-01 19:15:52 +00:00
do CASE "invoke OnRemove hooks"
local world = world_new()
local e1 = world:entity()
local e2 = world:entity()
local Stable = world:component()
world:set(Stable, jecs.OnRemove, function(e)
CHECK(e == e1)
end)
world:set(e1, Stable, true)
world:set(e2, Stable, true)
world:delete(e1)
end
2025-02-01 12:07:55 +00:00
do CASE "delete recycled entity id used as component"
local world = world_new()
local id = world:entity()
world:add(id, jecs.Component)
local e = world:entity()
world:set(e, id, 1)
CHECK(world:get(e, id) == 1)
world:delete(id)
local recycled = world:entity()
world:add(recycled, jecs.Component)
world:set(e, recycled, 1)
CHECK(world:has(recycled, jecs.Component))
CHECK(world:get(e, recycled) == 1)
end
do
CASE("bug: Empty entity does not respect cleanup policy")
local world = world_new()
local parent = world:entity()
local tag = world:entity()
local child = world:entity()
world:add(child, jecs.pair(jecs.ChildOf, parent))
world:delete(parent)
CHECK(not world:contains(parent))
CHECK(not world:contains(child))
local entity = world:entity()
world:add(entity, tag)
world:delete(tag)
CHECK(world:contains(entity))
CHECK(not world:contains(tag))
CHECK(not world:has(entity, tag)) -- => true
end
2025-03-28 00:53:19 +00:00
do CASE("should allow deleting components")
local world = jecs.World.new()
local Health = world:component()
local Poison = world:component()
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
local id1 = world:entity()
world:set(id1, Poison, 500)
world:set(id1, Health, 50)
world:delete(id)
CHECK(not world:contains(id))
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == nil)
CHECK(world:get(id1, Poison) == 500)
CHECK(world:get(id1, Health) == 50)
end
2025-03-28 00:53:19 +00:00
do CASE("delete entities using another Entity as component with Delete cleanup action")
local world = jecs.World.new()
2024-08-13 18:08:58 +00:00
local Health = world:entity()
world:add(Health, pair(jecs.OnDelete, jecs.Delete))
2024-08-13 18:08:58 +00:00
local Poison = world:component()
local id = world:entity()
world:set(id, Poison, 5)
2025-01-29 07:28:08 +00:00
CHECK_EXPECT_ERR(function()
world:set(id, Health, 50 :: any)
end)
2024-08-13 18:08:58 +00:00
local id1 = world:entity()
world:set(id1, Poison, 500)
2025-01-29 07:28:08 +00:00
CHECK_EXPECT_ERR(function()
world:set(id1, Health, 50 :: any)
end)
2024-08-13 18:08:58 +00:00
CHECK(world:has(id, Poison, Health))
CHECK(world:has(id1, Poison, Health))
world:delete(Poison)
CHECK(world:contains(id))
2024-08-13 18:08:58 +00:00
CHECK(not world:has(id, Poison))
CHECK(not world:has(id1, Poison))
world:delete(Health)
CHECK(not world:contains(id))
CHECK(not world:contains(id1))
CHECK(not world:has(id, Health))
CHECK(not world:has(id1, Health))
2024-08-13 18:08:58 +00:00
end
2025-03-28 00:53:19 +00:00
do CASE("delete children")
local world = jecs.World.new()
2024-08-13 18:08:58 +00:00
local Health = world:component()
local Poison = world:component()
local FriendsWith = world:component()
local e = world:entity()
world:set(e, Poison, 5)
world:set(e, Health, 50)
local children = {}
for i = 1, 10 do
local child = world:entity()
2024-08-13 18:08:58 +00:00
world:set(child, Poison, 9999)
world:set(child, Health, 100)
world:add(child, pair(jecs.ChildOf, e))
table.insert(children, child)
end
BENCH("delete children of entity", function()
world:delete(e)
2024-08-13 18:08:58 +00:00
end)
for i, child in children do
CHECK(not world:contains(child))
CHECK(not world:has(child, pair(jecs.ChildOf, e)))
2024-08-13 18:08:58 +00:00
CHECK(not world:has(child, Health))
end
e = world:entity()
local friends = {}
for i = 1, 10 do
local friend = world:entity()
2024-08-13 18:08:58 +00:00
world:set(friend, Poison, 9999)
world:set(friend, Health, 100)
world:add(friend, pair(FriendsWith, e))
table.insert(friends, friend)
end
BENCH("remove friends of entity", function()
world:delete(e)
end)
2024-08-13 18:08:58 +00:00
2025-03-12 14:29:24 +00:00
local d = debug_world_inspect(world)
for i, friend in friends do
CHECK(not world:has(friend, pair(FriendsWith, e)))
CHECK(world:has(friend, Health))
CHECK(world:contains(friend))
end
end
2024-08-13 18:08:58 +00:00
2025-03-28 00:53:19 +00:00
do CASE("remove deleted ID from entities")
local world = world_new()
do
local A = world:component()
local B = world:component()
local C = world:component()
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
world:set(id1, A, true)
world:set(id2, A, true)
world:set(id2, B, true)
world:set(id3, A, true)
world:set(id3, B, true)
world:set(id3, C, true)
world:delete(A)
CHECK(not world:has(id1, A))
CHECK(not world:has(id2, A))
CHECK(not world:has(id3, A))
CHECK(world:has(id2, B))
CHECK(world:has(id3, B, C))
world:delete(C)
CHECK(world:has(id2, B))
CHECK(world:has(id3, B))
CHECK(not world:contains(A))
CHECK(not world:contains(C))
end
do
local A = world:component()
world:add(A, pair(jecs.OnDeleteTarget, jecs.Delete))
local B = world:component()
local C = world:component()
world:add(C, pair(jecs.OnDeleteTarget, jecs.Delete))
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
world:set(id1, C, true)
world:set(id2, pair(A, id1), true)
world:set(id2, B, true)
world:set(id3, B, true)
world:set(id3, pair(C, id2), true)
world:delete(id1)
CHECK(not world:contains(id1))
CHECK(not world:contains(id2))
CHECK(not world:contains(id3))
end
do
local A = world:component()
local B = world:component()
local C = world:component()
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
world:set(id2, A, true)
world:set(id2, pair(B, id1), true)
world:set(id3, A, true)
world:set(id3, pair(B, id1), true)
world:set(id3, C, true)
world:delete(id1)
CHECK(not world:contains(id1))
CHECK(world:contains(id2))
CHECK(world:contains(id3))
CHECK(world:has(id2, A))
CHECK(world:has(id3, A, C))
CHECK(not world:target(id2, B))
CHECK(not world:target(id3, B))
end
end
do
CASE("fast delete")
local world = jecs.World.new()
2024-08-13 18:08:58 +00:00
local entities = {}
2024-08-13 18:08:58 +00:00
local Health = world:component()
local Poison = world:component()
2024-08-13 18:58:07 +00:00
for i = 1, 100 do
local child = world:entity()
2024-08-13 18:08:58 +00:00
world:set(child, Poison, 9999)
world:set(child, Health, 100)
table.insert(entities, child)
end
BENCH("simple deletion of entity", function()
for i = 1, START(100) do
2024-08-13 18:08:58 +00:00
local e = entities[i]
world:delete(e)
end
end)
for _, entity in entities do
CHECK(not world:contains(entity))
2024-08-13 18:08:58 +00:00
end
end
2024-08-13 23:15:04 +00:00
do
CASE("cycle")
local world = jecs.World.new()
local Likes = world:component()
world:add(Likes, pair(jecs.OnDeleteTarget, jecs.Delete))
local bob = world:entity()
local alice = world:entity()
2024-08-13 23:15:04 +00:00
world:add(bob, pair(Likes, alice))
world:add(alice, pair(Likes, bob))
2024-08-13 23:15:04 +00:00
world:delete(bob)
CHECK(not world:contains(bob))
CHECK(not world:contains(alice))
2024-08-13 23:15:04 +00:00
end
end)
TEST("world:target", function()
2024-12-26 00:06:14 +00:00
do CASE("nth index")
local world = world_new()
local A = world:component()
2024-12-26 00:06:14 +00:00
world:set(A, jecs.Name, "A")
local B = world:component()
2024-12-26 00:06:14 +00:00
world:set(B, jecs.Name, "B")
local C = world:component()
2024-12-26 00:06:14 +00:00
world:set(C, jecs.Name, "C")
local D = world:component()
2024-12-26 00:06:14 +00:00
world:set(D, jecs.Name, "D")
local E = world:component()
world:set(E, jecs.Name, "E")
local e = world:entity()
world:add(e, pair(A, B))
world:add(e, pair(A, C))
2024-12-26 00:06:14 +00:00
world:add(e, pair(A, D))
world:add(e, pair(A, E))
world:add(e, pair(B, C))
world:add(e, pair(B, D))
world:add(e, pair(C, D))
CHECK(pair(A, B) < pair(A, C))
2025-03-24 16:36:13 +00:00
CHECK(pair(A, C) < pair(A, D))
CHECK(pair(C, A) < pair(C, D))
2024-12-26 00:06:14 +00:00
local records = debug_world_inspect(world).records(e)
CHECK(jecs.pair_first(world, pair(B, C)) == B)
2025-03-24 16:36:13 +00:00
local r = jecs.entity_index_try_get(world.entity_index, e)
local archetype = r.archetype
local counts = archetype.counts
CHECK(counts[pair(A, __)] == 4)
CHECK(records[pair(B, C)] > records[pair(A, E)])
CHECK(world:target(e, A, 0) == B)
CHECK(world:target(e, A, 1) == C)
2024-12-26 00:06:14 +00:00
CHECK(world:target(e, A, 2) == D)
CHECK(world:target(e, A, 3) == E)
CHECK(world:target(e, B, 0) == C)
CHECK(world:target(e, B, 1) == D)
CHECK(world:target(e, C, 0) == D)
CHECK(world:target(e, C, 1) == nil)
2025-03-24 16:36:13 +00:00
CHECK(archetype.records[pair(A, B)] == 1)
CHECK(archetype.records[pair(A, C)] == 2)
CHECK(archetype.records[pair(A, D)] == 3)
CHECK(archetype.records[pair(A, E)] == 4)
CHECK(world:target(e, C, 0) == D)
CHECK(world:target(e, C, 1) == nil)
end
do
CASE("infer index when unspecified")
local world = world_new()
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local e = world:entity()
world:add(e, pair(A, B))
world:add(e, pair(A, C))
world:add(e, pair(B, C))
world:add(e, pair(B, D))
world:add(e, pair(C, D))
CHECK(world:target(e, A) == world:target(e, A, 0))
CHECK(world:target(e, B) == world:target(e, B, 0))
CHECK(world:target(e, C) == world:target(e, C, 0))
end
do
CASE("loop until no target")
local world = world_new()
local ROOT = world:entity()
local e1 = world:entity()
local targets = {}
for i = 1, 10 do
local target = world:entity()
targets[i] = target
world:add(e1, pair(ROOT, target))
end
local i = 0
local target = world:target(e1, ROOT, 0)
while target do
i += 1
CHECK(targets[i] == target)
target = world:target(e1, ROOT, i)
end
CHECK(i == 10)
end
end)
2024-08-13 18:08:58 +00:00
TEST("world:contains", function()
local world = jecs.World.new()
2024-08-13 18:08:58 +00:00
local id = world:entity()
CHECK(world:contains(id))
do
CASE("should not exist after delete")
world:delete(id)
CHECK(not world:contains(id))
end
2024-08-13 18:08:58 +00:00
end)
2024-10-12 20:01:08 +00:00
2024-08-11 02:03:18 +00:00
TEST("Hooks", function()
do CASE "OnAdd"
local world = jecs.World.new()
local Transform = world:component()
local e1 = world:entity()
world:set(Transform, jecs.OnAdd, function(entity)
CHECK(e1 == entity)
end)
world:add(e1, Transform)
end
do CASE "OnSet"
local world = jecs.World.new()
local Number = world:component()
local e1 = world:entity()
2025-03-30 19:31:18 +00:00
world:set(Number, jecs.OnChange, function(entity, data)
CHECK(e1 == entity)
2025-03-30 19:31:18 +00:00
CHECK(world:get(entity, Number) == nil)
CHECK(data == 1)
end)
world:set(e1, Number, 1)
end
do CASE("OnRemove")
do
-- basic
local world = jecs.World.new()
2025-01-29 07:28:08 +00:00
local A = world:component() :: Entity<boolean>
local e1 = world:entity()
world:set(A, jecs.OnRemove, function(entity)
CHECK(e1 == entity)
2025-03-13 15:56:02 +00:00
CHECK(world:has(e1, A))
end)
2025-03-13 15:56:02 +00:00
world:add(e1, A)
world:remove(e1, A)
CHECK(not world:has(e1, A))
end
do
-- [BUG] https://github.com/Ukendio/jecs/issues/118
local world = world_new()
local A = world:component()
local B = world:component()
local e = world:entity()
world:set(A, jecs.OnRemove, function(entity)
world:set(entity, B, true)
CHECK(world:get(entity, A))
CHECK(world:get(entity, B))
end)
world:set(e, A, true)
world:remove(e, A)
CHECK(not world:get(e, A))
2025-03-24 16:36:13 +00:00
CHECK(world:get(e, B))
end
end
end)
2025-01-14 10:09:18 +00:00
TEST("change tracking", function()
2025-01-29 07:28:08 +00:00
do CASE "#1"
2025-01-14 10:09:18 +00:00
local world = world_new()
2025-01-29 07:28:08 +00:00
local Foo = world:component() :: Entity<number>
2025-01-14 10:09:18 +00:00
local Previous = jecs.Rest
local q1 = world
:query(Foo)
:without(pair(Previous, Foo))
:cached()
local e1 = world:entity()
world:set(e1, Foo, 1)
local e2 = world:entity()
world:set(e2, Foo, 2)
local i = 0
2025-01-29 07:28:08 +00:00
for e, new in q1 :: any do
2025-01-14 10:09:18 +00:00
i += 1
world:set(e, pair(Previous, Foo), new)
end
CHECK(i == 2)
local j = 0
2025-01-29 07:28:08 +00:00
for e, new in q1 :: any do
2025-01-14 10:09:18 +00:00
j += 1
world:set(e, pair(Previous, Foo), new)
end
CHECK(j == 0)
end
2025-01-29 07:28:08 +00:00
do CASE "#2"
2025-01-14 10:09:18 +00:00
local world = world_new()
2025-01-29 07:28:08 +00:00
local component = world:component() :: Entity<number>
2025-01-14 10:09:18 +00:00
local tag = world:entity()
local previous = jecs.Rest
local q1 = world:query(component):without(pair(previous, component), tag):cached()
local testEntity = world:entity()
world:set(testEntity, component, 10)
local i = 0
2025-01-29 07:28:08 +00:00
for entity, number in q1 :: any do
2025-01-14 10:09:18 +00:00
i += 1
2025-01-16 08:02:21 +00:00
world:add(testEntity, tag)
2025-01-14 10:09:18 +00:00
end
CHECK(i == 1)
2025-01-29 07:28:08 +00:00
for e, n in q1 :: any do
2025-01-16 08:02:21 +00:00
world:set(e, pair(previous, component), n)
2025-01-14 10:09:18 +00:00
end
end
end)
TEST("repro", function()
do CASE "#1"
local world = world_new()
local reproEntity = world:component()
2025-03-24 13:31:56 +00:00
local components = { Cooldown = world:component() :: jecs.Entity<number> }
world:set(reproEntity, components.Cooldown, 2)
local function updateCooldowns(dt: number)
local toRemove = {}
2025-03-24 13:31:56 +00:00
local it = world:query(components.Cooldown):iter()
for id, cooldown in it do
cooldown -= dt
if cooldown <= 0 then
table.insert(toRemove, id)
-- world:remove(id, components.Cooldown)
else
world:set(id, components.Cooldown, cooldown)
end
end
for _, id in toRemove do
world:remove(id, components.Cooldown)
CHECK(not world:get(id, components.Cooldown))
end
end
updateCooldowns(1.5)
updateCooldowns(1.5)
end
2025-01-29 07:28:08 +00:00
do CASE "#2" -- ISSUE #171
local world = world_new()
local component1 = world:component()
local tag1 = world:entity()
local query = world:query(component1):with(tag1):cached()
local entity = world:entity()
world:set(entity, component1, "some data")
local counter = 0
for x in query:iter() do
counter += 1
end
CHECK(counter == 0)
end
2024-08-11 02:03:18 +00:00
end)
2025-01-16 08:02:21 +00:00
TEST("wildcard query", function()
do CASE "#1"
local world = world_new()
local pair = jecs.pair
local Relation = world:entity()
local Wildcard = jecs.Wildcard
local A = world:entity()
local relationship = pair(Relation, Wildcard)
local query = world:query(relationship):cached()
local entity = world:entity()
2025-03-24 16:36:13 +00:00
local p = pair(Relation, A)
CHECK(jecs.pair_first(world, p) == Relation)
CHECK(jecs.pair_second(world, p) == A)
local w = dwi(world)
2025-01-16 08:02:21 +00:00
world:add(entity, pair(Relation, A))
local counter = 0
for e in query:iter() do
counter += 1
end
CHECK(counter == 1)
end
do CASE "#2"
local world = world_new()
local pair = jecs.pair
local Relation = world:entity()
local Wildcard = jecs.Wildcard
local A = world:entity()
local relationship = pair(Relation, Wildcard)
local entity = world:entity()
world:add(entity, pair(Relation, A))
local counter = 0
for e in world:query(relationship):iter() do
counter += 1
end
CHECK(counter == 1)
end
do CASE "#3"
local world = world_new()
local pair = jecs.pair
local Relation = world:entity()
local Wildcard = jecs.Wildcard
local A = world:entity()
local entity = world:entity()
world:add(entity, pair(Relation, A))
local relationship = pair(Relation, Wildcard)
local query = world:query(relationship):cached()
local counter = 0
for e in query:iter() do
counter += 1
end
CHECK(counter == 1)
end
end)
TEST("world:delete() invokes OnRemove hook", function()
do CASE "#1"
local world = world_new()
2025-01-29 07:28:08 +00:00
local A = world:entity()
local entity = world:entity()
local called = false
world:set(A, jecs.OnRemove, function(e)
called = true
end)
world:add(entity, A)
world:delete(entity)
CHECK(called)
end
do CASE "#2"
local world = world_new()
local pair = jecs.pair
local Relation = world:entity()
local A = world:entity()
local B = world:entity()
world:add(Relation, pair(jecs.OnDelete, jecs.Delete))
local entity = world:entity()
local called = false
world:set(A, jecs.OnRemove, function(e)
called = true
end)
world:add(entity, A)
world:add(entity, pair(Relation, B))
2025-01-29 07:28:08 +00:00
world:delete(B)
CHECK(called)
end
do CASE "#3"
local world = world_new()
local pair = jecs.pair
local viewingContainer = world:entity()
local character = world:entity()
local container = world:entity()
local called = false
world:set(viewingContainer, jecs.OnRemove, function(e)
called = true
end)
world:add(character, pair(viewingContainer, container))
world:delete(container)
CHECK(called)
end
end)
FINISH()