Compare commits

..

4 commits

Author SHA1 Message Date
Ukendio
8822be58a9 Add assertion against existing idr
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-04-22 04:52:21 +02:00
Ukendio
ef0f69ac6d Set signal with hook 2025-04-22 04:49:52 +02:00
Marcus
42401f93ae
Add component registration and metadata API (#219)
* Add component registration and metadata API

* Fix test case erroring

* Fix type issues

* Add component to hook calls' arguments

* Add is_tag function

* Allow focus to capture a single Test

* Add test cases for preregistered tags
2025-04-22 04:38:30 +02:00
Axen
6835f91a09
Docs/hooks set order warning (#220)
* Add docs warning about hook set order

* Improve warning wording
2025-04-22 04:37:04 +02:00
6 changed files with 254 additions and 132 deletions

View file

@ -101,45 +101,50 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl
world.added = function(_, component, fn) world.added = function(_, component, fn)
local listeners = signals.added[component] local listeners = signals.added[component]
local component_index = world.component_index :: jecs.ComponentIndex
assert(component_index[component] == nil, "You cannot use hooks on components you intend to use this signal with")
if not listeners then if not listeners then
listeners = {} listeners = {}
signals.added[component] = listeners signals.added[component] = listeners
local idr = jecs.id_record_ensure(world, component) local function on_add(entity: number, id: number, value: any)
idr.hooks.on_add = function(entity) for _, listener in listeners :: any do
for _, listener in listeners do listener(entity, id, value)
listener(entity, component)
end end
end end
end world:set(component, jecs.OnAdd, on_add) end
table.insert(listeners, fn) table.insert(listeners, fn)
end end
world.changed = function(_, component, fn) world.changed = function(_, component, fn)
local listeners = signals.emplaced[component] local listeners = signals.emplaced[component]
local component_index = world.component_index :: jecs.ComponentIndex
assert(component_index[component] == nil, "You cannot use hooks on components you intend to use this signal with")
if not listeners then if not listeners then
listeners = {} listeners = {}
signals.emplaced[component] = listeners signals.emplaced[component] = listeners
local idr = jecs.id_record_ensure(world, component) local function on_change(entity: number, id: number, value: any)
idr.hooks.on_change = function(entity, value) for _, listener in listeners :: any do
for _, listener in listeners do listener(entity, id, value)
listener(entity, component, value)
end end
end end
world:set(component, jecs.OnChange, on_change)
end end
table.insert(listeners, fn) table.insert(listeners, fn)
end end
world.removed = function(_, component, fn) world.removed = function(_, component, fn)
local listeners = signals.removed[component] local listeners = signals.removed[component]
local component_index = world.component_index :: jecs.ComponentIndex
assert(component_index[component] == nil, "You cannot use hooks on components you intend to use this signal with")
if not listeners then if not listeners then
listeners = {} listeners = {}
signals.removed[component] = listeners signals.removed[component] = listeners
local idr = jecs.id_record_ensure(world, component) local function on_remove(entity: number, id: number, value: any)
idr.hooks.on_remove = function(entity) for _, listener in listeners :: any do
for _, listener in listeners do listener(entity, id, value)
listener(entity, component)
end end
end end
world:set(component, jecs.OnRemove, on_remove)
end end
table.insert(listeners, fn) table.insert(listeners, fn)
end end
@ -150,7 +155,7 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl
world.monitor = monitors_new world.monitor = monitors_new
return world return world :: PatchedWorld
end end
return observers_add return observers_add

View file

@ -14,6 +14,10 @@ A (component) ID can be marked with `Tag´ in which the component will never con
Hooks are part of the "interface" of a component. You could consider hooks as the counterpart to OOP methods in ECS. They define the behavior of a component, but can only be invoked through mutations on the component data. You can only configure a single `OnAdd`, `OnRemove` and `OnSet` hook per component, just like you can only have a single constructor and destructor. Hooks are part of the "interface" of a component. You could consider hooks as the counterpart to OOP methods in ECS. They define the behavior of a component, but can only be invoked through mutations on the component data. You can only configure a single `OnAdd`, `OnRemove` and `OnSet` hook per component, just like you can only have a single constructor and destructor.
::: warning
Hooks, added to a component that has already been added to other entities/components, will not be called.
:::
## Examples ## Examples
::: code-group ::: code-group

271
jecs.luau
View file

@ -45,9 +45,9 @@ type ecs_id_record_t = {
flags: number, flags: number,
size: number, size: number,
hooks: { hooks: {
on_add: ((entity: i53, data: any?) -> ())?, on_add: ((entity: i53, id: i53, data: any?) -> ())?,
on_change: ((entity: i53, data: any) -> ())?, on_change: ((entity: i53, id: i53, data: any) -> ())?,
on_remove: ((entity: i53) -> ())?, on_remove: ((entity: i53, id: i53) -> ())?,
}, },
} }
@ -116,13 +116,47 @@ local ECS_ID_MASK = 0b00
local ECS_ENTITY_MASK = bit32.lshift(1, 24) local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16) local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local NULL_ARRAY = table.freeze({}) local NULL_ARRAY = table.freeze({}) :: Column
local NULL = newproxy(false)
local ECS_INTERNAL_ERROR = [[ local ECS_INTERNAL_ERROR = [[
This is an internal error, please file a bug report via the following link: This is an internal error, please file a bug report via the following link:
https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md
]] ]]
local ecs_metadata: Map<i53, Map<i53, any>> = {}
local ecs_max_component_id = 0
local ecs_max_tag_id = EcsRest
local function ECS_COMPONENT()
ecs_max_component_id += 1
if ecs_max_component_id > HI_COMPONENT_ID then
error("Too many components")
end
return ecs_max_component_id
end
local function ECS_TAG()
ecs_max_tag_id += 1
return ecs_max_tag_id
end
local function ECS_META(id: i53, ty: i53, value: any?)
local bundle = ecs_metadata[id]
if bundle == nil then
bundle = {}
ecs_metadata[id] = bundle
end
bundle[ty] = if value == nil then NULL else value
end
local function ECS_META_RESET()
ecs_metadata = {}
ecs_max_component_id = 0
ecs_max_tag_id = EcsRest
end
local function ECS_COMBINE(id: number, generation: number): i53 local function ECS_COMBINE(id: number, generation: number): i53
return id + (generation * ECS_ENTITY_MASK) return id + (generation * ECS_ENTITY_MASK)
end end
@ -466,6 +500,14 @@ local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): b
return records[id] ~= nil return records[id] ~= nil
end end
local function ecs_is_tag(world: ecs_world_t, entity: i53): boolean
local idr = world.component_index[entity]
if idr then
return bit32.band(idr.flags, ECS_ID_IS_TAG) ~= 0
end
return not world_has_one_inline(world, entity, EcsComponent)
end
local function world_has(world: ecs_world_t, entity: i53, local function world_has(world: ecs_world_t, entity: i53,
a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean
@ -529,62 +571,64 @@ end
local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t
local component_index = world.component_index local component_index = world.component_index
local entity_index = world.entity_index local entity_index = world.entity_index
local idr: ecs_id_record_t = component_index[id] local idr: ecs_id_record_t? = component_index[id]
if not idr then if idr then
local flags = ECS_ID_MASK return idr
local relation = id
local target = 0
local is_pair = ECS_IS_PAIR(id)
if is_pair then
relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53
assert(relation and entity_index_is_alive(
entity_index, relation), ECS_INTERNAL_ERROR)
target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53
assert(target and entity_index_is_alive(
entity_index, target), ECS_INTERNAL_ERROR)
end
local cleanup_policy = world_target(world, relation, EcsOnDelete, 0)
local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0)
local has_delete = false
if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then
has_delete = true
end
local on_add, on_change, on_remove = world_get(world,
relation, EcsOnAdd, EcsOnChange, EcsOnRemove)
local is_tag = not world_has_one_inline(world,
relation, EcsComponent)
if is_tag and is_pair then
is_tag = not world_has_one_inline(world, target, EcsComponent)
end
flags = bit32.bor(
flags,
if has_delete then ECS_ID_DELETE else 0,
if is_tag then ECS_ID_IS_TAG else 0
)
idr = {
size = 0,
cache = {},
counts = {},
flags = flags,
hooks = {
on_add = on_add,
on_change = on_change,
on_remove = on_remove,
},
}
component_index[id] = idr
end end
local flags = ECS_ID_MASK
local relation = id
local target = 0
local is_pair = ECS_IS_PAIR(id)
if is_pair then
relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53
assert(relation and entity_index_is_alive(
entity_index, relation), ECS_INTERNAL_ERROR)
target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53
assert(target and entity_index_is_alive(
entity_index, target), ECS_INTERNAL_ERROR)
end
local cleanup_policy = world_target(world, relation, EcsOnDelete, 0)
local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0)
local has_delete = false
if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then
has_delete = true
end
local on_add, on_change, on_remove = world_get(world,
relation, EcsOnAdd, EcsOnChange, EcsOnRemove)
local is_tag = not world_has_one_inline(world,
relation, EcsComponent)
if is_tag and is_pair then
is_tag = not world_has_one_inline(world, target, EcsComponent)
end
flags = bit32.bor(
flags,
if has_delete then ECS_ID_DELETE else 0,
if is_tag then ECS_ID_IS_TAG else 0
)
idr = {
size = 0,
cache = {},
counts = {},
flags = flags,
hooks = {
on_add = on_add,
on_change = on_change,
on_remove = on_remove,
},
} :: ecs_id_record_t
component_index[id] = idr
return idr return idr
end end
@ -621,7 +665,7 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev:
local columns = (table.create(length) :: any) :: { Column } local columns = (table.create(length) :: any) :: { Column }
local records: { number } = {} local records: { number } = {}
local counts: {number} = {} local counts: { number } = {}
local archetype: ecs_archetype_t = { local archetype: ecs_archetype_t = {
columns = columns, columns = columns,
@ -670,7 +714,7 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev:
world.archetype_index[ty] = archetype world.archetype_index[ty] = archetype
world.archetypes[archetype_id] = archetype world.archetypes[archetype_id] = archetype
world.archetype_edges[archetype.id] = {} world.archetype_edges[archetype.id] = {} :: Map<i53, ecs_archetype_t>
return archetype return archetype
end end
@ -747,17 +791,17 @@ local function archetype_traverse_remove(
local edges = world.archetype_edges local edges = world.archetype_edges
local edge = edges[from.id] local edge = edges[from.id]
local to = edge[id] local to: ecs_archetype_t = edge[id]
if not to then if to == nil then
to = find_archetype_without(world, from, id) to = find_archetype_without(world, from, id)
edge[id] = to edge[id] = to
edges[to.id][id] = from edges[to.id][id] = from
end end
return to :: ecs_archetype_t return to
end end
local function find_archetype_with(world, id, from) local function find_archetype_with(world, id, from): ecs_archetype_t
local id_types = from.types local id_types = from.types
local at = find_insert(id_types, id) local at = find_insert(id_types, id)
@ -767,7 +811,7 @@ local function find_archetype_with(world, id, from)
return archetype_ensure(world, dst) return archetype_ensure(world, dst)
end end
local function archetype_traverse_add(world, id, from: ecs_archetype_t) local function archetype_traverse_add(world, id, from: ecs_archetype_t): ecs_archetype_t
from = from or world.ROOT_ARCHETYPE from = from or world.ROOT_ARCHETYPE
if from.records[id] then if from.records[id] then
return from return from
@ -813,7 +857,7 @@ local function world_add(
local on_add = idr.hooks.on_add local on_add = idr.hooks.on_add
if on_add then if on_add then
on_add(entity) on_add(entity, id)
end end
end end
@ -830,7 +874,7 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown
local idr_hooks = idr.hooks local idr_hooks = idr.hooks
if from == to then if from == to then
local tr = to.records[id] local tr = (to :: ecs_archetype_t).records[id]
local column = from.columns[tr] local column = from.columns[tr]
column[record.row] = data column[record.row] = data
@ -838,7 +882,7 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown
-- and just set the data directly. -- and just set the data directly.
local on_change = idr_hooks.on_change local on_change = idr_hooks.on_change
if on_change then if on_change then
on_change(entity, data) on_change(entity, id, data)
end end
return return
@ -861,7 +905,7 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown
local on_add = idr_hooks.on_add local on_add = idr_hooks.on_add
if on_add then if on_add then
on_add(entity, data) on_add(entity, id, data)
end end
end end
@ -893,7 +937,7 @@ local function world_remove(world: ecs_world_t, entity: i53, id: i53)
local idr = world.component_index[id] local idr = world.component_index[id]
local on_remove = idr.hooks.on_remove local on_remove = idr.hooks.on_remove
if on_remove then if on_remove then
on_remove(entity) on_remove(entity, id)
end end
local to = archetype_traverse_remove(world, id, record.archetype) local to = archetype_traverse_remove(world, id, record.archetype)
@ -945,7 +989,7 @@ local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t,
local idr = component_index[id] local idr = component_index[id]
local on_remove = idr.hooks.on_remove local on_remove = idr.hooks.on_remove
if on_remove then if on_remove then
on_remove(delete) on_remove(delete, id)
end end
end end
@ -984,8 +1028,8 @@ local function world_clear(world: ecs_world_t, entity: i53)
end end
if idr_t then if idr_t then
local queue local queue: { i53 }
local ids local ids: Map<i53, boolean>
local count = 0 local count = 0
local archetype_ids = idr_t.cache local archetype_ids = idr_t.cache
@ -1005,7 +1049,7 @@ local function world_clear(world: ecs_world_t, entity: i53)
continue continue
end end
if not ids then if not ids then
ids = {} ids = {} :: { [i53]: boolean }
end end
ids[id] = true ids[id] = true
removal_queued = true removal_queued = true
@ -1016,7 +1060,7 @@ local function world_clear(world: ecs_world_t, entity: i53)
end end
if not queue then if not queue then
queue = {} queue = {} :: { i53 }
end end
local n = #entities local n = #entities
@ -1175,8 +1219,8 @@ local function world_delete(world: ecs_world_t, entity: i53)
end end
if idr_t then if idr_t then
local children local children: { i53 }
local ids local ids: Map<i53, boolean>
local count = 0 local count = 0
local archetype_ids = idr_t.cache local archetype_ids = idr_t.cache
@ -1206,7 +1250,7 @@ local function world_delete(world: ecs_world_t, entity: i53)
break break
else else
if not ids then if not ids then
ids = {} ids = {} :: { [i53]: boolean }
end end
ids[id] = true ids[id] = true
removal_queued = true removal_queued = true
@ -1217,7 +1261,7 @@ local function world_delete(world: ecs_world_t, entity: i53)
continue continue
end end
if not children then if not children then
children = {} children = {} :: { i53 }
end end
local n = #entities local n = #entities
table.move(entities, 1, n, count + 1, children) table.move(entities, 1, n, count + 1, children)
@ -1240,7 +1284,7 @@ local function world_delete(world: ecs_world_t, entity: i53)
if idr_r then if idr_r then
local archetype_ids = idr_r.cache local archetype_ids = idr_r.cache
local flags = idr_r.flags local flags = idr_r.flags
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then if (bit32.band(flags, ECS_ID_DELETE) :: number) ~= 0 then
for archetype_id in archetype_ids do for archetype_id in archetype_ids do
local idr_r_archetype = archetypes[archetype_id] local idr_r_archetype = archetypes[archetype_id]
local entities = idr_r_archetype.entities local entities = idr_r_archetype.entities
@ -1754,7 +1798,7 @@ local function query_cached(query: ecs_query_data_t)
local observable = world.observable :: ecs_observable_t local observable = world.observable :: ecs_observable_t
local on_create_action = observable[EcsOnArchetypeCreate] local on_create_action = observable[EcsOnArchetypeCreate]
if not on_create_action then if not on_create_action then
on_create_action = {} on_create_action = {} :: Map<i53, { ecs_observer_t }>
observable[EcsOnArchetypeCreate] = on_create_action observable[EcsOnArchetypeCreate] = on_create_action
end end
local query_cache_on_create = on_create_action[A] local query_cache_on_create = on_create_action[A]
@ -1765,7 +1809,7 @@ local function query_cached(query: ecs_query_data_t)
local on_delete_action = observable[EcsOnArchetypeDelete] local on_delete_action = observable[EcsOnArchetypeDelete]
if not on_delete_action then if not on_delete_action then
on_delete_action = {} on_delete_action = {} :: Map<i53, { ecs_observer_t }>
observable[EcsOnArchetypeDelete] = on_delete_action observable[EcsOnArchetypeDelete] = on_delete_action
end end
local query_cache_on_delete = on_delete_action[A] local query_cache_on_delete = on_delete_action[A]
@ -2168,12 +2212,12 @@ local function world_query(world: ecs_world_t, ...)
return q return q
end end
if idr == nil or map.size < idr.size then if idr == nil or (map.size :: number) < (idr.size :: number) then
idr = map idr = map
end end
end end
if not idr then if idr == nil then
return q return q
end end
@ -2306,7 +2350,7 @@ local function world_new()
ROOT_ARCHETYPE = (nil :: any) :: Archetype, ROOT_ARCHETYPE = (nil :: any) :: Archetype,
max_archetype_id = 0, max_archetype_id = 0,
max_component_id = 0, max_component_id = ecs_max_component_id,
observable = {} :: Observable, observable = {} :: Observable,
}, World) :: any }, World) :: any
@ -2323,6 +2367,21 @@ local function world_new()
entity_index_new_id(entity_index) entity_index_new_id(entity_index)
end end
for i = EcsRest + 1, ecs_max_tag_id do
-- Initialize built-in components
entity_index_new_id(entity_index)
end
for i, bundle in ecs_metadata do
for ty, value in bundle do
if value == NULL then
world_add(self, i, ty)
else
world_set(self, i, ty, value)
end
end
end
world_add(self, EcsName, EcsComponent) world_add(self, EcsName, EcsComponent)
world_add(self, EcsOnChange, EcsComponent) world_add(self, EcsOnChange, EcsComponent)
world_add(self, EcsOnAdd, EcsComponent) world_add(self, EcsOnAdd, EcsComponent)
@ -2350,22 +2409,25 @@ end
World.new = world_new World.new = world_new
export type Entity<T = unknown> = { __T: T } export type Entity<T = any> = { __T: T }
export type Id<T = unknown> = { __T: T } export type Id<T = any> = { __T: T }
export type Pair<P, O> = Id<P> export type Pair<P, O> = Id<P>
type ecs_id_t<T=unknown> = Id<T> | Pair<T, "Tag"> | Pair<"Tag", T> type ecs_id_t<T=unknown> = Id<T> | Pair<T, "Tag"> | Pair<"Tag", T>
export type Item<T...> = (self: Query<T...>) -> (Entity, T...) export type Item<T...> = (self: Query<T...>) -> (Entity, T...)
export type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...) export type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
export type Query<T...> = typeof(setmetatable({}, { export type Query<T...> = typeof(setmetatable(
__iter = (nil :: any) :: Iter<T...>, {} :: {
})) & { iter: Iter<T...>,
iter: Iter<T...>, with: (self: Query<T...>, ...Id) -> Query<T...>,
with: (self: Query<T...>, ...Id) -> Query<T...>, without: (self: Query<T...>, ...Id) -> Query<T...>,
without: (self: Query<T...>, ...Id) -> Query<T...>, archetypes: (self: Query<T...>) -> { Archetype },
archetypes: (self: Query<T...>) -> { Archetype }, cached: (self: Query<T...>) -> Query<T...>,
cached: (self: Query<T...>) -> Query<T...>, },
} {} :: {
__iter: Iter<T...>
}
))
export type Observer = { export type Observer = {
callback: (archetype: Archetype) -> (), callback: (archetype: Archetype) -> (),
@ -2399,20 +2461,20 @@ export type World = {
component: <T>(self: World) -> Entity<T>, component: <T>(self: World) -> Entity<T>,
--- Gets the target of an relationship. For example, when a user calls --- Gets the target of an relationship. For example, when a user calls
--- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity.
target: (self: World, id: Entity, relation: Id, index: number?) -> Entity?, target: <T>(self: World, id: Entity, relation: Id<T>, index: number?) -> Entity?,
--- Deletes an entity and all it's related components and relationships. --- Deletes an entity and all it's related components and relationships.
delete: (self: World, id: Entity) -> (), delete: (self: World, id: Entity) -> (),
--- Adds a component to the entity with no value --- Adds a component to the entity with no value
add: <T>(self: World, id: Entity, component: Id) -> (), add: <T>(self: World, id: Entity, component: Id<T>) -> (),
--- Assigns a value to a component on the given entity --- Assigns a value to a component on the given entity
set: <T>(self: World, id: Entity, component: Id<T>, data: T) -> (), set: <T>(self: World, id: Entity, component: Id<T>, data: T) -> (),
cleanup: (self: World) -> (), cleanup: (self: World) -> (),
-- Clears an entity from the world -- Clears an entity from the world
clear: (self: World, id: Entity) -> (), clear: <T>(self: World, id: Id<T>) -> (),
--- Removes a component from the given entity --- Removes a component from the given entity
remove: (self: World, id: Entity, component: Id) -> (), remove: <T>(self: World, id: Entity, component: Id<T>) -> (),
--- Retrieves the value of up to 4 components. These values may be nil. --- Retrieves the value of up to 4 components. These values may be nil.
get: (<A>(self: World, id: Entity, Id<A>) -> A?) get: (<A>(self: World, id: Entity, Id<A>) -> A?)
& (<A, B>(self: World, id: Entity, Id<A>, Id<B>) -> (A?, B?)) & (<A, B>(self: World, id: Entity, Id<A>, Id<B>) -> (A?, B?))
@ -2431,9 +2493,9 @@ export type World = {
--- Checks if the world contains the given entity --- Checks if the world contains the given entity
contains:(self: World, entity: Entity) -> boolean, contains:(self: World, entity: Entity) -> boolean,
each: (self: World, id: Id) -> () -> Entity, each: <T>(self: World, id: Id<T>) -> () -> Entity,
children: (self: World, id: Id) -> () -> Entity, children: <T>(self: World, id: Id<T>) -> () -> Entity,
--- Searches the world for entities that match a given query --- Searches the world for entities that match a given query
query: (<A>(World, Id<A>) -> Query<A>) query: (<A>(World, Id<A>) -> Query<A>)
@ -2461,10 +2523,15 @@ export type World = {
-- return first -- return first
-- end -- end
-- end -- end
--
return { return {
World = World :: { new: () -> World }, World = World :: { new: () -> World },
world = world_new :: () -> World, world = world_new :: () -> World,
component = (ECS_COMPONENT :: any) :: <T>() -> Entity<T>,
tag = (ECS_TAG :: any) :: <T>() -> Entity<T>,
meta = (ECS_META :: any) :: <T>(id: Entity, id: Id<T>, value: T) -> Entity<T>,
is_tag = (ecs_is_tag :: any) :: <T>(World, Id<T>) -> boolean,
OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>, OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>,
OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>, OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>,
@ -2487,8 +2554,8 @@ return {
ECS_GENERATION_INC = ECS_GENERATION_INC, ECS_GENERATION_INC = ECS_GENERATION_INC,
ECS_GENERATION = ECS_GENERATION, ECS_GENERATION = ECS_GENERATION,
ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD, ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD,
ECS_ID_DELETE = ECS_ID_DELETE, ECS_ID_DELETE = ECS_ID_DELETE,
ECS_META_RESET = ECS_META_RESET,
IS_PAIR = (ECS_IS_PAIR :: any) :: <P, O>(pair: Pair<P, O>) -> boolean, IS_PAIR = (ECS_IS_PAIR :: any) :: <P, O>(pair: Pair<P, O>) -> boolean,
pair_first = (ecs_pair_first :: any) :: <P, O>(world: World, pair: Pair<P, O>) -> Id<P>, pair_first = (ecs_pair_first :: any) :: <P, O>(world: World, pair: Pair<P, O>) -> Id<P>,

View file

@ -82,6 +82,25 @@ TEST("addons/observers", function()
world:set(e, A, true) world:set(e, A, true)
CHECK(count == 3) CHECK(count == 3)
end end
do CASE "Call on pairs"
local A = world:component()
local callcount = 0
world:added(A, function(entity)
callcount += 1
end)
world:added(A, function(entity)
callcount += 1
end)
local e = world:entity()
local e1 = world:entity()
world:add(e1, jecs.pair(A, e))
world:add(e, jecs.pair(A, e1))
CHECK(callcount == 4)
end
end) end)
return FINISH() return FINISH()

View file

@ -22,6 +22,7 @@ type Entity<T=nil> = jecs.Entity<T>
type Id<T=unknown> = jecs.Id<T> type Id<T=unknown> = jecs.Id<T>
local entity_visualiser = require("@tools/entity_visualiser") local entity_visualiser = require("@tools/entity_visualiser")
local lifetime_tracker_add = require("@tools/lifetime_tracker")
local dwi = entity_visualiser.stringify local dwi = entity_visualiser.stringify
TEST("world:add()", function() TEST("world:add()", function()
@ -62,8 +63,8 @@ end)
TEST("world:children()", function() TEST("world:children()", function()
local world = jecs.world() local world = jecs.world()
local C = world:component() local C = jecs.component()
local T = world:entity() local T = jecs.tag()
local e1 = world:entity() local e1 = world:entity()
world:set(e1, C, true) world:set(e1, C, true)
@ -95,6 +96,8 @@ TEST("world:children()", function()
end end
CHECK(count == 1) CHECK(count == 1)
jecs.ECS_META_RESET()
end) end)
TEST("world:clear()", function() TEST("world:clear()", function()
@ -195,6 +198,26 @@ TEST("world:clear()", function()
end) end)
TEST("world:component()", function() TEST("world:component()", function()
do CASE "allow IDs to be registered"
local A = jecs.component()
local B = jecs.component()
local world = jecs.world()
local C = world:component()
CHECK((A :: any) == 1)
CHECK((B :: any) == 2)
CHECK((C :: any) == 3)
local e = world:entity()
world:set(e, A, "foo")
world:set(e, B, "foo")
world:set(e, C, "foo")
CHECK(world:has(e, A, B, C))
jecs.ECS_META_RESET() -- Reset the ECS metadata because they may have side effects
end
do CASE "only components should have EcsComponent trait" do CASE "only components should have EcsComponent trait"
local world = jecs.world() local world = jecs.world()
local A = world:component() local A = world:component()
@ -224,6 +247,7 @@ TEST("world:component()", function()
end) end)
TEST("world:contains()", function() TEST("world:contains()", function()
local tag = jecs.tag()
local world = jecs.world() local world = jecs.world()
local id = world:entity() local id = world:entity()
CHECK(world:contains(id)) CHECK(world:contains(id))
@ -233,6 +257,9 @@ TEST("world:contains()", function()
world:delete(id) world:delete(id)
CHECK(not world:contains(id)) CHECK(not world:contains(id))
end end
CHECK(world:contains(tag))
jecs.ECS_META_RESET()
end) end)
TEST("world:delete()", function() TEST("world:delete()", function()
@ -606,6 +633,7 @@ end)
TEST("world:entity()", function() TEST("world:entity()", function()
local N = 2^8 local N = 2^8
do CASE "unique IDs" do CASE "unique IDs"
local world = jecs.world() local world = jecs.world()
local set = {} local set = {}
@ -720,21 +748,21 @@ TEST("world:query()", function()
local i = 0 local i = 0
local iter = 0 local iter = 0
for _, e in q:iter() do for _ in q:iter() do
iter += 1 iter += 1
i=1 i=1
end end
CHECK (iter == 1) CHECK (iter == 1)
CHECK(i == 1) CHECK(i == 1)
for _, e in q:iter() do for _ in q:iter() do
i=2 i=2
end end
CHECK(i == 2) CHECK(i == 2)
for _, e in q :: any do for _ in q do
i=3 i=3
end end
CHECK(i == 3) CHECK(i == 3)
for _, e in q :: any do for _ in q do
i=4 i=4
end end
CHECK(i == 4) CHECK(i == 4)
@ -746,8 +774,8 @@ TEST("world:query()", function()
end end
do CASE "multiple iter" do CASE "multiple iter"
local world = jecs.World.new() local world = jecs.World.new()
local A = world:component() local A = world:component() :: jecs.Entity<string>
local B = world:component() local B = world:component() :: jecs.Entity<string>
local e = world:entity() local e = world:entity()
world:add(e, A) world:add(e, A)
world:add(e, B) world:add(e, B)
@ -1405,8 +1433,6 @@ TEST("#adding a recycled target", function()
end) end)
TEST("#repro2", function() TEST("#repro2", function()
local world = jecs.world() local world = jecs.world()
local Lifetime = world:component() :: Id<number> local Lifetime = world:component() :: Id<number>

View file

@ -226,6 +226,8 @@ local function CHECK<T>(value: T, stack: number?): T?
return value return value
end end
local test_focused = false
local function TEST(name: string, fn: () -> ()) local function TEST(name: string, fn: () -> ())
test = { test = {
@ -238,6 +240,10 @@ local function TEST(name: string, fn: () -> ())
local t = test local t = test
if check_for_focused and not test_focused then
test.focus = true
test_focused = true
end
table.insert(tests, t) table.insert(tests, t)
end end
@ -245,15 +251,10 @@ local function FOCUS()
assert(test, "no active test") assert(test, "no active test")
check_for_focused = true check_for_focused = true
if test.case then test_focused = false
test.case.focus = true
else
test.focus = true
end
end end
local function FINISH(): number local function FINISH(): number
local success = true
local total_cases = 0 local total_cases = 0
local passed_cases = 0 local passed_cases = 0
local passed_focus_cases = 0 local passed_focus_cases = 0