mirror of
https://github.com/Ukendio/jecs.git
synced 2026-03-18 00:44:32 +00:00
Throw error at structural changes within on_remove hooks
This commit is contained in:
parent
4d76e28425
commit
7170dbf6a1
3 changed files with 269 additions and 256 deletions
|
|
@ -193,7 +193,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
if not r then return end
|
||||
|
||||
local src = r.archetype
|
||||
if not src then return end
|
||||
|
||||
if not archetypes[src.id] then return end
|
||||
|
||||
|
|
@ -287,9 +286,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
end
|
||||
local r = jecs.record(world, entity)
|
||||
local archetype = r.archetype
|
||||
if not archetype then
|
||||
return
|
||||
end
|
||||
|
||||
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
|
||||
last_old_archetype = nil
|
||||
|
|
@ -309,9 +305,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
|
||||
local r = jecs.record(world, entity)
|
||||
local archetype = r.archetype
|
||||
if not archetype then
|
||||
return
|
||||
end
|
||||
if last_old_archetype == archetype then
|
||||
return
|
||||
end
|
||||
|
|
@ -332,9 +325,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
end
|
||||
local r = jecs.record(world, entity)
|
||||
local archetype = r.archetype
|
||||
if not archetype then
|
||||
return
|
||||
end
|
||||
|
||||
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
|
||||
callback_removed(entity)
|
||||
|
|
@ -349,9 +339,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
end
|
||||
local r = jecs.record(world, entity)
|
||||
local archetype = r.archetype
|
||||
if not archetype then
|
||||
return
|
||||
end
|
||||
local dst = jecs.archetype_traverse_remove(world, id, archetype)
|
||||
|
||||
if archetypes[dst.id] then
|
||||
|
|
|
|||
207
src/jecs.luau
207
src/jecs.luau
|
|
@ -156,6 +156,7 @@ type componentrecord = {
|
|||
counts: { [i53]: number },
|
||||
flags: number,
|
||||
size: number,
|
||||
cache: { number },
|
||||
|
||||
on_add: ((entity: i53, id: i53, value: any, oldarchetype: archetype) -> ())?,
|
||||
on_change: ((entity: i53, id: i53, value: any, oldarchetype: archetype) -> ())?,
|
||||
|
|
@ -567,7 +568,9 @@ end
|
|||
|
||||
local ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY = "Entity is outside range"
|
||||
|
||||
local function ENTITY_INDEX_NEW_ID(entity_index: entityindex): i53
|
||||
local function ENTITY_INDEX_NEW_ID(world: world): i53
|
||||
local entity_index = world.entity_index
|
||||
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
|
||||
local dense_array = entity_index.dense_array
|
||||
local alive_count = entity_index.alive_count
|
||||
local sparse_array = entity_index.sparse_array
|
||||
|
|
@ -589,7 +592,7 @@ local function ENTITY_INDEX_NEW_ID(entity_index: entityindex): i53
|
|||
entity_index.max_id = id
|
||||
entity_index.alive_count = next_count
|
||||
dense_array[next_count] = id
|
||||
sparse_array[id] = { dense = next_count } :: record
|
||||
sparse_array[id] = { dense = next_count, row = 0, archetype = ROOT_ARCHETYPE }
|
||||
|
||||
return id
|
||||
end
|
||||
|
|
@ -916,6 +919,7 @@ local function id_record_create(
|
|||
records = {},
|
||||
counts = {},
|
||||
flags = flags,
|
||||
cache = {},
|
||||
|
||||
on_add = on_add,
|
||||
on_change = on_change,
|
||||
|
|
@ -953,6 +957,7 @@ local function archetype_append_to_records(
|
|||
idr_records[archetype_id] = index
|
||||
idr_counts[archetype_id] = 1
|
||||
columns_map[id] = column
|
||||
table.insert(idr.cache, archetype_id)
|
||||
else
|
||||
local max_count = idr_counts[archetype_id] + 1
|
||||
idr_counts[archetype_id] = max_count
|
||||
|
|
@ -1044,8 +1049,10 @@ local function world_range(world: world, range_begin: number, range_end: number?
|
|||
for i = max_id + 1, range_begin do
|
||||
dense_array[i] = i
|
||||
sparse_array[i] = {
|
||||
dense = 0
|
||||
} :: record
|
||||
dense = 0,
|
||||
row = 0,
|
||||
archetype = world.ROOT_ARCHETYPE
|
||||
}
|
||||
end
|
||||
entity_index.max_id = range_begin
|
||||
entity_index.alive_count = range_begin
|
||||
|
|
@ -1085,10 +1092,11 @@ local function find_archetype_without(
|
|||
): archetype
|
||||
local id_types = node.types
|
||||
local at = table.find(id_types, id)
|
||||
|
||||
if at == nil then
|
||||
return node
|
||||
end
|
||||
local dst = table.clone(id_types)
|
||||
table.remove(dst, at)
|
||||
|
||||
return archetype_ensure(world, dst)
|
||||
end
|
||||
|
||||
|
|
@ -1211,6 +1219,13 @@ local function archetype_destroy(world: world, archetype: archetype)
|
|||
if archetype == world.ROOT_ARCHETYPE then
|
||||
return
|
||||
end
|
||||
-- RAII / idempotent: already destroyed or still has entities → no-op
|
||||
if world.archetypes[archetype.id] ~= archetype then
|
||||
return
|
||||
end
|
||||
if #archetype.entities > 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local component_index = world.component_index
|
||||
local archetype_edges = world.archetype_edges
|
||||
|
|
@ -2562,7 +2577,7 @@ local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values:
|
|||
end
|
||||
local from = r.archetype
|
||||
local component_index = world.component_index
|
||||
if not from then
|
||||
if from == world.ROOT_ARCHETYPE then
|
||||
local dst_types = table.clone(ids)
|
||||
table.sort(dst_types)
|
||||
|
||||
|
|
@ -2650,6 +2665,8 @@ local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values:
|
|||
end
|
||||
end
|
||||
|
||||
local ON_REMOVE_STRUCTURAL_WARN = "jecs: on_remove must not perform structural changes (world:add/world:remove); this will be removed as a lint in future versions and can cause silent failures"
|
||||
|
||||
local function ecs_bulk_remove(world: world, entity: i53, ids: { i53 })
|
||||
local entity_index = world.entity_index
|
||||
local r = entity_index_try_get(entity_index, entity)
|
||||
|
|
@ -2658,14 +2675,13 @@ local function ecs_bulk_remove(world: world, entity: i53, ids: { i53 })
|
|||
end
|
||||
local from = r.archetype
|
||||
local component_index = world.component_index
|
||||
if not from then
|
||||
return
|
||||
end
|
||||
|
||||
local remove: { [i53]: boolean } = {}
|
||||
|
||||
local columns_map = from.columns_map
|
||||
|
||||
local dst_types = table.clone(from.types) :: { i53 }
|
||||
|
||||
for i, id in ids do
|
||||
if not columns_map[id] then
|
||||
continue
|
||||
|
|
@ -2677,22 +2693,15 @@ local function ecs_bulk_remove(world: world, entity: i53, ids: { i53 })
|
|||
local on_remove = idr.on_remove
|
||||
if on_remove then
|
||||
on_remove(entity, id)
|
||||
if from ~= r.archetype then
|
||||
error(ON_REMOVE_STRUCTURAL_WARN)
|
||||
end
|
||||
end
|
||||
|
||||
local to = r.archetype
|
||||
if from ~= to then
|
||||
from = to
|
||||
end
|
||||
|
||||
local dst_types = table.clone(from.types) :: { i53 }
|
||||
|
||||
for id in remove do
|
||||
local at = table.find(dst_types, id)
|
||||
table.remove(dst_types, at)
|
||||
end
|
||||
|
||||
to = archetype_ensure(world, dst_types)
|
||||
local to = archetype_ensure(world, dst_types)
|
||||
|
||||
if from ~= to then
|
||||
entity_move(entity_index, entity, r, to)
|
||||
|
|
@ -2877,9 +2886,7 @@ local function world_new(DEBUG: boolean?)
|
|||
return
|
||||
end
|
||||
|
||||
local from: archetype = record.archetype
|
||||
local ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
||||
local src = from or ROOT_ARCHETYPE
|
||||
local src = record.archetype
|
||||
local column = src.columns_map[id]
|
||||
if column then
|
||||
local idr = component_index[id]
|
||||
|
|
@ -2909,9 +2916,9 @@ local function world_new(DEBUG: boolean?)
|
|||
local id_types = src.types
|
||||
if on_remove then
|
||||
on_remove(entity, id_types[cr])
|
||||
src = record.archetype
|
||||
id_types = src.types
|
||||
cr = idr.records[src.id]
|
||||
if src ~= record.archetype then
|
||||
error(ON_REMOVE_STRUCTURAL_WARN)
|
||||
end
|
||||
end
|
||||
|
||||
to = exclusive_traverse_add(src, cr, id)
|
||||
|
|
@ -2934,11 +2941,8 @@ local function world_new(DEBUG: boolean?)
|
|||
if cr then
|
||||
local id_types = src.types
|
||||
on_remove(entity, id_types[cr])
|
||||
local arche = record.archetype
|
||||
if src ~= arche then
|
||||
id_types = arche.types
|
||||
cr = idr.records[arche.id]
|
||||
to = exclusive_traverse_add(arche, cr, id)
|
||||
if src ~= record.archetype then
|
||||
error(ON_REMOVE_STRUCTURAL_WARN)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2957,7 +2961,9 @@ local function world_new(DEBUG: boolean?)
|
|||
idr = component_index[id]
|
||||
end
|
||||
|
||||
if from then
|
||||
local ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
||||
local src_is_root_archetype = src == ROOT_ARCHETYPE
|
||||
if not src_is_root_archetype then
|
||||
-- If there was a previous archetype, then the entity needs to move the archetype
|
||||
inner_entity_move(entity, record, to)
|
||||
else
|
||||
|
|
@ -2984,9 +2990,7 @@ local function world_new(DEBUG: boolean?)
|
|||
return
|
||||
end
|
||||
|
||||
local from = record.archetype
|
||||
local ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
||||
local src = from or ROOT_ARCHETYPE
|
||||
local src = record.archetype
|
||||
if src.columns_map[id] then
|
||||
return
|
||||
end
|
||||
|
|
@ -3008,10 +3012,9 @@ local function world_new(DEBUG: boolean?)
|
|||
local id_types = src.types
|
||||
if on_remove then
|
||||
on_remove(entity, id_types[cr])
|
||||
|
||||
src = record.archetype
|
||||
id_types = src.types
|
||||
cr = idr.records[src.id]
|
||||
if src ~= record.archetype then
|
||||
error(ON_REMOVE_STRUCTURAL_WARN)
|
||||
end
|
||||
end
|
||||
|
||||
to = exclusive_traverse_add(src, cr, id)
|
||||
|
|
@ -3057,7 +3060,9 @@ local function world_new(DEBUG: boolean?)
|
|||
idr = component_index[id]
|
||||
end
|
||||
|
||||
if from then
|
||||
local ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
||||
local src_is_root_archetype = src == ROOT_ARCHETYPE
|
||||
if not src_is_root_archetype then
|
||||
inner_entity_move(entity, record, to)
|
||||
else
|
||||
if #to.types > 0 then
|
||||
|
|
@ -3080,9 +3085,6 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
|
||||
local archetype = record.archetype
|
||||
if not archetype then
|
||||
return nil
|
||||
end
|
||||
|
||||
local columns_map = archetype.columns_map
|
||||
local row = record.row
|
||||
|
|
@ -3345,7 +3347,7 @@ local function world_new(DEBUG: boolean?)
|
|||
-- -- pre-populated some slots already.
|
||||
-- end
|
||||
|
||||
sparse_array[i] = { dense = 0 } :: record
|
||||
sparse_array[i] = { dense = 0, row = 0, archetype = ROOT_ARCHETYPE }
|
||||
end
|
||||
entity_index.max_id = index
|
||||
end
|
||||
|
|
@ -3354,7 +3356,7 @@ local function world_new(DEBUG: boolean?)
|
|||
entity_index.alive_count = alive_count
|
||||
dense_array[alive_count] = entity
|
||||
|
||||
r = { dense = alive_count } :: record
|
||||
r = { dense = alive_count, row = 0, archetype = ROOT_ARCHETYPE }
|
||||
sparse_array[index] = r
|
||||
|
||||
return entity
|
||||
|
|
@ -3377,7 +3379,7 @@ local function world_new(DEBUG: boolean?)
|
|||
entity_index.max_id = id
|
||||
entity_index.alive_count = next_count
|
||||
dense_array[next_count] = id
|
||||
sparse_array[id] = { dense = next_count } :: record
|
||||
sparse_array[id] = { dense = next_count, row = 0, archetype = ROOT_ARCHETYPE }
|
||||
return id
|
||||
end
|
||||
|
||||
|
|
@ -3388,10 +3390,6 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
local from = record.archetype
|
||||
|
||||
if not from then
|
||||
return
|
||||
end
|
||||
|
||||
if from.columns_map[id] then
|
||||
local idr = world.component_index[id]
|
||||
local on_remove = idr.on_remove
|
||||
|
|
@ -3469,7 +3467,6 @@ local function world_new(DEBUG: boolean?)
|
|||
for i = n, 1, -1 do
|
||||
world_delete(world, entities[i])
|
||||
end
|
||||
|
||||
archetype_destroy(world, idr_archetype)
|
||||
end
|
||||
else
|
||||
|
|
@ -3486,13 +3483,10 @@ local function world_new(DEBUG: boolean?)
|
|||
local r = eindex_sparse_array[ECS_ID(e :: number)]
|
||||
local from = r.archetype
|
||||
if from ~= idr_archetype then
|
||||
-- unfortunately the on_remove hook allows a window where `e` can have changed archetype
|
||||
-- this is hypothetically not that expensive of an operation anyways
|
||||
to = archetype_traverse_remove(world, entity, from)
|
||||
end
|
||||
inner_entity_move(e, r, to)
|
||||
end
|
||||
|
||||
archetype_destroy(world, idr_archetype)
|
||||
end
|
||||
else
|
||||
|
|
@ -3505,7 +3499,6 @@ local function world_new(DEBUG: boolean?)
|
|||
local e = entities[i]
|
||||
entity_move(entity_index, e, eindex_sparse_array[ECS_ID(e :: number)], to)
|
||||
end
|
||||
|
||||
archetype_destroy(world, idr_archetype)
|
||||
end
|
||||
end
|
||||
|
|
@ -3513,11 +3506,10 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
|
||||
if idr_t then
|
||||
local archetype_ids = idr_t.records
|
||||
local to_remove = {}:: { [i53]: componentrecord}
|
||||
local did_cascade_delete = false
|
||||
|
||||
for archetype_id in archetype_ids do
|
||||
local to_remove = {} :: { [i53]: componentrecord }
|
||||
local cache = idr_t.cache
|
||||
for i = #cache, 1, -1 do
|
||||
local archetype_id = cache[i]
|
||||
local idr_t_archetype = archetypes[archetype_id]
|
||||
if not idr_t_archetype then
|
||||
continue
|
||||
|
|
@ -3531,19 +3523,13 @@ local function world_new(DEBUG: boolean?)
|
|||
if not ECS_IS_PAIR(id) then
|
||||
continue
|
||||
end
|
||||
local object = entity_index_get_alive(
|
||||
entity_index, ECS_PAIR_SECOND(id))
|
||||
local object = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id))
|
||||
if object ~= entity then
|
||||
continue
|
||||
end
|
||||
local id_record = component_index[id]
|
||||
local flags = id_record.flags
|
||||
local flags_delete_mask = bit32.btest(flags, ECS_ID_DELETE)
|
||||
if flags_delete_mask then
|
||||
for i = #entities, 1, -1 do
|
||||
local child = entities[i]
|
||||
world_delete(world, child)
|
||||
end
|
||||
local has_delete = bit32.btest(id_record.flags, ECS_ID_DELETE)
|
||||
if has_delete then
|
||||
deleted_any = true
|
||||
break
|
||||
else
|
||||
|
|
@ -3553,71 +3539,49 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
|
||||
if deleted_any then
|
||||
did_cascade_delete = true
|
||||
for row = #entities, 1, -1 do
|
||||
world_delete(world, entities[row])
|
||||
end
|
||||
archetype_destroy(world, idr_t_archetype)
|
||||
continue
|
||||
end
|
||||
|
||||
if remove_count == 1 then
|
||||
local id, id_record = next(to_remove)
|
||||
local to_u = archetype_traverse_remove(world, id :: i53, idr_t_archetype)
|
||||
local to = archetype_traverse_remove(world, id::i53, idr_t_archetype)
|
||||
local on_remove = id_record.on_remove
|
||||
for i = #entities, 1, -1 do
|
||||
local child = entities[i]
|
||||
local r = entity_index_try_get_unsafe(child) :: record
|
||||
local to = to_u
|
||||
for row = #entities, 1, -1 do
|
||||
local child = entities[row]
|
||||
local r = entity_index_try_get_unsafe(child)::record
|
||||
local dst = to
|
||||
if on_remove then
|
||||
on_remove(child, id :: i53)
|
||||
local src = r.archetype
|
||||
if src ~= idr_t_archetype then
|
||||
to = archetype_traverse_remove(world, id::i53, src)
|
||||
end
|
||||
inner_entity_move(child, r, dst)
|
||||
end
|
||||
|
||||
inner_entity_move(child, r, to)
|
||||
end
|
||||
elseif remove_count > 1 then
|
||||
local dst_types = table.clone(idr_t_types)
|
||||
for id, component_record in to_remove do
|
||||
archetype_destroy(world, idr_t_archetype)
|
||||
else
|
||||
local dst_types = table.clone(idr_t_archetype.types)
|
||||
for id in to_remove do
|
||||
table.remove(dst_types, table.find(dst_types, id))
|
||||
end
|
||||
|
||||
local to_u = archetype_ensure(world, dst_types)
|
||||
for i = #entities, 1, -1 do
|
||||
local child = entities[i]
|
||||
local r = entity_index_try_get_unsafe(child) :: record
|
||||
local dst = archetype_ensure(world, dst_types)
|
||||
|
||||
local to = to_u
|
||||
for row = #entities, 1, -1 do
|
||||
local child = entities[row]
|
||||
local r = entity_index_try_get_unsafe(child) :: record
|
||||
for id, component_record in to_remove do
|
||||
local on_remove = component_record.on_remove
|
||||
if on_remove then
|
||||
on_remove(child, id)
|
||||
local src = r.archetype
|
||||
if src ~= idr_t_archetype then
|
||||
to = archetype_traverse_remove(world, id, src)
|
||||
end
|
||||
end
|
||||
inner_entity_move(child, r, dst)
|
||||
end
|
||||
|
||||
inner_entity_move(child, r, to)
|
||||
end
|
||||
archetype_destroy(world, idr_t_archetype)
|
||||
end
|
||||
|
||||
table.clear(to_remove)
|
||||
archetype_destroy(world, idr_t_archetype)
|
||||
end
|
||||
|
||||
if did_cascade_delete then
|
||||
for archetype_id in archetype_ids do
|
||||
local idr_t_archetype = archetypes[archetype_id]
|
||||
if not idr_t_archetype then
|
||||
continue
|
||||
end
|
||||
local entities = idr_t_archetype.entities
|
||||
for i = #entities, 1, -1 do
|
||||
world_delete(world, entities[i])
|
||||
end
|
||||
archetype_destroy(world, idr_t_archetype)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -3672,14 +3636,11 @@ local function world_new(DEBUG: boolean?)
|
|||
local r = entity_index_try_get_unsafe(e) :: record
|
||||
inner_entity_move(e, r, node)
|
||||
end
|
||||
|
||||
archetype_destroy(world, idr_r_archetype)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
local dense = record.dense
|
||||
local i_swap = entity_index.alive_count
|
||||
entity_index.alive_count = i_swap - 1
|
||||
|
|
@ -3687,8 +3648,8 @@ local function world_new(DEBUG: boolean?)
|
|||
local e_swap = eindex_dense_array[i_swap]
|
||||
local r_swap = entity_index_try_get_any(e_swap) :: record
|
||||
r_swap.dense = dense
|
||||
record.archetype = nil :: any
|
||||
record.row = nil :: any
|
||||
record.archetype = ROOT_ARCHETYPE
|
||||
record.row = 0
|
||||
record.dense = i_swap
|
||||
|
||||
eindex_dense_array[dense] = e_swap
|
||||
|
|
@ -3710,8 +3671,8 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
end
|
||||
archetype_delete(world, record.archetype, record.row)
|
||||
record.archetype = nil :: any
|
||||
record.row = nil :: any
|
||||
record.archetype = world.ROOT_ARCHETYPE
|
||||
record.row = 0
|
||||
end
|
||||
|
||||
local function world_exists(world: world, entity: i53): boolean
|
||||
|
|
@ -3870,7 +3831,7 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
|
||||
for i = 1, EcsRest do
|
||||
ENTITY_INDEX_NEW_ID(entity_index)
|
||||
ENTITY_INDEX_NEW_ID(world)
|
||||
end
|
||||
|
||||
for i = 1, max_component_id do
|
||||
|
|
@ -3906,7 +3867,7 @@ local function world_new(DEBUG: boolean?)
|
|||
world_add(world, EcsOnDeleteTarget, EcsExclusive)
|
||||
|
||||
for i = EcsRest + 1, ecs_max_tag_id do
|
||||
ENTITY_INDEX_NEW_ID(entity_index)
|
||||
ENTITY_INDEX_NEW_ID(world)
|
||||
end
|
||||
|
||||
for i, bundle in ecs_metadata do
|
||||
|
|
@ -3993,7 +3954,7 @@ local function entity_index_ensure(entity_index: entityindex, e: i53)
|
|||
end
|
||||
|
||||
local function new(world: world)
|
||||
local e = ENTITY_INDEX_NEW_ID(world.entity_index)
|
||||
local e = ENTITY_INDEX_NEW_ID(world)
|
||||
return e
|
||||
end
|
||||
|
||||
|
|
@ -4011,7 +3972,7 @@ local function new_low_id(world: world)
|
|||
end
|
||||
end
|
||||
if e == 0 or e >= HI_COMPONENT_ID then
|
||||
e = ENTITY_INDEX_NEW_ID(entity_index)
|
||||
e = ENTITY_INDEX_NEW_ID(world)
|
||||
else
|
||||
entity_index_ensure(entity_index, e)
|
||||
end
|
||||
|
|
@ -4019,7 +3980,7 @@ local function new_low_id(world: world)
|
|||
end
|
||||
|
||||
local function new_w_id(world: world, id: i53)
|
||||
local e = ENTITY_INDEX_NEW_ID(world.entity_index)
|
||||
local e = ENTITY_INDEX_NEW_ID(world)
|
||||
world.add(world, e, id)
|
||||
return e
|
||||
end
|
||||
|
|
|
|||
261
test/tests.luau
261
test/tests.luau
|
|
@ -25,6 +25,162 @@ local entity_visualiser = require("@modules/entity_visualiser")
|
|||
local dwi = entity_visualiser.stringify
|
||||
|
||||
-- FOCUS()
|
||||
TEST("Stale to_remove", function()
|
||||
local world = jecs.world()
|
||||
|
||||
local a = world:component()
|
||||
local b = world:component()
|
||||
local c = world:component()
|
||||
local d = world:component()
|
||||
local marker = world:component()
|
||||
|
||||
local target = world:entity()
|
||||
|
||||
local first = world:entity()
|
||||
world:add(first, jecs.pair(a, target))
|
||||
world:add(first, jecs.pair(b, target))
|
||||
world:add(first, jecs.pair(jecs.ChildOf, target))
|
||||
|
||||
local second = world:entity()
|
||||
world:add(second, jecs.pair(c, target))
|
||||
world:add(second, jecs.pair(d, target))
|
||||
world:add(second, marker)
|
||||
|
||||
print("-------")
|
||||
world:delete(target)
|
||||
print("-------")
|
||||
|
||||
CHECK(world:contains(second))
|
||||
CHECK(world:has(second, marker))
|
||||
end)
|
||||
|
||||
-- FOCUS()
|
||||
-- Exercises idr_t multi-remove path: entity has multiple pairs with same target (no cascade); delete target → both pairs removed, on_remove each called.
|
||||
TEST("Target delete: multi-remove path (removing_count > 1)", function()
|
||||
local world = jecs.world()
|
||||
local rel1 = world:entity()
|
||||
local rel2 = world:entity()
|
||||
local tag = world:component()
|
||||
local target = world:entity()
|
||||
local e = world:entity()
|
||||
world:add(e, jecs.pair(rel1, target))
|
||||
world:add(e, jecs.pair(rel2, target))
|
||||
world:add(e, tag)
|
||||
local removed_ids = {}
|
||||
world:removed(rel1, function(_e, id) table.insert(removed_ids, id) end)
|
||||
world:removed(rel2, function(_e, id) table.insert(removed_ids, id) end)
|
||||
world:delete(target)
|
||||
CHECK(world:contains(e))
|
||||
CHECK(world:has(e, tag))
|
||||
CHECK(not world:has(e, jecs.pair(rel1, target)))
|
||||
CHECK(not world:has(e, jecs.pair(rel2, target)))
|
||||
CHECK(#removed_ids == 2)
|
||||
end)
|
||||
|
||||
-- FOCUS()
|
||||
TEST("repro 2", function()
|
||||
local sessionDeletedCount = 0
|
||||
local slotDeletedCount = 0
|
||||
|
||||
for i = 1, 100 do
|
||||
local world = jecs.world(true);
|
||||
|
||||
-- randomness
|
||||
for _ = 1, i % 40 do
|
||||
world:entity()
|
||||
end
|
||||
|
||||
local ofMatch = world:component()
|
||||
local ofTeam = world:component()
|
||||
local ofRound = world:component()
|
||||
local ownedBy = world:component()
|
||||
local nextTeam = world:component()
|
||||
local activeRound = world:component()
|
||||
local team = world:component()
|
||||
local session = world:component()
|
||||
local round = world:component()
|
||||
|
||||
world:add(ofTeam, jecs.Exclusive)
|
||||
world:add(ownedBy, jecs.Exclusive)
|
||||
world:add(nextTeam, jecs.Exclusive)
|
||||
|
||||
world:add(activeRound, jecs.Exclusive)
|
||||
|
||||
local slotEntity = world:entity()
|
||||
local matchEntity = world:entity()
|
||||
world:add(matchEntity, jecs.pair(jecs.ChildOf, slotEntity))
|
||||
|
||||
local teams = {}
|
||||
for t = 1, 2 do
|
||||
local teamEntity = world:entity()
|
||||
world:add(teamEntity, team)
|
||||
world:add(teamEntity, jecs.pair(jecs.ChildOf, matchEntity))
|
||||
world:add(teamEntity, jecs.pair(ofMatch, matchEntity))
|
||||
table.insert(teams, teamEntity)
|
||||
end
|
||||
world:add(matchEntity, jecs.pair(nextTeam, teams[1]))
|
||||
|
||||
local roundEntity = world:entity()
|
||||
world:add(roundEntity, round)
|
||||
world:add(roundEntity, jecs.pair(jecs.ChildOf, matchEntity))
|
||||
|
||||
-- doing something as simple as adding this pair causes error rate to change. When this isn't here, sessionDeletedCount drops from 100% to 80%.
|
||||
world:add(matchEntity, jecs.pair(activeRound, roundEntity))
|
||||
|
||||
local sessions = {}
|
||||
for j = 1, #teams do
|
||||
-- random number of players on team
|
||||
for _ = 1, 1 + (i % 5) do
|
||||
local player = world:entity()
|
||||
|
||||
local sessionEntity = world:entity()
|
||||
world:add(sessionEntity, session)
|
||||
world:add(sessionEntity, jecs.pair(ofMatch, matchEntity))
|
||||
world:add(sessionEntity, jecs.pair(ofTeam, teams[j]))
|
||||
|
||||
-- not adding this next pair makes sessionDeletedCount to drop to 0%??
|
||||
world:add(sessionEntity, jecs.pair(ownedBy, player))
|
||||
world:add(sessionEntity, jecs.pair(ofRound, roundEntity))
|
||||
table.insert(sessions, sessionEntity)
|
||||
end
|
||||
end
|
||||
|
||||
world:delete(matchEntity)
|
||||
|
||||
-- session should stay alive after match deletion
|
||||
for _, entity in sessions do
|
||||
if not world:contains(entity) then
|
||||
sessionDeletedCount += 1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not world:contains(slotEntity) then
|
||||
slotDeletedCount += 1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
CHECK(sessionDeletedCount == 0)
|
||||
CHECK(slotDeletedCount == 0)
|
||||
|
||||
if (sessionDeletedCount + slotDeletedCount > 0)then
|
||||
print(`repro 2 incorrect session deletion count: {sessionDeletedCount}`)
|
||||
|
||||
-- this has never been above 0, but it's the issue i'm having
|
||||
print(`repro 2 incorrect slot deletion count: {slotDeletedCount}`)
|
||||
end
|
||||
end)
|
||||
TEST("migrating to real records", function()
|
||||
local world = jecs.world(true)
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
print(jecs.record(world, e1).row)
|
||||
world:add(e1, jecs.pair(jecs.ChildOf, e2))
|
||||
world:set(e1, jecs.Name, "hello")
|
||||
print(jecs.record(world, e1).row)
|
||||
print(world:get(e1, jecs.Name), world:has(e1, jecs.pair(jecs.ChildOf, jecs.Wildcard)))
|
||||
end)
|
||||
TEST("e2 is nil", function()
|
||||
local world = jecs.world(true)
|
||||
local e1 = world:entity(1000)
|
||||
|
|
@ -35,6 +191,7 @@ TEST("e2 is nil", function()
|
|||
CHECK(e1 and world:contains(e1))
|
||||
CHECK(e2 and world:contains(e2))
|
||||
end)
|
||||
-- FOCUS()
|
||||
TEST("reproduce idr_t nil archetype bug", function()
|
||||
local world = jecs.world(true)
|
||||
|
||||
|
|
@ -549,44 +706,6 @@ TEST("world:add()", function()
|
|||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
end
|
||||
|
||||
do CASE "exclusive relations invoke on_remove hooks that should allow side effects"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
local D = world:component()
|
||||
|
||||
world:add(A, jecs.Exclusive)
|
||||
local call_count = 0
|
||||
world:set(A, jecs.OnRemove, function(e, id)
|
||||
call_count += 1
|
||||
if call_count == 1 then
|
||||
world:add(e, C)
|
||||
else
|
||||
world:add(e, D)
|
||||
end
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, pair(A, B))
|
||||
world:add(e, pair(A, C))
|
||||
|
||||
CHECK(world:has(e, pair(A, B)) == false)
|
||||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
CHECK(world:has(e, C))
|
||||
|
||||
|
||||
-- We have to ensure that it actually invokes hooks everytime it
|
||||
-- traverses the archetype
|
||||
e = world:entity()
|
||||
world:add(e, pair(A, B))
|
||||
world:add(e, pair(A, C))
|
||||
|
||||
CHECK(world:has(e, pair(A, B)) == false)
|
||||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
CHECK(world:has(e, D))
|
||||
end
|
||||
|
||||
do CASE "idempotent"
|
||||
local world = jecs.world()
|
||||
local d = dwi(world)
|
||||
|
|
@ -609,8 +728,8 @@ TEST("world:add()", function()
|
|||
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(d.tbl(e) == world.ROOT_ARCHETYPE)
|
||||
CHECK(d.row(e) == 0)
|
||||
|
||||
local archetypes = #world.archetypes
|
||||
-- This should create a new archetype
|
||||
|
|
@ -1044,24 +1163,6 @@ TEST("world:delete()", function()
|
|||
-- CHECK(B_OnRemove_called)
|
||||
end
|
||||
|
||||
do CASE "idr_t//remove//on_remove//changed_archetype@3123..3126"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
world:set(A, jecs.OnRemove, function(entity, id)
|
||||
world:set(entity, B, true)
|
||||
end)
|
||||
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
world:add(e2, pair(A, e2))
|
||||
world:set(e2, pair(A, e1), true)
|
||||
|
||||
world:delete(e1)
|
||||
|
||||
CHECK(not world:has(e2, pair(A, e1)))
|
||||
end
|
||||
|
||||
do CASE "pair(OnDelete, Delete)"
|
||||
local world = jecs.world()
|
||||
local ct = world:component()
|
||||
|
|
@ -1604,6 +1705,7 @@ TEST("world:added", function()
|
|||
end
|
||||
end)
|
||||
|
||||
-- FOCUS()
|
||||
TEST("world:range()", function()
|
||||
|
||||
do CASE "spawn entity under min range"
|
||||
|
|
@ -2911,43 +3013,6 @@ TEST("world:set()", function()
|
|||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
end
|
||||
|
||||
do CASE "exclusive relations invoke on_remove hooks that should allow side effects"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
local D = world:component()
|
||||
|
||||
world:add(A, jecs.Exclusive)
|
||||
local call_count = 0
|
||||
world:set(A, jecs.OnRemove, function(e, id)
|
||||
call_count += 1
|
||||
if call_count == 1 then
|
||||
world:set(e, C, true)
|
||||
else
|
||||
world:set(e, D, true)
|
||||
end
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:set(e, pair(A, B), true)
|
||||
world:set(e, pair(A, C), true)
|
||||
|
||||
CHECK(world:has(e, pair(A, B)) == false)
|
||||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
CHECK(world:has(e, C))
|
||||
|
||||
|
||||
-- We have to ensure that it actually invokes hooks everytime it
|
||||
-- traverses the archetype
|
||||
e = world:entity()
|
||||
world:set(e, pair(A, B), true)
|
||||
world:set(e, pair(A, C), true)
|
||||
|
||||
CHECK(world:has(e, pair(A, B)) == false)
|
||||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
CHECK(world:has(e, D))
|
||||
end
|
||||
do CASE "archetype move"
|
||||
local world = jecs.world()
|
||||
|
||||
|
|
@ -2958,8 +3023,8 @@ TEST("world:set()", function()
|
|||
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(d.tbl(e) == world.ROOT_ARCHETYPE)
|
||||
CHECK(d.row(e) == 0)
|
||||
|
||||
local archetypes = #world.archetypes
|
||||
-- This should create a new archetype since it is the first
|
||||
|
|
@ -3295,7 +3360,7 @@ TEST("Hooks", function()
|
|||
local B = world:component()
|
||||
local e = world:entity()
|
||||
|
||||
world:set(A, jecs.OnRemove, function(entity)
|
||||
world:set(A, jecs.OnRemove, function(entity: jecs.Entity)
|
||||
world:set(entity, B, true)
|
||||
CHECK(world:get(entity, A))
|
||||
CHECK(world:get(entity, B))
|
||||
|
|
|
|||
Loading…
Reference in a new issue