mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-25 01:20:04 +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
|
||||
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 id = world.nextArchetypeId + 1
|
||||
|
@ -393,7 +393,7 @@ local function archetype_ensure(world: World, types, prev): Archetype
|
|||
return archetype
|
||||
end
|
||||
|
||||
return archetype_of(world, types, prev)
|
||||
return archetype_create(world, types, prev)
|
||||
end
|
||||
|
||||
local function find_insert(types: { i53 }, toAdd: i53): number
|
||||
|
@ -844,13 +844,16 @@ do
|
|||
end
|
||||
end
|
||||
|
||||
lastArchetype = 1
|
||||
archetype = compatible_archetypes[lastArchetype]
|
||||
|
||||
if not archetype then
|
||||
return EmptyQuery
|
||||
end
|
||||
|
||||
entities = archetype.entities
|
||||
columns = archetype.columns
|
||||
tr = column_indices[lastArchetype]
|
||||
i = #entities
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
@ -916,14 +919,26 @@ do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
if #compatible_archetypes == 0 then
|
||||
archetype = compatible_archetypes[lastArchetype]
|
||||
if not archetype then
|
||||
return EmptyQuery
|
||||
end
|
||||
|
||||
entities = archetype.entities
|
||||
columns = archetype.columns
|
||||
tr = column_indices[lastArchetype]
|
||||
i = #entities
|
||||
|
||||
return query
|
||||
end
|
||||
|
||||
|
@ -1190,7 +1205,7 @@ function World.new()
|
|||
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
||||
}, World)
|
||||
|
||||
self.ROOT_ARCHETYPE = archetype_of(self, {})
|
||||
self.ROOT_ARCHETYPE = archetype_create(self, {})
|
||||
|
||||
for i = HI_COMPONENT_ID + 1, EcsRest do
|
||||
-- Initialize built-in components
|
||||
|
@ -1210,7 +1225,7 @@ return {
|
|||
Component = EcsComponent,
|
||||
Wildcard = EcsWildcard :: Entity,
|
||||
w = EcsWildcard :: Entity,
|
||||
Rest = EcsRest,
|
||||
Rest = EcsRest :: Entity,
|
||||
|
||||
pair = (ECS_PAIR :: any) :: <R, T>(pred: Entity, obj: Entity) -> number,
|
||||
|
||||
|
|
698
test/tests.luau
698
test/tests.luau
|
@ -20,11 +20,137 @@ local function CHECK_NO_ERR<T...>(s: string, fn: (T...) -> (), ...: T...)
|
|||
CHECK(msg == s, 2)
|
||||
end
|
||||
end
|
||||
local N = 10
|
||||
local N = 2^8
|
||||
|
||||
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"
|
||||
local world = jecs.World.new()
|
||||
|
||||
|
@ -43,44 +169,72 @@ TEST("world", function()
|
|||
CHECK(world:get(id, Poison) == nil)
|
||||
CHECK(world:get(id, Health) == 50)
|
||||
end
|
||||
do CASE("should find every component id")
|
||||
local world = jecs.World.new() :: World
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
world:entity()
|
||||
world:entity()
|
||||
world:entity()
|
||||
end)
|
||||
|
||||
local count = 0
|
||||
for componentId in world:query(jecs.Component) do
|
||||
if componentId ~= A and componentId ~= B then
|
||||
error("found entity")
|
||||
end
|
||||
count += 1
|
||||
end
|
||||
|
||||
CHECK(count == 2)
|
||||
end
|
||||
|
||||
do CASE("should remove its components")
|
||||
local world = jecs.World.new() :: World
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
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: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)
|
||||
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 drain query while iterating")
|
||||
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)
|
||||
|
||||
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
|
||||
end)
|
||||
|
||||
|
||||
TEST("world:query()", function()
|
||||
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) 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()
|
||||
|
@ -105,8 +259,30 @@ TEST("world", function()
|
|||
CHECK(i == 2)
|
||||
CHECK(j == 0)
|
||||
end
|
||||
end
|
||||
|
||||
do CASE("should be able to get next results")
|
||||
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
|
||||
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()
|
||||
|
@ -123,36 +299,12 @@ TEST("world", function()
|
|||
|
||||
local e, data = q:next()
|
||||
while e do
|
||||
CHECK(
|
||||
if e == eA then data == true
|
||||
elseif e == eAB then data == true
|
||||
else false
|
||||
)
|
||||
if e ~= eA and e ~= eAB then
|
||||
CHECK(false)
|
||||
end
|
||||
e, data = q:next()
|
||||
end
|
||||
end
|
||||
|
||||
do CASE("should query all matching entities")
|
||||
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) do
|
||||
table.remove(entities, CHECK(table.find(entities, id)))
|
||||
end
|
||||
|
||||
CHECK(#entities == 0)
|
||||
CHECK(true)
|
||||
end
|
||||
|
||||
do CASE("should query all matching entities when irrelevant component is removed")
|
||||
|
@ -208,65 +360,6 @@ TEST("world", function()
|
|||
CHECK(#entities == 0)
|
||||
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")
|
||||
local world = jecs.World.new()
|
||||
local Eats = world:entity()
|
||||
|
@ -392,49 +485,8 @@ TEST("world", function()
|
|||
CHECK(count == 2)
|
||||
end
|
||||
|
||||
do CASE "should be able to add/remove matching entity during iteration"
|
||||
local world = jecs.World.new()
|
||||
local Name = world:component()
|
||||
for i = 1, 5 do
|
||||
local e = world:entity()
|
||||
world:set(e, Name, tostring(e))
|
||||
end
|
||||
local count = 0
|
||||
for id, name in world:query(Name) do
|
||||
count += 1
|
||||
CHECK(id == tonumber(name))
|
||||
|
||||
world:remove(id, Name)
|
||||
local e = world:entity()
|
||||
world:set(e, Name, tostring(e))
|
||||
end
|
||||
CHECK(count == 5)
|
||||
end
|
||||
|
||||
do CASE "should allow adding a matching entity during iteration"
|
||||
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) do
|
||||
local e = world:entity()
|
||||
world:add(e, A)
|
||||
world:add(e, B)
|
||||
count += 1
|
||||
end
|
||||
|
||||
CHECK(count == 3)
|
||||
end
|
||||
|
||||
|
||||
do CASE "should not iterate same entity when adding component"
|
||||
do CASE "iterator invalidation"
|
||||
do CASE "adding"
|
||||
SKIP()
|
||||
local world = jecs.World.new()
|
||||
local A = world:component()
|
||||
|
@ -456,65 +508,27 @@ TEST("world", function()
|
|||
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"
|
||||
do CASE "spawning"
|
||||
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(B) do
|
||||
for id in world:query(A) do
|
||||
local e = world:entity()
|
||||
world:add(e, A)
|
||||
world:add(e, B)
|
||||
count += 1
|
||||
end
|
||||
|
||||
CHECK(count == 0)
|
||||
CHECK(count == 3)
|
||||
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
|
||||
|
||||
do CASE "should not find any entities"
|
||||
|
@ -535,6 +549,94 @@ TEST("world", function()
|
|||
CHECK(withoutCount == 0)
|
||||
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"
|
||||
local world = jecs.World.new()
|
||||
|
||||
|
@ -561,7 +663,40 @@ TEST("world", function()
|
|||
|
||||
CHECK(world:has(e, A, B, C, D) == false)
|
||||
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)
|
||||
|
||||
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 }
|
||||
|
||||
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>
|
||||
|
||||
do
|
||||
|
@ -594,37 +748,15 @@ do
|
|||
end
|
||||
|
||||
if is_trivial == nil then
|
||||
isTrivial = typeof(data) ~= "table"
|
||||
end
|
||||
|
||||
if not isTrivial then
|
||||
data = table.clone(data)
|
||||
is_trivial = typeof(data) ~= "table"
|
||||
end
|
||||
|
||||
add[id] = data
|
||||
|
||||
return id, data
|
||||
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 q = world:query(T, PreviousT)
|
||||
|
||||
|
@ -635,7 +767,8 @@ do
|
|||
return nil
|
||||
end
|
||||
|
||||
if isTrivial and new ~= old then
|
||||
if is_trivial and new ~= old then
|
||||
break
|
||||
elseif diff(new, old) then
|
||||
break
|
||||
end
|
||||
|
@ -669,8 +802,8 @@ do
|
|||
}
|
||||
|
||||
local function track(fn)
|
||||
added = true
|
||||
removed = true
|
||||
added = false
|
||||
removed = false
|
||||
|
||||
fn(changes)
|
||||
|
||||
|
@ -685,7 +818,7 @@ do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
|
@ -702,78 +835,89 @@ do
|
|||
end
|
||||
end
|
||||
|
||||
TEST("changetracker", function()
|
||||
TEST("changetracker:track()", function()
|
||||
local world = jecs.World.new()
|
||||
|
||||
do CASE "should allow change tracking"
|
||||
do CASE "added"
|
||||
local Test = world:component() :: Entity<{ foo: number }>
|
||||
local TestTracker = ChangeTracker(world, Test)
|
||||
|
||||
local e = world:entity()
|
||||
world:set(e, Test, { foo = 11 })
|
||||
local e1 = world:entity()
|
||||
local data = { foo = 11 }
|
||||
world:set(e1, Test, data)
|
||||
|
||||
TestTracker.track(function(changes)
|
||||
local added = 0
|
||||
local changed = 0
|
||||
local removed = 0
|
||||
for e, data in changes.added() do
|
||||
for e, test in changes.added() do
|
||||
added+=1
|
||||
CHECK(test == data)
|
||||
end
|
||||
for e, old, new in changes.changed() do
|
||||
changed+=1
|
||||
CHECK(false)
|
||||
end
|
||||
for e in changes.removed() do
|
||||
removed+=1
|
||||
CHECK(false)
|
||||
end
|
||||
CHECK(added == 1)
|
||||
CHECK(changed == 0)
|
||||
CHECK(removed == 0)
|
||||
end)
|
||||
|
||||
for e, test in world:query(Test) do
|
||||
test.foo = test.foo + 1
|
||||
end
|
||||
do CASE "changed"
|
||||
local Test = world:component() :: Entity<{ foo: number }>
|
||||
local TestTracker = ChangeTracker(world, Test)
|
||||
|
||||
local data = { foo = 11 }
|
||||
local e1 = world:entity()
|
||||
world:set(e1, Test, data)
|
||||
|
||||
TestTracker.track(function(changes)
|
||||
local added = 0
|
||||
local changed = 0
|
||||
local removed = 0
|
||||
end)
|
||||
|
||||
for e, data in changes.added() do
|
||||
added+=1
|
||||
data.foo += 1
|
||||
|
||||
TestTracker.track(function(changes)
|
||||
for _ in changes.added() do
|
||||
CHECK(false)
|
||||
end
|
||||
local changed = 0
|
||||
for e, old, new in changes.changed() do
|
||||
CHECK(e == e1)
|
||||
CHECK(new == data)
|
||||
CHECK(old ~= new)
|
||||
CHECK(diff(new, old))
|
||||
changed +=1
|
||||
end
|
||||
for e in changes.removed() do
|
||||
removed+=1
|
||||
end
|
||||
|
||||
CHECK(added == 0)
|
||||
CHECK(changed == 1)
|
||||
CHECK(removed == 0)
|
||||
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)
|
||||
local added = 0
|
||||
local changed = 0
|
||||
end)
|
||||
|
||||
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
|
||||
for e, data in changes.added() do
|
||||
added+=1
|
||||
end
|
||||
for e, old, new in changes.changed() do
|
||||
changed+=1
|
||||
end
|
||||
for e in changes.removed() do
|
||||
removed += 1
|
||||
CHECK(e == e1)
|
||||
end
|
||||
CHECK(added == 0)
|
||||
CHECK(changed == 0)
|
||||
CHECK(removed == 1)
|
||||
end)
|
||||
end
|
||||
|
||||
end)
|
||||
|
||||
FINISH()
|
||||
|
|
Loading…
Reference in a new issue