Implement world:targets() as a valid method (#311)

* Implement world:targets

* Proper indexing in ECS_PAIR_SECOND

* Unit tests

* pull idr.records index out of iterator

* move tests below world:target

* style

* better test names

* change nth to use idr.records and pull out variables from iter

* local NOOP fn

* pull NOOP out of fn

* redeclare component_index as ct_idx

* black magic (inlined most of the function calls in iterator - yes it still passes tests)

* remove redundant fn call (shoutout to nnullcolumn for spotting this)

* add test for rapid add/remove calls

* run secondary mixing step for all entities and not just the alive ones

* redundant check

* be a bit more explicit for checking if an entity is alive
This commit is contained in:
kuro 2026-04-21 07:15:13 -04:00 committed by GitHub
parent 19823453aa
commit e2c56f5420
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 255 additions and 1 deletions

View file

@ -194,6 +194,7 @@ type world = {
entity: (self: world, id: i53?) -> i53,
component: (self: world) -> i53,
target: (self: world, id: i53, relation: i53, index: number?) -> i53?,
targets: (self: world, id: i53, relation: i53) -> () -> i53?,
delete: (self: world, id: i53) -> (),
add: (self: world, id: i53, component: i53) -> (),
set: (self: world, id: i53, component: i53, data: any) -> (),
@ -253,6 +254,13 @@ export type World = {
index: number?
) -> Entity<unknown>?,
-- Gets an iterator for all viable targets of a relationship
targets: <T>(
self: World,
id: Entity<T> | number,
relation: ecs_entity_t<Component>
) -> () -> Id,
--- Deletes an entity and all it's related components and relationships.
delete: <T>(self: World, id: Entity<T>) -> (),
@ -3302,6 +3310,50 @@ local function world_new(DEBUG: boolean?)
ECS_PAIR_SECOND(nth))
end
local NOOP = NOOP :: () -> i53
local function world_targets(world: world, entity: i53, relation: i53): () -> i53?
local record = entity_index_try_get_unsafe(entity)
if not record then
return NOOP
end
local archetype = record.archetype
if not archetype then
return NOOP
end
local r = ECS_PAIR(relation, EcsWildcard)
local ct_idx = world.component_index
local idr = ct_idx[r]
if not idr then
return NOOP
end
local archetype_id = archetype.id
local count = idr.counts[archetype_id]
if not count then
return NOOP
end
local nth = idr.records[archetype_id]
local end_count = nth + count
local archetype_types = archetype.types
local sparse_array = entity_index.sparse_array
local dense_array = entity_index.dense_array
return function(): i53?
if nth == end_count then
return nil
end
local target = dense_array[sparse_array[ECS_PAIR_SECOND(archetype_types[nth])].dense]
nth += 1
return target
end
end
local function world_parent(world: world, entity: i53): i53?
return world_target(world, entity, EcsChildOf, 0)
end
@ -3730,6 +3782,7 @@ local function world_new(DEBUG: boolean?)
world.get = world_get :: any
world.has = world_has :: any
world.target = world_target
world.targets = world_targets
world.parent = world_parent
world.contains = world_contains
world.exists = world_exists

View file

@ -3077,7 +3077,7 @@ TEST("world:set()", function()
end
end)
TEST("world:target", function()
TEST("world:target()", function()
do CASE "nth index"
local world = jecs.world()
local A = world:component()
@ -3178,6 +3178,207 @@ TEST("world:target", function()
end
end)
TEST("world:targets()", function()
do CASE "should find single relation"
local world = jecs.world()
local Alice = world:entity()
local Bob = world:entity()
local Likes = world:entity()
world:add(Alice, jecs.pair(Likes, Bob))
local i = 0
for target in world:targets(Alice, Likes) do
i += 1
CHECK(target == Bob)
end
CHECK(i == 1)
end
do CASE "basic iteration"
local world = jecs.world()
local ROOT = world:entity()
local e1 = world:entity()
local targets = {}
for i = 1, 10 do
local target = world:entity()
targets[i] = target
world:add(e1, pair(ROOT, target))
end
local i = 0
for target in world:targets(e1, ROOT) do
i += 1
CHECK(targets[i] == target)
end
CHECK(i == 10)
end
do CASE "multiple iterations"
local world = jecs.world()
local ROOT = world:entity()
local OTHER = world:entity()
local e = world:entity()
local root_targets = {}
local other_targets = {}
for i = 1, 5 do
local t = world:entity()
root_targets[i] = t
world:add(e, pair(ROOT, t))
end
for i = 1, 3 do
local t = world:entity()
other_targets[i] = t
world:add(e, pair(OTHER, t))
end
local i = 0
for target in world:targets(e, ROOT) do
i += 1
CHECK(root_targets[i] == target)
end
CHECK(i == 5)
local j = 0
for target in world:targets(e, OTHER) do
j += 1
CHECK(other_targets[j] == target)
end
CHECK(j == 3)
end
do CASE "empty iterator"
local world = jecs.world()
local ROOT = world:entity()
local OTHER = world:entity()
local e = world:entity()
world:add(e, pair(ROOT, world:entity()))
local count = 0
for _ in world:targets(e, OTHER) do
count += 1
end
CHECK(count == 0)
end
do CASE "should ignore deleted targets"
local world = jecs.world()
local ROOT = world:entity()
local e = world:entity()
local alive = {}
local dead = {}
for i = 1, 3 do
local t = world:entity()
alive[#alive + 1] = t
world:add(e, pair(ROOT, t))
end
for i = 1, 2 do
local t = world:entity()
dead[#dead + 1] = t
world:add(e, pair(ROOT, t))
world:delete(t)
end
local count = 0
for t in world:targets(e, ROOT) do
count += 1
CHECK(t ~= nil)
end
CHECK(count == #alive)
end
do CASE "should properly handle rapid add/remove calls"
local world = jecs.world()
local ROOT = world:entity()
local e = world:entity()
local alive = {}
local all_targets = {} :: {Entity}
for i = 1, 100 do
local t = world:entity()
all_targets[#all_targets + 1] = t
world:add(e, jecs.pair(ROOT, t))
alive[t] = true
end
for i = 1, 500 do
local t: Entity
while t == nil do
t = all_targets[math.random(#all_targets)]
end
if math.random() < 0.5 then
world:remove(e, jecs.pair(ROOT, t))
alive[t] = nil
else
world:add(e, jecs.pair(ROOT, t))
alive[t] = true
end
end
local entity_index = world.entity_index
local function entity_index_check_alive(entity)
local r = entity_index.sparse_array[ECS_ID(entity)]
if not r or r.dense == 0 then
return false
end
local dense = r.dense
if dense > entity_index.alive_count then
return false
end
return entity_index.dense_array[dense] ~= nil
end
for i=1, 10 do
local seen = {}
for t in world:targets(e, ROOT) do
CHECK(entity_index_check_alive(t))
CHECK(entity_index_check_alive(jecs.pair(ROOT, t)))
CHECK(alive[t] == true)
CHECK(seen[t] == nil)
seen[t] = true
end
for t, _ in pairs(alive) do
CHECK(seen[t] == true)
end
for j = 1, #all_targets do
local t = all_targets[j]
if math.random() < 0.5 then
world:remove(e, jecs.pair(ROOT, t))
alive[t] = nil
else
world:add(e, jecs.pair(ROOT, t))
alive[t] = true
end
end
end
end
end)
TEST("#adding a recycled target", function()
local world = jecs.world()
local R = world:component()