Add nth parameter to world:target

This commit is contained in:
Ukendio 2024-09-10 02:59:27 +02:00
parent 411138e1f7
commit 54b67ebdf3
3 changed files with 214 additions and 89 deletions

View file

@ -0,0 +1,4 @@
_G.JECS_DEBUG = true
_G.JECS_HI_COMPONENT_ID = 32
require(game:GetService("ReplicatedStorage").ecs)
return

View file

@ -382,11 +382,8 @@ local function world_has(world: World, entity: number, ...: i53): boolean
return true
end
-- TODO:
-- should have an additional `nth` parameter which selects the nth target
-- this is important when an entity can have multiple relationships with the same target
local function world_target(world: World, entity: i53,
relation: i24--[[, nth: number]]): i24?
relation: i24, index: number): i24?
local record = world.entityIndex.sparse[entity]
local archetype = record.archetype
@ -404,7 +401,24 @@ local function world_target(world: World, entity: i53,
return nil
end
return ecs_pair_second(world, archetype.types[tr.column])
local count = tr.count
if index >= count then
index = index + count + 1
end
local nth = archetype.types[index + tr.column]
if not nth then
return nil
end
return ecs_pair_second(world, nth)
end
local function ECS_ID_IS_WILDCARD(e: i53): boolean
local first = ECS_ENTITY_T_HI(e)
local second = ECS_ENTITY_T_LO(e)
return first == EcsWildcard or second == EcsWildcard
end
local function id_record_ensure(world: World, id: number): ArchetypeMap
@ -415,8 +429,8 @@ local function id_record_ensure(world: World, id: number): ArchetypeMap
local flags = ECS_ID_MASK
local relation = ECS_ENTITY_T_HI(id)
local cleanup_policy = world_target(world, relation, EcsOnDelete)
local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget)
local cleanup_policy = world_target(world, relation, EcsOnDelete, 0)
local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0)
local has_delete = false
@ -426,14 +440,14 @@ local function id_record_ensure(world: World, id: number): ArchetypeMap
local on_add, on_set, on_remove = world_get(world, relation, EcsOnAdd, EcsOnSet, EcsOnRemove)
local has_tag = world_has_one_inline(world, id, EcsTag)
local is_tag = not world_has_one_inline(world, relation, EcsComponent)
flags = bit32.bor(flags,
if on_add then ECS_ID_HAS_ON_ADD else 0,
if on_remove then ECS_ID_HAS_ON_REMOVE else 0,
if on_set then ECS_ID_HAS_ON_SET else 0,
if has_delete then ECS_ID_DELETE else 0,
if has_tag then ECS_ID_IS_TAG else 0
if is_tag then ECS_ID_IS_TAG else 0
)
idr = {
@ -447,13 +461,6 @@ local function id_record_ensure(world: World, id: number): ArchetypeMap
return idr
end
local function ECS_ID_IS_WILDCARD(e: i53): boolean
assert(ECS_IS_PAIR(e))
local first = ECS_ENTITY_T_HI(e)
local second = ECS_ENTITY_T_LO(e)
return first == EcsWildcard or second == EcsWildcard
end
local function archetype_create(world: World, types: { i24 }, prev: Archetype?): Archetype
local ty = hash(types)
@ -465,29 +472,39 @@ local function archetype_create(world: World, types: { i24 }, prev: Archetype?):
local records: { ArchetypeRecord } = {}
for i, componentId in types do
local tr = { column = i, count = 1 }
local idr = id_record_ensure(world, componentId)
idr.cache[id] = tr
idr.size += 1
records[componentId] = tr
local tr = { column = i, count = 1 }
idr.cache[id] = tr
idr.size += 1
records[componentId] = tr
if ECS_IS_PAIR(componentId) then
local relation = ecs_pair_first(world, componentId)
local object = ecs_pair_second(world, componentId)
local r = ECS_PAIR(relation, EcsWildcard)
local idr_r = id_record_ensure(world, r)
local r_tr = idr_r.cache[id]
if not r_tr then
r_tr = { column = i, count = 1 }
idr_r.cache[id] = r_tr
idr_r.size += 1
records[r] = r_tr
else
r_tr.count += 1
end
local o = ECS_PAIR(EcsWildcard, object)
local idr_o = id_record_ensure(world, o)
records[r] = tr
records[o] = tr
idr_r.cache[id] = tr
idr_o.cache[id] = tr
idr_r.size += 1
idr_o.size += 1
local t = ECS_PAIR(EcsWildcard, object)
local idr_t = id_record_ensure(world, t)
local t_tr = idr_t.cache[id]
if not t_tr then
t_tr = { column = i, count = 1 }
idr_t.cache[id] = t_tr
idr_t.size += 1
records[t] = t_tr
else
t_tr.count += 1
end
end
if bit32.band(idr.flags, ECS_ID_IS_TAG) == 0 then
columns[i] = {}
@ -519,7 +536,7 @@ local function world_entity(world: World): i53
end
local function world_parent(world: World, entity: i53)
return world_target(world, entity, EcsChildOf)
return world_target(world, entity, EcsChildOf, 0)
end
local function archetype_ensure(world: World, types, prev): Archetype
@ -747,7 +764,9 @@ do
column_count: number, types: { i53 }, entity: i53)
for i, column in columns do
column[column_count] = nil
if column ~= NULL_ARRAY then
column[column_count] = nil
end
end
end
@ -755,8 +774,10 @@ do
column_count: number, row, types, entity)
for i, column in columns do
column[row] = column[column_count]
column[column_count] = nil
if column ~= NULL_ARRAY then
column[row] = column[column_count]
column[column_count] = nil
end
end
end
local function archetype_delete(world: World,
@ -1449,9 +1470,7 @@ if _G.__JECS_DEBUG == true then
return world_query(world, ...)
end
World.set = function(world: World, entity: i53,
id: i53, value: any): ()
World.set = function(world: World, entity: i53, id: i53, value: any): ()
local idr = world.componentIndex[id]
local flags = idr.flags
local id_is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0
@ -1475,7 +1494,7 @@ if _G.__JECS_DEBUG == true then
world_add(world, entity, id)
end
World.get = function(world: World, entity: i53, id: i53, ...: i53)
World.get = function(world: World, entity: i53, ...)
local length = select("#", ...)
ASSERT(length > 4, "world:get does not support more than 4 components")
for i = 1, length do
@ -1484,15 +1503,12 @@ if _G.__JECS_DEBUG == true then
local flags = idr.flags
local id_is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0
if id_is_tag then
local name = world_get_one_inline(world, id, EcsName) or `${id}`
throw(`cannot get component ({name}) value because it is a tag. If this was intentional, use "world:has(entity, {name})"`)
end
end
if value ~= nil then
local name = world_get_one_inline(world, id, EcsName) or `${id}`
throw(`You provided a value when none was expected. Did you mean to use "world:add(entity, {name})"`)
end
return world_get(world, entity, id)
return world_get(world, entity, ...)
end
end
@ -1518,6 +1534,11 @@ function World.new()
entity_index_new_id(self.entityIndex, i)
end
world_add(self, EcsOnSet, EcsComponent)
world_add(self, EcsOnAdd, EcsComponent)
world_add(self, EcsOnRemove, EcsComponent)
world_add(self, EcsRest, EcsComponent)
world_add(self, EcsName, EcsComponent)
world_add(self, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete))
return self
@ -1563,7 +1584,7 @@ export type World = {
component: <T>(self: World) -> Entity<T>,
--- Gets the target of an relationship. For example, when a user calls
--- `world:target(id, ChildOf(parent))`, you will obtain the parent entity.
target: (self: World, id: Entity, relation: Entity) -> Entity?,
target: (self: World, id: Entity, relation: Entity, nth: number) -> Entity?,
--- Deletes an entity and all it's related components and relationships.
delete: (self: World, id: Entity) -> (),

View file

@ -1,3 +1,4 @@
local jecs = require("@jecs")
local testkit = require("@testkit")
local BENCH, START = testkit.benchmark()
@ -9,6 +10,7 @@ local pair = jecs.pair
local getAlive = jecs.entity_index_get_alive
local ecs_pair_first = jecs.pair_first
local ecs_pair_second = jecs.pair_second
local world_new = jecs.World.new
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
local function CHECK_NO_ERR<T...>(s: string, fn: (T...) -> (), ...: T...)
@ -138,24 +140,50 @@ TEST("world:set()", function()
end
do CASE "arbitrary order"
local world = jecs.World.new()
local world = jecs.World.new()
local Health = world:entity()
local Poison = world:component()
local Health = world:entity()
local Poison = world:component()
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
CHECK(world:get(id, Poison) == 5)
end
CHECK(world:get(id, Poison) == 5)
end
do CASE "pairs"
local world = jecs.World.new()
local C1 = world:component()
local C2 = world:component()
local T1 = world:entity()
local T2 = world:entity()
local e = world:entity()
world:set(e, pair(C1, C2), true)
world:set(e, pair(C1, T1), true)
world:set(e, pair(T1, C1), true)
world:set(e, pair(T1, T2), true)
CHECK(world:get(e, pair(C1, C2)))
CHECK(world:get(e, pair(C1, T1)))
CHECK(not world:get(e, pair(T1, C1)))
CHECK(not world:get(e, pair(T1, T2)))
local e2 = world:entity()
world:set(e2, pair(jecs.ChildOf, e), true)
CHECK(not world:get(e2, pair(jecs.ChildOf, e)))
end
end)
TEST("world:remove()", function()
do CASE "should allow remove a component that doesn't exist on entity"
local world = jecs.World.new()
local Health = world:entity()
local Health = world:component()
local Poison = world:component()
local id = world:entity()
@ -213,14 +241,35 @@ end)
TEST("world:query()", function()
do CASE "tag"
local world = jecs.World.new()
local A = world:component()
world:add(A, jecs.Tag)
local A = world:entity()
local e = world:entity()
world:set(e, A, "test")
for id, a in world:query(A) do
CHECK(a == nil)
end
end
do CASE "pairs"
local world = jecs.World.new()
local C1 = world:component()
local C2 = world:component()
local T1 = world:entity()
local T2 = world:entity()
local e = world:entity()
world:set(e, pair(C1, C2), true)
world:set(e, pair(C1, T1), true)
world:set(e, pair(T1, C1), true)
world:set(e, pair(T1, T2), true)
for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)) do
CHECK(a == true)
CHECK(b == true)
CHECK(c == nil)
CHECK(d == nil)
end
end
do CASE "query single component"
do
local world = jecs.World.new()
@ -405,8 +454,8 @@ TEST("world:query()", function()
do CASE("should allow querying for relations")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local Eats = world:component()
local Apples = world:component()
local bob = world:entity()
world:set(bob, pair(Eats, Apples), true)
@ -418,7 +467,7 @@ TEST("world:query()", function()
do CASE("should allow wildcards in queries")
local world = jecs.World.new()
local Eats = world:entity()
local Eats = world:component()
local Apples = world:entity()
local bob = world:entity()
@ -437,7 +486,7 @@ TEST("world:query()", function()
do CASE("should match against multiple pairs")
local world = jecs.World.new()
local Eats = world:entity()
local Eats = world:component()
local Apples = world:entity()
local Oranges = world:entity()
local bob = world:entity()
@ -697,32 +746,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:component()
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:component()
local B = world:component()
local C = world:component()
local D = world:component()
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()
@ -738,10 +787,8 @@ TEST("world:component()", function()
do CASE "tag"
local world = jecs.World.new() :: World
local A = world:component()
local B = world:component()
local C = world:component()
world:add(B, jecs.Tag)
world:add(C, jecs.Tag)
local B = world:entity()
local C = world:entity()
local e = world:entity()
world:set(e, A, "test")
world:add(e, B, "test")
@ -758,7 +805,7 @@ TEST("world:delete", function()
do CASE("should allow deleting components")
local world = jecs.World.new()
local Health = world:entity()
local Health = world:component()
local Poison = world:component()
local id = world:entity()
@ -900,13 +947,66 @@ TEST("world:delete", function()
end
end)
TEST("world:target", function()
do CASE "nth index"
local world = world_new()
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local e = world:entity()
world:add(e, pair(A, B))
world:add(e, pair(A, C))
world:add(e, pair(B, C))
world:add(e, pair(B, D))
world:add(e, pair(C, D))
CHECK(pair(A, B) < pair(A, C))
CHECK(world:target(e, A, 0) == B)
CHECK(world:target(e, A, 1) == C)
CHECK(world:target(e, B, 0) == C)
CHECK(world:target(e, B, 1) == D)
CHECK(world:target(e, C, 0) == D)
CHECK(world:target(e, C, 1) == nil)
end
do CASE "loop until no target"
local world = world_new()
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
local target = world:target(e1, ROOT, 0)
while target do
i+=1
CHECK(targets[i] == target)
target = world:target(e1, ROOT, i)
end
CHECK(i == 10)
end
end)
TEST("world:contains", function()
local world = jecs.World.new()
local id = world:entity()
CHECK(world:contains(id))
world:delete(id)
CHECK(not world:contains(id))
do CASE "should not exist after delete"
world:delete(id)
CHECK(not world:contains(id))
end
end)
type Tracker<T> = { track: (world: World, fn: (changes: {
added: () -> () -> (number, T),
@ -1434,7 +1534,7 @@ TEST("scheduler", function()
local DependsOn = components.DependsOn
local Physics = scheduler.phase(Heartbeat)
CHECK(world:target(Physics, DependsOn) == Heartbeat)
CHECK(world:target(Physics, DependsOn, 0) == Heartbeat)
end
do CASE "user-defined sub phases"
@ -1447,7 +1547,7 @@ TEST("scheduler", function()
local A = scheduler.phase(phases.Heartbeat)
local B = scheduler.phase(A)
CHECK(world:target(B, DependsOn) == A)
CHECK(world:target(B, DependsOn, 0) == A)
end
do CASE "phase order"