Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve cycles in world:delete #129

Merged
merged 1 commit into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 67 additions & 92 deletions src/init.luau
Original file line number Diff line number Diff line change
Expand Up @@ -992,98 +992,6 @@ do
else
archetype_fast_delete(columns, column_count, row, types, delete)
end

local component_index = world.componentIndex

local archetypes = world.archetypes

local idr = component_index[delete]
if idr then
local children = {}
for archetype_id in idr.cache do
local idr_archetype = archetypes[archetype_id]

for i, child in idr_archetype.entities do
table.insert(children, child)
end
end
local flags = idr.flags
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
for _, child in children do
-- Cascade deletion to children
world_delete(world, child)
end
else
for _, child in children do
world_remove(world, child, delete)
end
end
end
-- TODO: iterate each linked record.
-- local r = ECS_PAIR(delete, EcsWildcard)
-- local idr_r = component_index[r]
-- if idr_r then
-- -- Doesn't work for relations atm
-- for archetype_id in idr_o.cache do
-- local children = {}
-- local idr_r_archetype = archetypes[archetype_id]
-- local idr_r_types = idr_r_archetype.types

-- for _, child in idr_r_archetype.entities do
-- table.insert(children, child)
-- end

-- for _, id in idr_r_types do
-- local relation = ECS_ENTITY_T_HI(id)
-- if world_target(world, child, relation) == delete then
-- world_remove(world, child, ECS_PAIR(relation, delete))
-- end
-- end
-- end
-- end

local o = ECS_PAIR(EcsWildcard, delete)
local idr_o = component_index[o]

if idr_o then
for archetype_id in idr_o.cache do
local children = {}
local idr_o_archetype = archetypes[archetype_id]
-- In the future, this needs to be optimized to only
-- look for linked records instead of doing this linearly

local idr_o_types = idr_o_archetype.types

for _, child in idr_o_archetype.entities do
table.insert(children, child)
end

for _, id in idr_o_types do
if not ECS_IS_PAIR(id) then
continue
end

local id_record = component_index[id]

if id_record then
local flags = id_record.flags
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
for _, child in children do
-- Cascade deletions of it has Delete as component trait
world_delete(world, child, destruct)
end
else
local object = ECS_ENTITY_T_LO(id)
if object == delete then
for _, child in children do
world_remove(world, child, id)
end
end
end
end
end
end
end
end

function world_delete(world: World, entity: i53, destruct: boolean?)
Expand All @@ -1103,6 +1011,73 @@ do
archetype_delete(world, archetype, row, destruct)
end

local delete = entity
local component_index = world.componentIndex
local archetypes = world.archetypes
local tgt = ECS_PAIR(EcsWildcard, delete)
local idr_t = component_index[tgt]
local idr = component_index[delete]

if idr then
local children = {}
for archetype_id in idr.cache do
local idr_archetype = archetypes[archetype_id]

for i, child in idr_archetype.entities do
table.insert(children, child)
end
end
local flags = idr.flags
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
for _, child in children do
-- Cascade deletion to children
world_delete(world, child)
end
else
for _, child in children do
world_remove(world, child, delete)
end
end
end

if idr_t then
for archetype_id in idr_t.cache do
local children = {}
local idr_o_archetype = archetypes[archetype_id]

local idr_o_types = idr_o_archetype.types

for _, child in idr_o_archetype.entities do
table.insert(children, child)
end

for _, id in idr_o_types do
if not ECS_IS_PAIR(id) then
continue
end

local id_record = component_index[id]

if id_record then
local flags = id_record.flags
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
for _, child in children do
-- Cascade deletions of it has Delete as component trait
world_delete(world, child, destruct)
end
else
local object = ECS_ENTITY_T_LO(id)
if object == delete then
for _, child in children do
world_remove(world, child, id)
end
end
end
end
end
end
end

record.archetype = nil :: any
entityIndex.sparse[entity] = nil

Expand Down
96 changes: 59 additions & 37 deletions test/tests.luau
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ local function debug_world_inspect(world)
}
end

local function name(world, e)
return world:get(e, jecs.Name)
end

TEST("world:entity()", function()
do CASE "unique IDs"
local world = jecs.World.new()
Expand Down Expand Up @@ -254,7 +258,6 @@ TEST("world:query()", function()
for x in q:iter() do
counter += 1
end
print(counter)
CHECK(counter == 2)
end
do CASE "tag"
Expand Down Expand Up @@ -764,32 +767,32 @@ TEST("world:clear()", function()
end)

TEST("world:has()", function()
do CASE "should find Tag on entity"
local world = jecs.World.new()
do CASE "should find Tag on entity"
local world = jecs.World.new()

local Tag = world:entity()
local Tag = world:entity()

local e = world:entity()
world:add(e, Tag)
local e = world:entity()
world:add(e, Tag)

CHECK(world:has(e, Tag))
end
CHECK(world:has(e, Tag))
end

do CASE "should return false when missing one tag"
local world = jecs.World.new()
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 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)
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
CHECK(world:has(e, A, B, C, D) == false)
end
end)

TEST("world:component()", function()
Expand Down Expand Up @@ -820,27 +823,46 @@ TEST("world:component()", function()
end)

TEST("world:delete", function()
do CASE "bug: Empty entity does not respect cleanup policy"
local world = world_new()
local parent = world:entity()
local tag = world:entity()

local child = world:entity()
world:add(child, jecs.pair(jecs.ChildOf, parent))
world:delete(parent)

CHECK(not world:contains(parent))
CHECK(not world:contains(child))

local entity = world:entity()
world:add(entity, tag)
world:delete(tag)
CHECK(world:contains(entity))
CHECK(not world:contains(tag))
CHECK(not world:has(entity, tag)) -- => true
end
do CASE("should allow deleting components")
local world = jecs.World.new()
local world = jecs.World.new()

local Health = world:component()
local Poison = world:component()
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)
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)
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
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.new()
Expand Down Expand Up @@ -918,7 +940,7 @@ TEST("world:delete", function()
end)

for i, friend in friends do
CHECK(not world:has(friends, pair(jecs.ChildOf, e)))
CHECK(not world:has(friend, pair(FriendsWith, e)))
CHECK(world:has(friend, Health))
end
end
Expand Down