mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-25 09:30:03 +00:00
Unit tests
This commit is contained in:
parent
ff54fb3d62
commit
0f7cb78a59
2 changed files with 519 additions and 360 deletions
|
@ -283,7 +283,7 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean
|
||||||
return first == EcsWildcard or second == EcsWildcard
|
return first == EcsWildcard or second == EcsWildcard
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetype_of(world: any, types: { i24 }, prev: Archetype?): Archetype
|
local function archetype_create(world: any, types: { i24 }, prev: Archetype?): Archetype
|
||||||
local ty = hash(types)
|
local ty = hash(types)
|
||||||
|
|
||||||
local id = world.nextArchetypeId + 1
|
local id = world.nextArchetypeId + 1
|
||||||
|
@ -393,7 +393,7 @@ local function archetype_ensure(world: World, types, prev): Archetype
|
||||||
return archetype
|
return archetype
|
||||||
end
|
end
|
||||||
|
|
||||||
return archetype_of(world, types, prev)
|
return archetype_create(world, types, prev)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function find_insert(types: { i53 }, toAdd: i53): number
|
local function find_insert(types: { i53 }, toAdd: i53): number
|
||||||
|
@ -841,15 +841,18 @@ do
|
||||||
end
|
end
|
||||||
compatible_archetypes[last] = nil
|
compatible_archetypes[last] = nil
|
||||||
column_indices[last] = nil
|
column_indices[last] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
lastArchetype = 1
|
|
||||||
archetype = compatible_archetypes[lastArchetype]
|
archetype = compatible_archetypes[lastArchetype]
|
||||||
|
if not archetype then
|
||||||
|
return EmptyQuery
|
||||||
|
end
|
||||||
|
|
||||||
if not archetype then
|
entities = archetype.entities
|
||||||
return EmptyQuery
|
columns = archetype.columns
|
||||||
end
|
tr = column_indices[lastArchetype]
|
||||||
|
i = #entities
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
@ -916,14 +919,26 @@ do
|
||||||
end
|
end
|
||||||
|
|
||||||
if shouldRemove then
|
if shouldRemove then
|
||||||
table.remove(compatible_archetypes, i)
|
local last = #compatible_archetypes
|
||||||
|
if last ~= i then
|
||||||
|
compatible_archetypes[i] = compatible_archetypes[last]
|
||||||
|
column_indices[i] = column_indices[last]
|
||||||
|
end
|
||||||
|
compatible_archetypes[last] = nil
|
||||||
|
column_indices[last] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if #compatible_archetypes == 0 then
|
archetype = compatible_archetypes[lastArchetype]
|
||||||
|
if not archetype then
|
||||||
return EmptyQuery
|
return EmptyQuery
|
||||||
end
|
end
|
||||||
|
|
||||||
|
entities = archetype.entities
|
||||||
|
columns = archetype.columns
|
||||||
|
tr = column_indices[lastArchetype]
|
||||||
|
i = #entities
|
||||||
|
|
||||||
return query
|
return query
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1190,7 +1205,7 @@ function World.new()
|
||||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
||||||
}, World)
|
}, World)
|
||||||
|
|
||||||
self.ROOT_ARCHETYPE = archetype_of(self, {})
|
self.ROOT_ARCHETYPE = archetype_create(self, {})
|
||||||
|
|
||||||
for i = HI_COMPONENT_ID + 1, EcsRest do
|
for i = HI_COMPONENT_ID + 1, EcsRest do
|
||||||
-- Initialize built-in components
|
-- Initialize built-in components
|
||||||
|
@ -1210,7 +1225,7 @@ return {
|
||||||
Component = EcsComponent,
|
Component = EcsComponent,
|
||||||
Wildcard = EcsWildcard :: Entity,
|
Wildcard = EcsWildcard :: Entity,
|
||||||
w = EcsWildcard :: Entity,
|
w = EcsWildcard :: Entity,
|
||||||
Rest = EcsRest,
|
Rest = EcsRest :: Entity,
|
||||||
|
|
||||||
pair = (ECS_PAIR :: any) :: <R, T>(pred: Entity, obj: Entity) -> number,
|
pair = (ECS_PAIR :: any) :: <R, T>(pred: Entity, obj: Entity) -> number,
|
||||||
|
|
||||||
|
|
842
test/tests.luau
842
test/tests.luau
|
@ -20,11 +20,137 @@ local function CHECK_NO_ERR<T...>(s: string, fn: (T...) -> (), ...: T...)
|
||||||
CHECK(msg == s, 2)
|
CHECK(msg == s, 2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local N = 10
|
local N = 2^8
|
||||||
|
|
||||||
type World = jecs.WorldShim
|
type World = jecs.WorldShim
|
||||||
|
|
||||||
TEST("world", function()
|
local function debug_world_inspect(world)
|
||||||
|
local function record(e)
|
||||||
|
return world.entityIndex.sparse[e]
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
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"
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local e = world:entity()
|
||||||
|
CHECK(ECS_ID(e) == 1 + jecs.Rest)
|
||||||
|
CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e)
|
||||||
|
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.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 pair = pair(e2, e3)
|
||||||
|
CHECK(IS_PAIR(pair) == true)
|
||||||
|
|
||||||
|
CHECK(ecs_pair_relation(world.entityIndex, pair) == e2)
|
||||||
|
CHECK(ecs_pair_object(world.entityIndex, pair) == e3)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
TEST("world:set()", function()
|
||||||
|
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
|
||||||
|
CHECK(world.archetypeIndex[oldArchetype].columns[_1][oldRow] == nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "arbitrary order"
|
||||||
|
local world = jecs.World.new()
|
||||||
|
|
||||||
|
local Health = world:entity()
|
||||||
|
local Poison = world:component()
|
||||||
|
|
||||||
|
local id = world:entity()
|
||||||
|
world:set(id, Poison, 5)
|
||||||
|
world:set(id, Health, 50)
|
||||||
|
|
||||||
|
CHECK(world:get(id, Poison) == 5)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
TEST("world:remove()", function()
|
||||||
do CASE "should allow remove a component that doesn't exist on entity"
|
do CASE "should allow remove a component that doesn't exist on entity"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
|
||||||
|
@ -43,117 +169,143 @@ TEST("world", function()
|
||||||
CHECK(world:get(id, Poison) == nil)
|
CHECK(world:get(id, Poison) == nil)
|
||||||
CHECK(world:get(id, Health) == 50)
|
CHECK(world:get(id, Health) == 50)
|
||||||
end
|
end
|
||||||
do CASE("should find every component id")
|
end)
|
||||||
local world = jecs.World.new() :: World
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
world:entity()
|
|
||||||
world:entity()
|
|
||||||
world:entity()
|
|
||||||
|
|
||||||
local count = 0
|
TEST("world:add()", function()
|
||||||
for componentId in world:query(jecs.Component) do
|
do CASE "idempotent"
|
||||||
if componentId ~= A and componentId ~= B then
|
local world = jecs.World.new()
|
||||||
error("found entity")
|
local d = debug_world_inspect(world)
|
||||||
end
|
local _1, _2 = world:component(), world:component()
|
||||||
count += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
CHECK(count == 2)
|
local e = world:entity()
|
||||||
end
|
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("should remove its components")
|
do CASE "archetype move"
|
||||||
local world = jecs.World.new() :: World
|
do
|
||||||
local A = world:component()
|
local world = jecs.World.new()
|
||||||
local B = world:component()
|
|
||||||
|
|
||||||
local e = world:entity()
|
local d = debug_world_inspect(world)
|
||||||
|
|
||||||
world:set(e, A, true)
|
local _1 = world:component()
|
||||||
world:set(e, B, true)
|
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)
|
||||||
|
|
||||||
CHECK(world:get(e, A))
|
local archetypes = #world.archetypes
|
||||||
CHECK(world:get(e, B))
|
-- This should create a new archetype
|
||||||
|
world:add(e, _1)
|
||||||
|
CHECK(#world.archetypes == archetypes + 1)
|
||||||
|
|
||||||
world:clear(e)
|
CHECK(d.archetype(e) == "1")
|
||||||
CHECK(world:get(e, A) == nil)
|
CHECK(d.tbl(e))
|
||||||
CHECK(world:get(e, B) == nil)
|
end
|
||||||
end
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
do CASE("should drain query while iterating")
|
|
||||||
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)
|
|
||||||
|
|
||||||
local q = world:query(A)
|
TEST("world:query()", function()
|
||||||
|
do CASE "query single component"
|
||||||
|
do
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local A = world:component()
|
||||||
|
local B = world:component()
|
||||||
|
|
||||||
local i = 0
|
local entities = {}
|
||||||
local j = 0
|
for i = 1, N do
|
||||||
for _ in q do
|
local id = world:entity()
|
||||||
i+=1
|
|
||||||
end
|
|
||||||
for _ in q do
|
|
||||||
j+=1
|
|
||||||
end
|
|
||||||
CHECK(i == 2)
|
|
||||||
CHECK(j == 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE("should be able to get next results")
|
world:set(id, A, true)
|
||||||
local world = jecs.World.new() :: World
|
if i > 5 then
|
||||||
world:component()
|
world:set(id, B, true)
|
||||||
local A = world:component()
|
end
|
||||||
local B = world:component()
|
entities[i] = id
|
||||||
local eA = world:entity()
|
end
|
||||||
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 q = world:query(A)
|
for id in world:query(A) do
|
||||||
|
table.remove(entities, CHECK(table.find(entities, id)))
|
||||||
|
end
|
||||||
|
|
||||||
local e, data = q:next()
|
CHECK(#entities == 0)
|
||||||
while e do
|
end
|
||||||
CHECK(
|
|
||||||
if e == eA then data == true
|
|
||||||
elseif e == eAB then data == true
|
|
||||||
else false
|
|
||||||
)
|
|
||||||
e, data = q:next()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE("should query all matching entities")
|
do
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new() :: World
|
||||||
local A = world:component()
|
local A = world:component()
|
||||||
local B = 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 entities = {}
|
local q = world:query(A)
|
||||||
for i = 1, N do
|
|
||||||
local id = world:entity()
|
|
||||||
|
|
||||||
world:set(id, A, true)
|
local i = 0
|
||||||
if i > 5 then
|
local j = 0
|
||||||
world:set(id, B, true)
|
for _ in q do
|
||||||
end
|
i+=1
|
||||||
entities[i] = id
|
end
|
||||||
end
|
for _ in q do
|
||||||
|
j+=1
|
||||||
|
end
|
||||||
|
CHECK(i == 2)
|
||||||
|
CHECK(j == 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
for id in world:query(A) do
|
do CASE "query missing component"
|
||||||
table.remove(entities, CHECK(table.find(entities, id)))
|
local world = jecs.World.new()
|
||||||
end
|
local A = world:component()
|
||||||
|
local B = world:component()
|
||||||
|
local C = world:component()
|
||||||
|
|
||||||
CHECK(#entities == 0)
|
local e1 = world:entity()
|
||||||
end
|
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) do
|
||||||
|
counter += 1
|
||||||
|
end
|
||||||
|
CHECK(counter == 0)
|
||||||
|
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 q = world:query(A)
|
||||||
|
|
||||||
|
local e, data = q:next()
|
||||||
|
while e do
|
||||||
|
if e ~= eA and e ~= eAB then
|
||||||
|
CHECK(false)
|
||||||
|
end
|
||||||
|
e, data = q:next()
|
||||||
|
end
|
||||||
|
CHECK(true)
|
||||||
|
end
|
||||||
|
|
||||||
do CASE("should query all matching entities when irrelevant component is removed")
|
do CASE("should query all matching entities when irrelevant component is removed")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
@ -208,65 +360,6 @@ TEST("world", function()
|
||||||
CHECK(#entities == 0)
|
CHECK(#entities == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE("should allow setting components in arbitrary order")
|
|
||||||
local world = jecs.World.new()
|
|
||||||
|
|
||||||
local Health = world:entity()
|
|
||||||
local Poison = world:component()
|
|
||||||
|
|
||||||
local id = world:entity()
|
|
||||||
world:set(id, Poison, 5)
|
|
||||||
world:set(id, Health, 50)
|
|
||||||
|
|
||||||
CHECK(world:get(id, Poison) == 5)
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE("should allow deleting components")
|
|
||||||
local world = jecs.World.new()
|
|
||||||
|
|
||||||
local Health = world:entity()
|
|
||||||
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(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("should increment generation")
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local e = world:entity()
|
|
||||||
CHECK(ECS_ID(e) == 1 + jecs.Rest)
|
|
||||||
CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e)
|
|
||||||
CHECK(ECS_GENERATION(e) == 0) -- 0
|
|
||||||
e = ECS_GENERATION_INC(e)
|
|
||||||
CHECK(ECS_GENERATION(e) == 1) -- 1
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE("should get alive from index in the dense array")
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local _e = world:entity()
|
|
||||||
local e2 = world:entity()
|
|
||||||
local e3 = world:entity()
|
|
||||||
|
|
||||||
CHECK(IS_PAIR(world:entity()) == false)
|
|
||||||
|
|
||||||
local pair = pair(e2, e3)
|
|
||||||
CHECK(IS_PAIR(pair) == true)
|
|
||||||
|
|
||||||
CHECK(ecs_pair_relation(world.entityIndex, pair) == e2)
|
|
||||||
CHECK(ecs_pair_object(world.entityIndex, pair) == e3)
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE("should allow querying for relations")
|
do CASE("should allow querying for relations")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local Eats = world:entity()
|
local Eats = world:entity()
|
||||||
|
@ -392,129 +485,50 @@ TEST("world", function()
|
||||||
CHECK(count == 2)
|
CHECK(count == 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should be able to add/remove matching entity during iteration"
|
do CASE "iterator invalidation"
|
||||||
local world = jecs.World.new()
|
do CASE "adding"
|
||||||
local Name = world:component()
|
SKIP()
|
||||||
for i = 1, 5 do
|
local world = jecs.World.new()
|
||||||
local e = world:entity()
|
local A = world:component()
|
||||||
world:set(e, Name, tostring(e))
|
local B = world:component()
|
||||||
end
|
|
||||||
local count = 0
|
|
||||||
for id, name in world:query(Name) do
|
|
||||||
count += 1
|
|
||||||
CHECK(id == tonumber(name))
|
|
||||||
|
|
||||||
world:remove(id, Name)
|
local e1 = world:entity()
|
||||||
local e = world:entity()
|
local e2 = world:entity()
|
||||||
world:set(e, Name, tostring(e))
|
world:add(e1, A)
|
||||||
end
|
world:add(e2, A)
|
||||||
CHECK(count == 5)
|
world:add(e2, B)
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "should allow adding a matching entity during iteration"
|
local count = 0
|
||||||
local world = jecs.World.new()
|
for id in world:query(A) do
|
||||||
local A = world:component()
|
world:add(id, B)
|
||||||
local B = world:component()
|
|
||||||
|
|
||||||
local e1 = world:entity()
|
count += 1
|
||||||
local e2 = world:entity()
|
end
|
||||||
world:add(e1, A)
|
|
||||||
world:add(e2, A)
|
|
||||||
world:add(e2, B)
|
|
||||||
|
|
||||||
local count = 0
|
CHECK(count == 2)
|
||||||
for id in world:query(A) do
|
|
||||||
local e = world:entity()
|
|
||||||
world:add(e, A)
|
|
||||||
world:add(e, B)
|
|
||||||
count += 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
CHECK(count == 3)
|
do CASE "spawning"
|
||||||
end
|
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)
|
||||||
|
|
||||||
do CASE "should not iterate same entity when adding component"
|
local count = 0
|
||||||
SKIP()
|
for id in world:query(A) do
|
||||||
local world = jecs.World.new()
|
local e = world:entity()
|
||||||
local A = world:component()
|
world:add(e, A)
|
||||||
local B = world:component()
|
world:add(e, B)
|
||||||
|
count += 1
|
||||||
|
end
|
||||||
|
|
||||||
local e1 = world:entity()
|
CHECK(count == 3)
|
||||||
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) do
|
|
||||||
world:add(id, B)
|
|
||||||
|
|
||||||
count += 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
CHECK(count == 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "should replace component data"
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
local C = world:component()
|
|
||||||
|
|
||||||
local e = world:entity()
|
|
||||||
world:set(e, A, 1)
|
|
||||||
world:set(e, B, true)
|
|
||||||
world:set(e, C, "hello ")
|
|
||||||
|
|
||||||
world:query(A, B, C):replace(function(a, b, c)
|
|
||||||
return a * 2, not b, c.."world"
|
|
||||||
end)
|
|
||||||
|
|
||||||
CHECK(world:get(e, A) == 2)
|
|
||||||
CHECK(world:get(e, B) == false)
|
|
||||||
CHECK(world:get(e, C) == "hello world")
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "should not iterate when nothing matches query"
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
|
|
||||||
local e1 = world:entity()
|
|
||||||
world:add(e1, A)
|
|
||||||
|
|
||||||
local count = 0
|
|
||||||
for id in world:query(B) do
|
|
||||||
count += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
CHECK(count == 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "should return nothing for empty iteration"
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
|
|
||||||
local e1 = world:entity()
|
|
||||||
world:add(e1, A)
|
|
||||||
|
|
||||||
local query = world:query(B)
|
|
||||||
CHECK(query.next() == nil)
|
|
||||||
CHECK(query.replace() == nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "should properly handle query:without for empty iteration"
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
|
|
||||||
local e1 = world:entity()
|
|
||||||
world:add(e1, A)
|
|
||||||
|
|
||||||
local query = world:query(B)
|
|
||||||
CHECK(query == query:without())
|
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should not find any entities"
|
do CASE "should not find any entities"
|
||||||
|
@ -535,6 +549,94 @@ TEST("world", function()
|
||||||
CHECK(withoutCount == 0)
|
CHECK(withoutCount == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do CASE "Empty Query"
|
||||||
|
do
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local A = world:component()
|
||||||
|
local B = world:component()
|
||||||
|
|
||||||
|
local e1 = world:entity()
|
||||||
|
world:add(e1, A)
|
||||||
|
|
||||||
|
local query = world:query(B)
|
||||||
|
CHECK(query:next() == nil)
|
||||||
|
CHECK(query:replace() == nil)
|
||||||
|
CHECK(query:without() == query)
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local A = world:component()
|
||||||
|
local B = world:component()
|
||||||
|
|
||||||
|
local e1 = world:entity()
|
||||||
|
world:add(e1, A)
|
||||||
|
|
||||||
|
local count = 0
|
||||||
|
for id in world:query(B) do
|
||||||
|
count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
CHECK(count == 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "replace"
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local A = world:component()
|
||||||
|
local B = world:component()
|
||||||
|
local C = world:component()
|
||||||
|
|
||||||
|
local e = world:entity()
|
||||||
|
world:set(e, A, 1)
|
||||||
|
world:set(e, B, true)
|
||||||
|
world:set(e, C, "hello ")
|
||||||
|
|
||||||
|
world:query(A, B, C):replace(function(a, b, c)
|
||||||
|
return a * 2, not b, c.."world"
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(world:get(e, A) == 2)
|
||||||
|
CHECK(world:get(e, B) == false)
|
||||||
|
CHECK(world:get(e, C) == "hello world")
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "without"
|
||||||
|
do
|
||||||
|
-- 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) do
|
||||||
|
counter += 1
|
||||||
|
end
|
||||||
|
CHECK(counter == 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
TEST("world:clear()", function()
|
||||||
|
do CASE("should remove its components")
|
||||||
|
local world = jecs.World.new() :: World
|
||||||
|
local A = world:component()
|
||||||
|
local B = world:component()
|
||||||
|
|
||||||
|
local e = world:entity()
|
||||||
|
|
||||||
|
world:set(e, A, true)
|
||||||
|
world:set(e, B, true)
|
||||||
|
|
||||||
|
CHECK(world:get(e, A))
|
||||||
|
CHECK(world:get(e, B))
|
||||||
|
|
||||||
|
world:clear(e)
|
||||||
|
CHECK(world:get(e, A) == nil)
|
||||||
|
CHECK(world:get(e, B) == nil)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
TEST("world:has()", function()
|
||||||
do CASE "should find Tag on entity"
|
do CASE "should find Tag on entity"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
|
||||||
|
@ -547,21 +649,54 @@ TEST("world", function()
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should return false when missing one tag"
|
do CASE "should return false when missing one tag"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
|
||||||
local A = world:component()
|
local A = world:component()
|
||||||
local B = world:component()
|
local B = world:component()
|
||||||
local C = world:component()
|
local C = world:component()
|
||||||
local D = world:component()
|
local D = world:component()
|
||||||
|
|
||||||
local e = world:entity()
|
local e = world:entity()
|
||||||
world:add(e, A)
|
world:add(e, A)
|
||||||
world:add(e, C)
|
world:add(e, C)
|
||||||
world:add(e, D)
|
world:add(e, D)
|
||||||
|
|
||||||
CHECK(world:has(e, A, B, C, D) == false)
|
CHECK(world:has(e, A, B, C, D) == false)
|
||||||
end
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
TEST("world:component()", function()
|
||||||
|
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
|
||||||
|
end)
|
||||||
|
|
||||||
|
TEST("world:delete", function()
|
||||||
|
do CASE("should allow deleting components")
|
||||||
|
local world = jecs.World.new()
|
||||||
|
|
||||||
|
local Health = world:entity()
|
||||||
|
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(world:get(id, Poison) == nil)
|
||||||
|
CHECK(world:get(id, Health) == nil)
|
||||||
|
CHECK(world:get(id1, Poison) == 500)
|
||||||
|
CHECK(world:get(id1, Health) == 50)
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
type Tracker<T> = { track: (world: World, fn: (changes: {
|
type Tracker<T> = { track: (world: World, fn: (changes: {
|
||||||
|
@ -573,6 +708,25 @@ type Tracker<T> = { track: (world: World, fn: (changes: {
|
||||||
|
|
||||||
type Entity<T = any> = number & { __nominal_type_dont_use: T }
|
type Entity<T = any> = number & { __nominal_type_dont_use: T }
|
||||||
|
|
||||||
|
local function diff(a, b)
|
||||||
|
local size = 0
|
||||||
|
for k, v in a do
|
||||||
|
if b[k] ~= v then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
size += 1
|
||||||
|
end
|
||||||
|
for k, v in b do
|
||||||
|
size -= 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if size ~= 0 then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
local ChangeTracker: <T>(world: World, component: Entity<T>) -> Tracker<T>
|
local ChangeTracker: <T>(world: World, component: Entity<T>) -> Tracker<T>
|
||||||
|
|
||||||
do
|
do
|
||||||
|
@ -594,37 +748,15 @@ do
|
||||||
end
|
end
|
||||||
|
|
||||||
if is_trivial == nil then
|
if is_trivial == nil then
|
||||||
isTrivial = typeof(data) ~= "table"
|
is_trivial = typeof(data) ~= "table"
|
||||||
end
|
|
||||||
|
|
||||||
if not isTrivial then
|
|
||||||
data = table.clone(data)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
add[id] = data
|
add[id] = data
|
||||||
|
|
||||||
return id, data
|
return id, data
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function diff(a, b)
|
|
||||||
local size = 0
|
|
||||||
for k, v in a do
|
|
||||||
if b[k] ~= v then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
size += 1
|
|
||||||
end
|
|
||||||
for k, v in b do
|
|
||||||
size -= 1
|
|
||||||
end
|
|
||||||
|
|
||||||
if size ~= 0 then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function changes_changed()
|
local function changes_changed()
|
||||||
local q = world:query(T, PreviousT)
|
local q = world:query(T, PreviousT)
|
||||||
|
|
||||||
|
@ -635,7 +767,8 @@ do
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
if isTrivial and new ~= old then
|
if is_trivial and new ~= old then
|
||||||
|
break
|
||||||
elseif diff(new, old) then
|
elseif diff(new, old) then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
@ -669,8 +802,8 @@ do
|
||||||
}
|
}
|
||||||
|
|
||||||
local function track(fn)
|
local function track(fn)
|
||||||
added = true
|
added = false
|
||||||
removed = true
|
removed = false
|
||||||
|
|
||||||
fn(changes)
|
fn(changes)
|
||||||
|
|
||||||
|
@ -685,7 +818,7 @@ do
|
||||||
end
|
end
|
||||||
|
|
||||||
for e, data in add do
|
for e, data in add do
|
||||||
world:set(e, PreviousT, if isTrivial then data else table.clone(data))
|
world:set(e, PreviousT, if is_trivial then data else table.clone(data))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -702,78 +835,89 @@ do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
TEST("changetracker", function()
|
TEST("changetracker:track()", function()
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
|
||||||
do CASE "should allow change tracking"
|
do CASE "added"
|
||||||
local Test = world:component() :: Entity<{ foo: number }>
|
local Test = world:component() :: Entity<{ foo: number }>
|
||||||
local TestTracker = ChangeTracker(world, Test)
|
local TestTracker = ChangeTracker(world, Test)
|
||||||
|
|
||||||
local e = world:entity()
|
local e1 = world:entity()
|
||||||
world:set(e, Test, { foo = 11 })
|
local data = { foo = 11 }
|
||||||
|
world:set(e1, Test, data)
|
||||||
|
|
||||||
TestTracker.track(function(changes)
|
TestTracker.track(function(changes)
|
||||||
local added = 0
|
local added = 0
|
||||||
local changed = 0
|
for e, test in changes.added() do
|
||||||
local removed = 0
|
|
||||||
for e, data in changes.added() do
|
|
||||||
added+=1
|
added+=1
|
||||||
|
CHECK(test == data)
|
||||||
end
|
end
|
||||||
for e, old, new in changes.changed() do
|
for e, old, new in changes.changed() do
|
||||||
changed+=1
|
CHECK(false)
|
||||||
end
|
end
|
||||||
for e in changes.removed() do
|
for e in changes.removed() do
|
||||||
removed+=1
|
CHECK(false)
|
||||||
end
|
end
|
||||||
CHECK(added == 1)
|
CHECK(added == 1)
|
||||||
CHECK(changed == 0)
|
|
||||||
CHECK(removed == 0)
|
|
||||||
end)
|
end)
|
||||||
|
end
|
||||||
|
do CASE "changed"
|
||||||
|
local Test = world:component() :: Entity<{ foo: number }>
|
||||||
|
local TestTracker = ChangeTracker(world, Test)
|
||||||
|
|
||||||
for e, test in world:query(Test) do
|
local data = { foo = 11 }
|
||||||
test.foo = test.foo + 1
|
local e1 = world:entity()
|
||||||
end
|
world:set(e1, Test, data)
|
||||||
|
|
||||||
TestTracker.track(function(changes)
|
TestTracker.track(function(changes)
|
||||||
local added = 0
|
end)
|
||||||
|
|
||||||
|
data.foo += 1
|
||||||
|
|
||||||
|
TestTracker.track(function(changes)
|
||||||
|
for _ in changes.added() do
|
||||||
|
CHECK(false)
|
||||||
|
end
|
||||||
local changed = 0
|
local changed = 0
|
||||||
local removed = 0
|
for e, old, new in changes.changed() do
|
||||||
|
CHECK(e == e1)
|
||||||
for e, data in changes.added() do
|
CHECK(new == data)
|
||||||
added+=1
|
CHECK(old ~= new)
|
||||||
end
|
CHECK(diff(new, old))
|
||||||
for e, old, new in changes.changed() do
|
changed +=1
|
||||||
changed+=1
|
end
|
||||||
end
|
|
||||||
for e in changes.removed() do
|
|
||||||
removed+=1
|
|
||||||
end
|
|
||||||
|
|
||||||
CHECK(added == 0)
|
|
||||||
CHECK(changed == 1)
|
CHECK(changed == 1)
|
||||||
CHECK(removed == 0)
|
|
||||||
end)
|
end)
|
||||||
|
end
|
||||||
|
do CASE "removed"
|
||||||
|
local Test = world:component() :: Entity<{ foo: number }>
|
||||||
|
local TestTracker = ChangeTracker(world, Test)
|
||||||
|
|
||||||
world:remove(e, Test)
|
local data = { foo = 11 }
|
||||||
|
local e1 = world:entity()
|
||||||
|
world:set(e1, Test, data)
|
||||||
|
|
||||||
TestTracker.track(function(changes)
|
TestTracker.track(function(changes)
|
||||||
local added = 0
|
end)
|
||||||
local changed = 0
|
|
||||||
|
world:remove(e1, Test)
|
||||||
|
|
||||||
|
TestTracker.track(function(changes)
|
||||||
|
for _ in changes.added() do
|
||||||
|
CHECK(false)
|
||||||
|
end
|
||||||
|
for _ in changes.changed() do
|
||||||
|
CHECK(false)
|
||||||
|
end
|
||||||
local removed = 0
|
local removed = 0
|
||||||
for e, data in changes.added() do
|
for e in changes.removed() do
|
||||||
added+=1
|
removed += 1
|
||||||
end
|
CHECK(e == e1)
|
||||||
for e, old, new in changes.changed() do
|
end
|
||||||
changed+=1
|
|
||||||
end
|
|
||||||
for e in changes.removed() do
|
|
||||||
removed+=1
|
|
||||||
end
|
|
||||||
CHECK(added == 0)
|
|
||||||
CHECK(changed == 0)
|
|
||||||
CHECK(removed == 1)
|
CHECK(removed == 1)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
FINISH()
|
FINISH()
|
||||||
|
|
Loading…
Reference in a new issue