From 5ff6a43750efd5cb9edba995af9fb997094b4975 Mon Sep 17 00:00:00 2001 From: Marcus Date: Wed, 3 Jul 2024 01:24:17 +0200 Subject: [PATCH] Add clear (#59) * Rename files to luau * Rename remaining files * Update bench.project.json for luau files * Stress test insertion * Add clear * Add a few guards * Use next() in World.__iter * Add tests --- lib/init.luau | 28 +++++++++-- tests/world.luau | 126 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 112 insertions(+), 42 deletions(-) diff --git a/lib/init.luau b/lib/init.luau index 1242cba..0aa3a9f 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -128,6 +128,10 @@ local function ECS_ENTITY_T_LO(e: i53): i24 return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e end +local function STRIP_GENERATION(e: i53): i24 + return ECS_ENTITY_T_LO(e) +end + local function ECS_PAIR(pred: i53, obj: i53): i53 return ECS_COMBINE(ECS_ENTITY_T_LO(obj), ECS_ENTITY_T_LO(pred)) + addFlags(--[[isPair]] true) :: i53 end @@ -442,6 +446,24 @@ function World.delete(world: World, entityId: i53) sparse[entityId] = nil :: any dense[#dense] = nil :: any + +end + +function World.clear(world: World, entityId: i53) + --TODO: use sparse_get (stashed) + local record = world.entityIndex.sparse[entityId] + if not record then + return + end + + local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE + local archetype = record.archetype + + if archetype == nil or archetype == ROOT_ARCHETYPE then + return + end + + moveEntity(world.entityIndex, entityId, record, ROOT_ARCHETYPE) end local function ensureArchetype(world: World, types, prev): Archetype @@ -823,7 +845,6 @@ type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() - function World.__iter(world: World): WorldIterator local entityIndex = world.entityIndex - local dense = entityIndex.dense local sparse = entityIndex.sparse local last @@ -832,14 +853,13 @@ function World.__iter(world: World): WorldIterator local i = 0 local function iterator() i+=1 - local entityId = dense[i] + local entityId, record = next(sparse, last) if not entityId then return end - last = lastEntity + last = entityId - local record = sparse[entityId] local archetype = record.archetype if not archetype then -- Returns only the entity id as an entity without data should not return diff --git a/tests/world.luau b/tests/world.luau index 7ca883c..43a5f2f 100644 --- a/tests/world.luau +++ b/tests/world.luau @@ -22,10 +22,11 @@ local function CHECK_NO_ERR(s: string, fn: (T...) -> (), ...: T...) end local N = 10 +type World = jecs.WorldShim + TEST("world", function() - do - CASE("should be iterable") - local world = jecs.World.new() + do CASE("should be iterable") + local world = jecs.World.new() :: World local A = world:component() local B = world:component() local eA = world:entity() @@ -36,9 +37,7 @@ TEST("world", function() world:set(eAB, A, true) world:set(eAB, B, true) - local count = 0 for id, data in world do - count += 1 if id == eA then CHECK(data[A] == true) CHECK(data[B] == nil) @@ -50,14 +49,79 @@ TEST("world", function() CHECK(data[B] == true) end end - - -- components are registered in the entity index as well - -- so this test has to add 2 to account for them - CHECK(count == 3 + 2) end - do - CASE("should query all matching entities") + 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 + + do CASE("iterator should not drain the query") + 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) + + local i = 0 + local j = 0 + for _ in q do + i+=1 + end + for _ in q do + j+=1 + end + CHECK(i == j) + 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 + 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") local world = jecs.World.new() local A = world:component() local B = world:component() @@ -80,8 +144,7 @@ TEST("world", function() CHECK(#entities == 0) 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 A = world:component() local B = world:component() @@ -110,8 +173,7 @@ TEST("world", function() CHECK(added == N) end - do - CASE("should query all entities without B") + do CASE("should query all entities without B") local world = jecs.World.new() local A = world:component() local B = world:component() @@ -135,8 +197,7 @@ TEST("world", function() CHECK(#entities == 0) end - do - CASE("should allow setting components in arbitrary order") + do CASE("should allow setting components in arbitrary order") local world = jecs.World.new() local Health = world:entity() @@ -149,8 +210,7 @@ TEST("world", function() CHECK(world:get(id, Poison) == 5) end - do - CASE("should allow deleting components") + do CASE("should allow deleting components") local world = jecs.World.new() local Health = world:entity() @@ -171,8 +231,7 @@ TEST("world", function() CHECK(world:get(id1, Health) == 50) end - do - CASE("should allow remove that doesn't exist on entity") + do CASE("should allow remove that doesn't exist on entity") local world = jecs.World.new() local Health = world:entity() @@ -186,8 +245,7 @@ TEST("world", function() CHECK(world:get(id, Health) == 50) end - do - CASE("should increment generation") + do CASE("should increment generation") local world = jecs.World.new() local e = world:entity() CHECK(ECS_ID(e) == 1 + jecs.Rest) @@ -197,8 +255,7 @@ TEST("world", function() CHECK(ECS_GENERATION(e) == 1) -- 1 end - do - CASE("should get alive from index in the dense array") + do CASE("should get alive from index in the dense array") local world = jecs.World.new() local _e = world:entity() local e2 = world:entity() @@ -213,8 +270,7 @@ TEST("world", function() 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 Eats = world:entity() local Apples = world:entity() @@ -227,8 +283,7 @@ TEST("world", function() end end - do - CASE("should allow wildcards in queries") + do CASE("should allow wildcards in queries") local world = jecs.World.new() local Eats = world:entity() local Apples = world:entity() @@ -247,8 +302,7 @@ TEST("world", function() end end - do - CASE("should match against multiple pairs") + do CASE("should match against multiple pairs") local world = jecs.World.new() local Eats = world:entity() local Apples = world:entity() @@ -280,8 +334,7 @@ TEST("world", function() CHECK(count == 1) end - do - CASE("should only relate alive entities") + do CASE("should only relate alive entities") local world = jecs.World.new() local Eats = world:entity() @@ -308,8 +361,7 @@ TEST("world", function() CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) end - do - CASE("should error when setting invalid pair") + do CASE("should error when setting invalid pair") local world = jecs.World.new() local Eats = world:entity() local Apples = world:entity() @@ -320,8 +372,7 @@ TEST("world", function() world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") end - do - CASE("should find target for ChildOf") + do CASE("should find target for ChildOf") local world = jecs.World.new() local ChildOf = world:component() @@ -343,7 +394,6 @@ TEST("world", function() local count = 0 for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do - print(name) count += 1 end CHECK(count == 2)