jecs/test/tests.luau
2025-04-13 05:21:41 +02:00

1757 lines
39 KiB
Text

local jecs = require("@jecs")
local testkit = require("@testkit")
local BENCH, START = testkit.benchmark()
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
local pair = jecs.pair
local ecs_pair_first = jecs.pair_first
local ecs_pair_second = jecs.pair_second
local ChildOf = jecs.ChildOf
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
type World = jecs.World
type Entity<T=nil> = jecs.Entity<T>
type Id<T=unknown> = jecs.Id<T>
local entity_visualiser = require("@tools/entity_visualiser")
local dwi = entity_visualiser.stringify
TEST("world:add()", function()
do CASE "idempotent"
local world = jecs.world()
local d = dwi(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")
local world = jecs.World.new()
local d = dwi(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)
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)
TEST("world:children()", function()
local world = jecs.world()
local C = world:component()
local T = world:entity()
local e1 = world:entity()
world:set(e1, C, true)
local e2 = world:entity()
world:add(e2, T)
world:add(e2, pair(ChildOf, e1))
local e3 = world:entity()
world:add(e3, pair(ChildOf, e1))
local count = 0
for entity 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)
TEST("world:clear()", function()
do CASE "should remove its components"
local world = jecs.world()
local A = world:component()
local B = world:component()
local e = world:entity()
local e1 = world:entity()
local _e2 = world:entity()
world:set(e, A, true)
world:set(e, B, true)
world:set(e1, A, true)
world:set(e1, B, true)
CHECK(world:get(e, A))
CHECK(world:get(e, B))
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
do CASE "remove cleared ID from entities"
local world = jecs.world()
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
end)
TEST("world:component()", function()
do CASE "only components should have EcsComponent trait"
local world = jecs.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()
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)
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
end)
TEST("world:contains()", function()
local world = jecs.world()
local id = world:entity()
CHECK(world:contains(id))
do
CASE("should not exist after delete")
world:delete(id)
CHECK(not world:contains(id))
end
end)
TEST("world:delete()", function()
do CASE "remove pair when relationship is deleted"
local world = jecs.world()
local e1 = world:entity()
local e2 = world:entity()
local A = world:component()
local B = world:component()
local C = world:component()
world:add(e1, pair(A, e2))
world:add(e1, pair(B, e2))
world:add(e1, pair(C, e2))
world:delete(A)
CHECK(not world:has(e1, pair(A, e2)))
CHECK(world:has(e1, pair(B, e2)))
CHECK(world:has(e1, pair(C, e2)))
end
do CASE "invoke OnRemove hook on all components of deleted entity"
local world = jecs.world()
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
do CASE "invoke OnRemove hook on relationship if target was deleted"
local world = jecs.world()
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))
world:delete(B)
CHECK(called)
end
do CASE "delete recycled entity id used as component"
local world = jecs.world()
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 = jecs.world()
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
do CASE "should allow deleting components"
local world = jecs.world()
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
do CASE "delete entities using another Entity as component with Delete cleanup action"
local world = jecs.world()
local Health = world:entity()
world:add(Health, pair(jecs.OnDelete, jecs.Delete))
local Poison = world:component()
local id = world:entity()
world:set(id, Poison, 5)
CHECK_EXPECT_ERR(function()
world:set(id, Health, 50 :: any)
end)
local id1 = world:entity()
world:set(id1, Poison, 500)
CHECK_EXPECT_ERR(function()
world:set(id1, Health, 50 :: any)
end)
CHECK(world:has(id, Poison, Health))
CHECK(world:has(id1, Poison, Health))
world:delete(Poison)
CHECK(world:contains(id))
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))
end
do CASE "delete children"
local world = jecs.world()
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()
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)
end)
for i, child in children do
CHECK(not world:contains(child))
CHECK(not world:has(child, pair(jecs.ChildOf, e)))
CHECK(not world:has(child, Health))
end
e = world:entity()
local friends = {}
for i = 1, 10 do
local friend = world:entity()
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)
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
do CASE "remove deleted ID from entities"
local world = jecs.world()
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()
local entities = {}
local Health = world:component()
local Poison = world:component()
for i = 1, 100 do
local child = world:entity()
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
local e = entities[i]
world:delete(e)
end
end)
for _, entity in entities do
CHECK(not world:contains(entity))
end
end
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()
world:add(bob, pair(Likes, alice))
world:add(alice, pair(Likes, bob))
world:delete(bob)
CHECK(not world:contains(bob))
CHECK(not world:contains(alice))
end
end)
TEST("world:each()", function()
local world = jecs.world()
local A = world:component()
local B = world:component()
local C = world:component()
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)
for entity in world:each(A) do
if entity == e1 or entity == e2 or entity == e3 then
CHECK(true)
continue
end
CHECK(false)
end
end)
TEST("world:entity()", function()
local N = 2^8
do CASE "unique IDs"
local world = jecs.world()
local set = {}
for i = 1, N do
local e = world:entity()
CHECK(not set[e])
set[e] = true
end
end
do CASE "generations"
local world = jecs.world()
local e = world:entity() :: any
CHECK(ECS_ID(e) == 1 + jecs.Rest :: any)
CHECK(ECS_GENERATION(e) == 0) -- 0
e = ECS_GENERATION_INC(e)
CHECK(ECS_GENERATION(e) == 1) -- 1
end
do CASE "pairs"
local world = jecs.world()
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)
CHECK(ecs_pair_first(world, p) == e2)
CHECK(ecs_pair_second(world, p) == e3)
world:delete(e2)
local e2v2 = world:entity() :: any
CHECK(IS_PAIR(e2v2) == false)
CHECK(IS_PAIR(pair(e2v2, e3) :: any) == true)
end
do CASE "Recycling"
local world = jecs.world()
local e = world:entity()
world:delete(e)
local e1 = world:entity()
world:delete(e1)
local e2 = world:entity()
CHECK(ECS_ID(e2 :: any) :: any == e)
CHECK(ECS_GENERATION(e2 :: any) == 2)
CHECK(world:contains(e2))
CHECK(not world:contains(e1))
CHECK(not world:contains(e))
end
do CASE "Recycling max generation"
local world = jecs.world()
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() :: any
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
end)
TEST("world:has()", function()
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
do CASE "should return false when missing one tag"
local world = jecs.World.new()
local A = world:entity()
local B = world:entity()
local C = world:entity()
local D = world:entity()
local e = world:entity()
world:add(e, A)
world:add(e, C)
world:add(e, D)
CHECK(world:has(e, A, B, C, D) == false)
end
end)
TEST("world:query()", function()
local N = 2^8
do CASE "cached"
local world = jecs.world()
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
local iter = 0
for _, e in q:iter() do
iter += 1
i=1
end
CHECK (iter == 1)
CHECK(i == 1)
for _, e in q:iter() do
i=2
end
CHECK(i == 2)
for _, e in q :: any do
i=3
end
CHECK(i == 3)
for _, e in q :: any do
i=4
end
CHECK(i == 4)
CHECK(#q:archetypes() == 1)
CHECK(not table.find(q:archetypes(), world.archetype_index[table.concat({Foo, Bar, Baz}, "_")]))
world:delete(Foo)
CHECK(#q:archetypes() == 0)
end
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
do CASE "tag"
local world = jecs.World.new()
local A = world:entity()
local e = world:entity()
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
CHECK(count == 1)
end
do CASE "pairs"
local world = jecs.World.new()
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()
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)
CHECK_EXPECT_ERR(function()
world:set(e, pair(T1, T2), true :: any)
end)
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 "iterate wildcard pairs in cached query"
local world = jecs.world()
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()
local p = pair(Relation, A)
CHECK(jecs.pair_first(world, p) == Relation)
CHECK(jecs.pair_second(world, p) == A)
world:add(entity, pair(Relation, A))
local counter = 0
for e in query:iter() do
counter += 1
end
CHECK(counter == 1)
end
do CASE "iterate wildcard pairs in uncached query"
local world = jecs.world()
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 "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
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()
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
for _ in q :: any do
i += 1
end
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()
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
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()
local components = {}
for i = 1, 9 do
local id = world:component()
components[i] = id
end
local e1 = world:entity()
for i, id in components do
world:set(e1, id, 13 ^ i)
end
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()
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()
local e, data = it()
while e do
if e == eA then
CHECK(data)
elseif e == eAB then
CHECK(data)
else
CHECK(false)
end
e, data = it()
end
CHECK(true)
end
do CASE "should query all matching entities when irrelevant component is removed"
local world = jecs.world()
local A = world:component()
local B = 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
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
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()
world:set(bob, pair(Eats, Apples), true)
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()
world:set(bob, pair(Eats, Apples), "bob eats apples")
local w = jecs.Wildcard
for e, data in world:query(pair(Eats, w)) :: any do
CHECK(e == bob)
CHECK(data == "bob eats apples")
end
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()
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
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
for e, data in world:query(pair(w, Apples)) :: any do
count += 1
CHECK(data == "bob eats apples")
end
CHECK(count == 1)
end
do CASE "should only relate alive entities"
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:component()
local Oranges = world:component()
local bob = world:entity()
local alice = world:entity()
world:set(bob, Apples, "apples")
world:set(bob, pair(Eats, Apples), "bob eats apples")
world:set(alice, pair(Eats, Oranges), "alice eats oranges")
world:delete(Apples)
local Wildcard = jecs.Wildcard
local count = 0
for _, data in world:query(pair(Wildcard, Apples)) :: any do
count += 1
end
world:delete(pair(Eats, Apples))
CHECK(count == 0)
CHECK(world:get(bob, pair(Eats, Apples)) == nil)
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)
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()
local ChildOf = jecs.ChildOf
local Name = world:component()
local bob = world:entity()
local alice = world:entity()
local sara = world:entity()
world:add(bob, pair(ChildOf, alice))
world:set(bob, Name, "bob")
world:add(sara, pair(ChildOf, alice))
world:set(sara, Name, "sara")
CHECK(world:parent(bob) == alice) -- O(1)
local count = 0
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()
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
for id in world:query(A) :: any do
world:clear(id)
count += 1
end
CHECK(count == 2)
end
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
for _ in world:query(pair(Hello, Bob)):without(Bob) :: any do
withoutCount += 1
end
CHECK(withoutCount == 0)
end
do CASE "world:query():without()"
-- REGRESSION TEST
local world = jecs.World.new()
local _1, _2, _3 = world:component(), world:component(), world:component()
local counter = 0
for e, a, b in world:query(_1, _2):without(_3) :: any do
counter += 1
end
CHECK(counter == 0)
end
end)
TEST("world:remove()", function()
do
CASE("should allow remove a component that doesn't exist on entity")
local world = jecs.World.new()
local Health = world:component()
local Poison = world:component()
local id = world:entity()
do
world:remove(id, Poison)
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
end)
TEST("world:set()", function()
do CASE "archetype move"
local world = jecs.world()
local d = dwi(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
CHECK(world.archetype_index[oldArchetype].columns[_1 :: any][oldRow] == nil)
end
do CASE "pairs"
local world = jecs.world()
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)
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()
CHECK_EXPECT_ERR(function()
world:set(e2, pair(jecs.ChildOf, e), true :: any)
end)
CHECK(not world:get(e2, pair(jecs.ChildOf, e)))
end
end)
TEST("world:target", function()
do CASE "nth index"
local world = jecs.world()
local A = world:component()
world:set(A, jecs.Name, "A")
local B = world:component()
world:set(B, jecs.Name, "B")
local C = world:component()
world:set(C, jecs.Name, "C")
local D = world:component()
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))
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) :: any) < (pair(A, C) :: any))
CHECK((pair(A, C) :: any) < (pair(A, D) :: any))
CHECK((pair(C, A) :: any) < (pair(C, D) :: any))
CHECK(jecs.pair_first(world, pair(B, C)) == B)
local r = (jecs.entity_index_try_get(world.entity_index :: any, e :: any) :: any) :: jecs.Record
local archetype = r.archetype
local records = archetype.records
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)
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)
CHECK(archetype.records[pair(A, B):: any] == 1)
CHECK(archetype.records[pair(A, C):: any] == 2)
CHECK(archetype.records[pair(A, D):: any] == 3)
CHECK(archetype.records[pair(A, E):: any] == 4)
CHECK(world:target(e, C, 0) == D)
CHECK(world:target(e, C, 1) == nil)
end
do CASE "infer index when unspecified"
local world = jecs.world()
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 = jecs.world()
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)
TEST("#adding a recycled target", function()
local world = jecs.world()
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 :: any)
end)
TEST("#repro2", function()
local world = jecs.world()
local Lifetime = world:component() :: 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)
world:set(entity, pair(4 :: any, 5 :: any), 6) -- noise
-- entity_visualizer.components(world, entity)
for e in world:each(pair(Lifetime, __)) do
local i = 0
local nth = world:target(e, Lifetime, i)
while nth do
-- entity_visualizer.components(world, e)
local data = world:get(e, pair(Lifetime, nth)) :: number
data -= 1
if data <= 0 then
world:remove(e, pair(Lifetime, nth))
else
world:set(e, pair(Lifetime, nth), data)
end
i += 1
nth = world:target(e, Lifetime, i)
end
end
CHECK(not world:has(entity, pair(Lifetime, Particle)))
CHECK(world:get(entity, pair(Lifetime, Beam)) == 1)
end)
TEST("another", function()
local world = jecs.world()
-- world = lifetime_tracker_add(world, {padding_enabled=false})
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 :: any)
CHECK(jecs.pair_second(world, e2_e3) == e3)
CHECK_EXPECT_ERR(function()
world:add(e1, pair(e2, e3))
end)
end)
TEST("#repro", function()
local world = jecs.world()
local function getTargets(relation)
local tgts = {}
local pairwildcard = pair(relation, jecs.Wildcard)
for _, archetype in world:query(pairwildcard):archetypes() do
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] :: any)
table.insert(tgts, tgt)
end
end
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)
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
end)
TEST("Hooks", function()
do CASE "OnAdd"
local world = jecs.world()
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()
local Number = world:component()
local e1 = world:entity()
local call = 0
world:set(Number, jecs.OnChange, function(entity, data)
CHECK(e1 == entity)
if call == 1 then
CHECK(false)
elseif call == 2 then
CHECK(world:get(entity, Number) == data)
end
CHECK(data == 1)
end)
call = 1
world:set(e1, Number, 1)
call = 2
world:set(e1, Number, 1)
end
do CASE("OnRemove")
do
-- basic
local world = jecs.world()
local A = world:component() :: Id<boolean>
local e1 = world:entity()
world:set(A, jecs.OnRemove, function(entity)
CHECK(e1 == entity)
CHECK(world:has(e1, A))
end)
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 = jecs.world()
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))
CHECK(world:get(e, B))
end
end
end)
TEST("change tracking", function()
do CASE "#1"
local world = jecs.world()
local Foo = world:component() :: Id<number>
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
for e, new in q1 :: any do
i += 1
world:set(e, pair(Previous, Foo), new)
end
CHECK(i == 2)
local j = 0
for e, new in q1 :: any do
j += 1
world:set(e, pair(Previous, Foo), new)
end
CHECK(j == 0)
end
do CASE "#2"
local world = jecs.world()
local component = world:component() :: Id<number>
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
for entity, number in q1 :: any do
i += 1
world:add(testEntity, tag)
end
CHECK(i == 1)
for e, n in q1 :: any do
world:set(e, pair(previous, component), n)
end
end
end)
TEST("repro", function()
do CASE "#1"
local world = jecs.world()
local reproEntity = world:component()
local components = { Cooldown = world:component() :: Id<number> }
world:set(reproEntity, components.Cooldown, 2)
local function updateCooldowns(dt: number)
local toRemove = {}
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
do CASE "#2" -- ISSUE #171
local world = jecs.world()
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
end)
TEST("wildcard query", function()
do CASE "#1"
local world = jecs.world()
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()
local p = pair(Relation, A)
CHECK(jecs.pair_first(world, p) == Relation)
CHECK(jecs.pair_second(world, p) == A)
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 = jecs.world()
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 = jecs.world()
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)
return FINISH()