jecs/src/jecs.luau

3533 lines
88 KiB
Text
Raw Normal View History

2024-04-23 15:10:49 +00:00
--!optimize 2
--!native
--!strict
--draft 4
type i53 = number
type i24 = number
2025-06-25 11:31:25 +00:00
type Ty = { Entity }
2024-04-23 15:10:49 +00:00
type ArchetypeId = number
type Column = { any }
2024-04-23 15:10:49 +00:00
2024-09-21 22:52:58 +00:00
type Map<K, V> = { [K]: V }
2025-06-25 11:31:25 +00:00
export type Archetype = {
2025-03-25 22:13:53 +00:00
id: number,
types: Ty,
type: string,
2025-06-25 11:31:25 +00:00
entities: { Entity },
2025-03-25 22:13:53 +00:00
columns: { Column },
2025-11-18 20:02:39 +00:00
columns_map: { [Component]: Column }
}
2025-03-25 22:13:53 +00:00
2025-06-25 11:31:25 +00:00
export type QueryInner = {
compatible_archetypes: { Archetype },
2025-11-18 20:02:39 +00:00
archetypes_map: { [number]: number },
ids: { Component },
filter_with: { Component },
filter_without: { Component },
next: () -> (Entity, ...any),
2025-12-09 19:34:05 +00:00
-- world: World,
2025-06-25 11:31:25 +00:00
}
2025-11-18 20:02:39 +00:00
type function ecs_entity_t(ty: type)
if ty:is("union") then
for _, component in ty:components() do
assert(not component:readproperty(types.singleton("__IS_PAIR")), "Expected Entity got Pair")
end
end
assert(ty:readproperty(types.singleton("__T")), "Expected Entity")
assert(not ty:readproperty(types.singleton("__IS_PAIR")), "Expected Entity got Pair")
return ty
end
type function ecs_pair_t(first: type, second: type)
local __T = types.singleton("__T")
local __IS_PAIR = types.singleton("__IS_PAIR")
local first_t = first:readproperty(__T)
local second_t = second:readproperty(__T)
assert(first_t and second_t, "Expected at least one Entity in pair")
local id = types.newtable()
local ty = if first_t:is("nil") then second_t else first_t
id:setproperty(__T, ty)
id:setproperty(__IS_PAIR, types.singleton(true))
return id
end
type function ecs_id_t(first: type, second: type)
local __T = types.singleton("__T")
2025-10-21 22:36:00 +00:00
local p = ecs_pair_t(Entity(first), Entity(second))
if second:is("nil") then
return first
2025-11-18 20:02:39 +00:00
end
2025-10-21 22:36:00 +00:00
return p
2025-11-18 20:02:39 +00:00
end
2025-12-09 19:34:05 +00:00
export type Entity<T = nil> = { __T: T }
2025-10-21 22:36:00 +00:00
export type Id<T = any> = { __T: T }
2025-11-18 20:02:39 +00:00
export type Pair<First=any, Second=any> = ecs_pair_t<Entity<First>, Entity<Second>>
export type Component<T=any> = { __T: T }
2025-10-21 22:36:00 +00:00
export type Id2<First, Second=nil> = ecs_id_t<First, Second>
2025-11-18 20:02:39 +00:00
2025-06-25 11:31:25 +00:00
export type Item<T...> = (self: Query<T...>) -> (Entity, T...)
export type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
2025-11-18 20:02:39 +00:00
export type CachedIter<T...> = (query: CachedQuery<T...>) -> () -> (Entity, T...)
2025-12-09 19:34:05 +00:00
type TypePack<T...> = (T...) -> never
2025-11-18 20:02:39 +00:00
export type CachedQuery<T...> = typeof(setmetatable(
{} :: {
iter: CachedIter<T...>,
2025-12-09 19:34:05 +00:00
archetypes: (CachedQuery<T...>) -> { Archetype },
2025-11-18 20:02:39 +00:00
has: (CachedQuery<T...>, Entity) -> boolean,
ids: { Id<any> },
2025-10-21 22:36:00 +00:00
filter_with: { Id<any> }?,
filter_without: { Id<any> }?,
2025-11-18 20:02:39 +00:00
archetypes_map: { [number]: number },
-- world: World
},
{} :: {
__iter: CachedIter<T...>,
})
)
2025-06-25 11:31:25 +00:00
export type Query<T...> = typeof(setmetatable(
{} :: {
iter: Iter<T...>,
2025-11-18 20:02:39 +00:00
with: ((Query<T...>, ...Component) -> Query<T...>),
without: ((Query<T...>, ...Component) -> Query<T...>),
2025-12-09 19:34:05 +00:00
archetypes: (Query<T...>) -> { Archetype },
cached: (Query<T...>) -> CachedQuery<T...>,
2025-11-18 20:02:39 +00:00
has: (Query<T...>, Entity) -> boolean,
2025-07-27 12:39:43 +00:00
ids: { Id<any> },
2025-10-21 22:36:00 +00:00
filter_with: { Id<any> }?,
filter_without: { Id<any> }?,
2025-07-27 12:39:43 +00:00
-- world: World
2025-06-25 11:31:25 +00:00
},
{} :: {
2025-06-25 19:07:58 +00:00
__iter: Iter<T...>,
2025-06-25 11:31:25 +00:00
}
))
type QueryArm<T...> = () -> ()
2025-06-25 11:31:25 +00:00
export type Observer = {
2025-06-25 19:07:58 +00:00
callback: (archetype: Archetype) -> (),
query: QueryInner,
2025-06-25 11:31:25 +00:00
}
2025-08-05 23:40:40 +00:00
type query = {
compatible_archetypes: { archetype },
ids: { i53 },
filter_with: { i53 },
filter_without: { i53 },
next: () -> (i53, ...any),
2025-08-22 20:07:30 +00:00
world: world,
2025-08-05 23:40:40 +00:00
}
export type observer = {
callback: (archetype: archetype) -> (),
query: query,
}
2025-07-23 23:38:05 +00:00
type archetype = {
id: number,
types: { i53 },
type: string,
entities: { i53 },
columns: { Column },
columns_map: { [i53]: Column }
}
type componentrecord = {
2025-08-29 15:13:13 +00:00
component: i53,
2025-08-05 23:40:40 +00:00
records: { [number]: number },
2025-07-23 23:38:05 +00:00
counts: { [i53]: number },
flags: number,
size: number,
2025-08-29 15:13:13 +00:00
on_add: ((entity: i53, id: i53, value: any, oldarchetype: archetype) -> ())?,
on_change: ((entity: i53, id: i53, value: any, oldarchetype: archetype) -> ())?,
2025-10-21 22:36:00 +00:00
on_remove: ((entity: i53, id: i53, delete: boolean?) -> ())?,
2025-08-05 23:40:40 +00:00
wildcard_pairs: { [number]: componentrecord },
2025-07-23 23:38:05 +00:00
}
type record = {
archetype: archetype,
row: number,
dense: i24,
}
type entityindex = {
dense_array: Map<number, i53>,
sparse_array: Map<i24, record>,
alive_count: number,
max_id: number,
range_begin: number?,
range_end: number?,
}
type world = {
archetype_edges: Map<i53, Map<i53, archetype>>,
archetype_index: { [string]: archetype },
archetypes: { [i53]: archetype },
component_index: Map<i53, componentrecord>,
entity_index: entityindex,
ROOT_ARCHETYPE: archetype,
max_component_id: number,
max_archetype_id: number,
observable: Map<i53, Map<i53, { Observer }>>,
range: (self: world, range_begin: number, range_end: number?) -> (),
entity: (self: world, id: i53?) -> i53,
component: (self: world) -> i53,
target: (self: world, id: i53, relation: i53, index: number?) -> i53?,
delete: (self: world, id: i53) -> (),
add: (self: world, id: i53, component: i53) -> (),
set: (self: world, id: i53, component: i53, data: any) -> (),
cleanup: (self: world) -> (),
2025-08-28 15:10:23 +00:00
clear: (self: world, entity: i53) -> (),
2025-07-23 23:38:05 +00:00
remove: (self: world, id: i53, component: i53) -> (),
get: (world, ...i53) -> (),
has: (world, ...i53) -> boolean,
parent: (self: world, entity: i53) -> i53?,
contains: (self: world, entity: i53) -> boolean,
exists: (self: world, entity: i53) -> boolean,
each: (self: world, id: i53) -> () -> i53,
children: (self: world, id: i53) -> () -> i53,
2025-07-27 12:39:43 +00:00
query: (world, ...i53) -> Query<...any>,
added: (world, i53, (e: i53, id: i53, value: any?) -> ()) -> () -> (),
changed: (world, i53, (e: i53, id: i53, value: any?) -> ()) -> () -> (),
2025-10-21 22:36:00 +00:00
removed: (world, i53, (e: i53, id: i53, delete: boolean?) -> ()) -> () -> (),
2025-07-23 23:38:05 +00:00
}
2025-11-18 20:02:39 +00:00
2025-06-25 11:31:25 +00:00
export type World = {
2025-07-23 23:38:05 +00:00
archetype_edges: Map<number, Map<Entity, Archetype>>,
2025-06-25 11:31:25 +00:00
archetype_index: { [string]: Archetype },
archetypes: Archetypes,
component_index: ComponentIndex,
entity_index: EntityIndex,
ROOT_ARCHETYPE: Archetype,
max_component_id: number,
max_archetype_id: number,
2025-11-18 20:02:39 +00:00
observable: Map<Component, Map<Component, { Observer }>>,
2025-06-25 11:31:25 +00:00
2025-11-18 20:02:39 +00:00
added: <T>(World, Component<T>, (e: Entity, id: Id<T>, value: T, oldarchetype: Archetype) -> ()) -> () -> (),
2025-10-21 22:36:00 +00:00
removed: <T>(World, Component<T>, (e: Entity, id: Id<T>, delete: boolean?) -> ()) -> () -> (),
2025-11-18 20:12:47 +00:00
changed: <T>(World, Component<T>, (e: Entity, id: Id<T>, value: T, oldarchetype: Archetype) -> ()) -> () -> (),
2025-07-27 12:39:43 +00:00
2025-06-25 11:31:25 +00:00
--- Enforce a check on entities to be created within desired range
range: (self: World, range_begin: number, range_end: number?) -> (),
--- Creates a new entity
2025-11-18 20:02:39 +00:00
entity:
& ((self: World) -> Entity<nil>)
& ((self: World, id: Entity) -> Entity)
& ((self: World, id: number) -> Entity)
& (<T>(self: World, id: Component<T>) -> Component<T>),
2025-06-25 11:31:25 +00:00
--- Creates a new entity located in the first 256 ids.
--- These should be used for static components for fast access.
component: <T>(self: World) -> Entity<T>,
--- Gets the target of an relationship. For example, when a user calls
--- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity.
2025-12-09 19:34:05 +00:00
target: <T, a>(self: World, id: Entity<T>, relation: ecs_entity_t<Component>, index: number?) -> Entity<unknown>?,
--- Deletes an entity and all it's related components and relationships.
2025-06-25 11:31:25 +00:00
delete: <T>(self: World, id: Entity<T>) -> (),
--- Adds a component to the entity with no value
2025-11-18 20:02:39 +00:00
add: <a>(
self: World,
2025-12-09 19:34:05 +00:00
id: ecs_entity_t<Entity<any>>,
2025-11-18 20:02:39 +00:00
component: Component<a>
) -> (),
2025-06-25 11:31:25 +00:00
--- Assigns a value to a component on the given entity
2025-11-18 20:02:39 +00:00
set: <T, a>(self: World, id: Entity<T>, component: Component<a>, data: a) -> (),
2025-06-25 11:31:25 +00:00
cleanup: (self: World) -> (),
2025-08-28 15:10:23 +00:00
-- Removes all components from the entity
clear: (self: World, entity: Entity) -> (),
2025-06-25 11:31:25 +00:00
--- Removes a component from the given entity
2025-11-18 20:02:39 +00:00
remove: <T, a>(self: World, id: Entity<T>, component: Component<a>) -> (),
2025-06-25 11:31:25 +00:00
--- Retrieves the value of up to 4 components. These values may be nil.
2025-12-09 19:34:05 +00:00
get: & (<T, a>(World, Entity<T> | number, Component<a>) -> a?)
& (<T, a, b>(World, Entity<T> | number, Component<a>, Component<b>) -> (a?, b?))
& (<T, a, b, c>(World, Entity<T> | number, Component<a>, Component<b>, Component<c>) -> (a?, b?, c?))
& (<T, a, b, c, d>(World, Entity<T> | number, Component<a>, Component<b>, Component<c>, Component<d>) -> (a?, b?, c?, d?)),
2025-06-25 11:31:25 +00:00
--- Returns whether the entity has the ID.
2025-11-18 20:02:39 +00:00
has: (<T, a>(World, Entity<T>, Component<a>) -> boolean)
& (<T, a, b>(World, Entity<T>, Component<a>, Component<a>) -> boolean)
& (<T, a, b, c>(World, Entity<T>, Component<a>, Component<b>, Component<c>) -> boolean)
& <T, a, b, c, d>(World, Entity<T>, Component<a>, Component<b>, Component<c>, Component<d>) -> boolean,
2025-06-25 11:31:25 +00:00
--- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil.
parent: <T>(self: World, entity: Entity<T>) -> Entity?,
--- Checks if the world contains the given entity
contains: <T>(self: World, entity: Entity<T>) -> boolean,
--- Checks if the entity exists
exists: <T>(self: World, entity: Entity<T>) -> boolean,
each: <T>(self: World, id: Id<T>) -> () -> Entity,
children: <T>(self: World, id: Id<T>) -> () -> Entity,
--- Searches the world for entities that match a given query
2025-08-31 01:38:06 +00:00
query: ((World) -> Query<nil>)
2025-11-18 20:02:39 +00:00
& (<A>(World, Component<A>) -> Query<A>)
& (<A, B>(World, Component<A>, Component<B>) -> Query<A, B>)
& (<A, B, C>(World, Component<A>, Component<B>, Component<C>) -> Query<A, B, C>)
& (<A, B, C, D>(World, Component<A>, Component<B>, Component<C>, Component<D>) -> Query<A, B, C, D>)
& (<A, B, C, D, E>(World, Component<A>, Component<B>, Component<C>, Component<D>, Component<E>) -> Query<A, B, C, D, E>)
& (<A, B, C, D, E, F>(World, Component<A>, Component<B>, Component<C>, Component<D>, Component<E>, Component<F>) -> Query<A, B, C, D, E, F>)
2025-06-25 19:07:58 +00:00
& (<A, B, C, D, E, F, G>(
World,
2025-11-18 20:02:39 +00:00
Component<A>,
Component<B>,
Component<C>,
Component<D>,
Component<E>,
Component<F>,
Component<G>
2025-06-25 19:07:58 +00:00
) -> Query<A, B, C, D, E, F, G>)
& (<A, B, C, D, E, F, G, H>(
World,
2025-11-18 20:02:39 +00:00
Component<A>,
Component<B>,
Component<C>,
Component<D>,
Component<E>,
Component<F>,
Component<G>,
Component<H>,
...Component<any>
2025-06-25 19:07:58 +00:00
) -> Query<A, B, C, D, E, F, G, H>),
2025-03-25 22:13:53 +00:00
}
2025-06-25 11:31:25 +00:00
export type Record = {
archetype: Archetype,
2024-04-23 15:10:49 +00:00
row: number,
2025-03-27 01:33:38 +00:00
dense: i24,
2024-04-23 15:10:49 +00:00
}
2025-06-25 11:31:25 +00:00
export type ComponentRecord = {
2025-07-23 23:38:05 +00:00
records: { [i24]: number },
counts: { [i24]: number },
flags: number,
size: number,
2025-08-29 15:13:13 +00:00
on_add: (<T>(entity: Entity, id: Entity<T>, value: T, oldarchetype: Archetype) -> ())?,
on_change: (<T>(entity: Entity, id: Entity<T>, value: T, oldArchetype: Archetype) -> ())?,
on_remove: ((entity: Entity, id: Entity) -> ())?,
2024-05-16 22:17:53 +00:00
}
2025-11-18 20:02:39 +00:00
export type ComponentIndex = Map<Component, ComponentRecord>
2025-07-23 23:38:05 +00:00
export type Archetypes = { [i24]: Archetype }
2024-05-16 22:17:53 +00:00
2025-06-25 11:31:25 +00:00
export type EntityIndex = {
dense_array: Map<number, Entity>,
sparse_array: Map<i24, Record>,
alive_count: number,
max_id: number,
range_begin: number?,
2025-06-25 19:07:58 +00:00
range_end: number?,
}
2024-11-10 03:14:08 +00:00
-- stylua: ignore start
2025-06-07 00:35:52 +00:00
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local ECS_PAIR_OFFSET = 2^48
local ECS_ID_DELETE = 0b0001
local ECS_ID_IS_TAG = 0b0010
local ECS_ID_IS_EXCLUSIVE = 0b0100
local ECS_ID_MASK = 0b0000
2025-06-07 00:35:52 +00:00
local HI_COMPONENT_ID = 256
local EcsOnAdd = HI_COMPONENT_ID + 1
local EcsOnRemove = HI_COMPONENT_ID + 2
local EcsOnChange = HI_COMPONENT_ID + 3
local EcsWildcard = HI_COMPONENT_ID + 4
local EcsChildOf = HI_COMPONENT_ID + 5
local EcsComponent = HI_COMPONENT_ID + 6
local EcsOnDelete = HI_COMPONENT_ID + 7
local EcsOnDeleteTarget = HI_COMPONENT_ID + 8
local EcsDelete = HI_COMPONENT_ID + 9
local EcsRemove = HI_COMPONENT_ID + 10
local EcsName = HI_COMPONENT_ID + 11
local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12
local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13
local EcsExclusive = HI_COMPONENT_ID + 14
local EcsRest = HI_COMPONENT_ID + 15
local NULL_ARRAY = table.freeze({}) :: Column
local NULL = newproxy(false)
2025-03-26 02:39:04 +00:00
local ECS_INTERNAL_ERROR = [[
This is an internal error, please file a bug report via the following link:
2025-03-26 02:39:04 +00:00
https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md
]]
local function ecs_assert(condition, msg: string?)
if not condition then
error(msg)
end
end
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
2025-07-23 23:38:05 +00:00
bundle = {} :: Map<i53, any>
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
2025-03-24 16:36:13 +00:00
local function ECS_COMBINE(id: number, generation: number): i53
return id + (generation * ECS_ENTITY_MASK)
end
local function ECS_IS_PAIR(e: number): boolean
2025-03-24 16:36:13 +00:00
return e > ECS_PAIR_OFFSET
end
2025-03-25 22:13:53 +00:00
local function ECS_GENERATION_INC(e: i53): i53
2024-06-05 22:38:27 +00:00
if e > ECS_ENTITY_MASK then
2025-03-24 16:36:13 +00:00
local id = e % ECS_ENTITY_MASK
local generation = e // ECS_ENTITY_MASK
local next_gen = generation + 1
2025-03-24 16:36:13 +00:00
if next_gen >= ECS_GENERATION_MASK then
return id
end
return ECS_COMBINE(id, next_gen)
2024-06-05 22:38:27 +00:00
end
return ECS_COMBINE(e, 1)
end
2025-03-24 16:36:13 +00:00
local function ECS_ENTITY_T_LO(e: i53): i24
return e % ECS_ENTITY_MASK
end
local function ECS_ID(e: i53)
return e % ECS_ENTITY_MASK
end
2025-03-24 16:36:13 +00:00
local function ECS_GENERATION(e: i53)
return e // ECS_ENTITY_MASK
end
2025-03-24 16:36:13 +00:00
local function ECS_ENTITY_T_HI(e: i53): i24
return e // ECS_ENTITY_MASK
end
local function ECS_PAIR(pred: i53, obj: i53): i53
2025-03-24 16:36:13 +00:00
pred %= ECS_ENTITY_MASK
obj %= ECS_ENTITY_MASK
2025-03-25 22:13:53 +00:00
return obj + (pred * ECS_ENTITY_MASK) + ECS_PAIR_OFFSET
end
2025-03-26 02:39:04 +00:00
local function ECS_PAIR_FIRST(e: i53): i24
return (e - ECS_PAIR_OFFSET) // ECS_ENTITY_MASK
end
local function ECS_PAIR_SECOND(e: i53): i24
return (e - ECS_PAIR_OFFSET) % ECS_ENTITY_MASK
end
2025-03-25 22:13:53 +00:00
local function entity_index_try_get_any(
2025-07-23 23:38:05 +00:00
entity_index: entityindex,
entity: i53
): record?
local r = entity_index.sparse_array[ECS_ID(entity)]
2024-07-03 15:48:32 +00:00
if not r or r.dense == 0 then
return nil
end
2024-07-03 15:48:32 +00:00
return r
end
2025-07-23 23:38:05 +00:00
local function entity_index_try_get(entity_index: entityindex, entity: i53): record?
local r = entity_index_try_get_any(entity_index, entity)
if r then
local r_dense = r.dense
if r_dense > entity_index.alive_count then
return nil
end
if entity_index.dense_array[r_dense] ~= entity then
return nil
2024-07-03 15:48:32 +00:00
end
end
return r
end
2024-07-03 15:48:32 +00:00
2025-07-23 23:38:05 +00:00
local function entity_index_try_get_fast(entity_index: entityindex, entity: i53): record?
local r = entity_index_try_get_any(entity_index, entity)
if r then
local r_dense = r.dense
-- if r_dense > entity_index.alive_count then
-- return nil
-- end
if entity_index.dense_array[r_dense] ~= entity then
return nil
end
end
return r
end
2025-07-23 23:38:05 +00:00
local function entity_index_is_alive(entity_index: entityindex, entity: i53): boolean
2025-03-26 02:39:04 +00:00
return entity_index_try_get(entity_index, entity) ~= nil
end
2025-07-23 23:38:05 +00:00
local function entity_index_get_alive(entity_index: entityindex, entity: i53): i53?
2025-06-25 11:31:25 +00:00
local r = entity_index_try_get_any(entity_index, entity :: number)
if r then
return entity_index.dense_array[r.dense]
2024-07-03 15:48:32 +00:00
end
return nil
end
2024-07-03 15:48:32 +00:00
2025-07-23 23:38:05 +00:00
local function ecs_get_alive(world: world, entity: i53): i53
2025-03-26 02:39:04 +00:00
if entity == 0 then
return 0
end
local eindex = world.entity_index
if entity_index_is_alive(eindex, entity) then
return entity
end
2025-06-25 11:31:25 +00:00
if (entity :: number) > ECS_ENTITY_MASK then
2025-03-26 02:39:04 +00:00
return 0
end
local current = entity_index_get_alive(eindex, entity)
if not current or not entity_index_is_alive(eindex, current) then
return 0
end
return current
2024-07-03 15:48:32 +00:00
end
local ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY = "Entity is outside range"
2025-08-28 15:10:23 +00:00
local function ENTITY_INDEX_NEW_ID(entity_index: entityindex): i53
local dense_array = entity_index.dense_array
local alive_count = entity_index.alive_count
local sparse_array = entity_index.sparse_array
2025-03-25 22:13:53 +00:00
local max_id = entity_index.max_id
if alive_count < max_id then
alive_count += 1
entity_index.alive_count = alive_count
local id = dense_array[alive_count]
return id
end
2025-03-25 22:13:53 +00:00
local id = max_id + 1
local range_end = entity_index.range_end
ecs_assert(range_end == nil or id < range_end, ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY)
entity_index.max_id = id
alive_count += 1
entity_index.alive_count = alive_count
dense_array[alive_count] = id
2025-07-23 23:38:05 +00:00
sparse_array[id] = { dense = alive_count } :: record
return id
end
2025-07-23 23:38:05 +00:00
local function ecs_pair_first(world: world, e: i53)
2025-03-26 02:39:04 +00:00
local pred = ECS_PAIR_FIRST(e)
return ecs_get_alive(world, pred)
end
2024-05-14 15:52:41 +00:00
2025-07-23 23:38:05 +00:00
local function ecs_pair_second(world: world, e: i53)
2025-03-26 02:39:04 +00:00
local obj = ECS_PAIR_SECOND(e)
return ecs_get_alive(world, obj)
end
2024-04-23 15:10:49 +00:00
local function query_match(query: query, archetype: archetype)
2025-06-25 11:31:25 +00:00
local columns_map = archetype.columns_map
local with = query.filter_with
for _, id in with do
2025-06-25 11:31:25 +00:00
if not columns_map[id] then
return false
end
end
local without = query.filter_without
if without then
for _, id in without do
2025-06-25 11:31:25 +00:00
if columns_map[id] then
return false
end
end
end
return true
end
local function find_observers(world: world, event: i53, component: i53): { observer }?
2025-01-29 07:28:08 +00:00
local cache = world.observable[event]
if not cache then
return nil
end
return cache[component] :: any
end
2025-03-25 22:13:53 +00:00
local function archetype_move(
2025-07-23 23:38:05 +00:00
entity_index: entityindex,
entity: i53,
to: archetype,
2025-03-25 22:13:53 +00:00
dst_row: i24,
2025-07-23 23:38:05 +00:00
from: archetype,
2025-03-25 22:13:53 +00:00
src_row: i24
)
2024-08-12 20:43:46 +00:00
local src_columns = from.columns
local dst_entities = to.entities
local src_entities = from.entities
local last = #src_entities
local id_types = from.types
2025-06-25 11:31:25 +00:00
local columns_map = to.columns_map
2025-06-30 20:37:20 +00:00
if src_row ~= last then
-- If the entity is the last row in the archetype then swapping it would be meaningless.
2025-06-30 20:37:20 +00:00
for i, column in src_columns do
if column == NULL_ARRAY then
continue
end
-- Retrieves the new column index from the source archetype's record from each component
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
local dst_column = columns_map[id_types[i]]
-- Sometimes target column may not exist, e.g. when you remove a component.
if dst_column then
dst_column[dst_row] = column[src_row]
end
-- Swap rempves columns to ensure there are no holes in the archetype.
2024-08-12 20:43:46 +00:00
column[src_row] = column[last]
2025-06-30 20:37:20 +00:00
column[last] = nil
end
2024-04-23 15:10:49 +00:00
2024-04-30 14:05:31 +00:00
2025-06-30 20:37:20 +00:00
-- Move the entity from the source to the destination archetype.
-- Because we have swapped columns we now have to update the records
-- corresponding to the entities' rows that were swapped.
2025-06-30 20:37:20 +00:00
local e2 = src_entities[last]
2024-08-12 20:43:46 +00:00
src_entities[src_row] = e2
2025-06-30 20:37:20 +00:00
local sparse_array = entity_index.sparse_array
2025-07-23 23:38:05 +00:00
local record2 = sparse_array[ECS_ID(e2)]
2025-06-30 20:37:20 +00:00
record2.row = src_row
else
for i, column in src_columns do
if column == NULL_ARRAY then
continue
end
-- Retrieves the new column index from the source archetype's record from each component
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
local dst_column = columns_map[id_types[i]]
2025-06-30 20:37:20 +00:00
-- Sometimes target column may not exist, e.g. when you remove a component.
if dst_column then
dst_column[dst_row] = column[src_row]
end
column[last] = nil
end
end
2025-07-23 23:38:05 +00:00
src_entities[last] = nil
2025-06-30 20:37:20 +00:00
dst_entities[dst_row] = entity
2024-04-23 15:10:49 +00:00
end
2025-03-25 22:13:53 +00:00
local function archetype_append(
2025-07-23 23:38:05 +00:00
entity: i53,
archetype: archetype
2025-03-25 22:13:53 +00:00
): number
2024-04-23 15:10:49 +00:00
local entities = archetype.entities
local length = #entities + 1
entities[length] = entity
return length
2024-04-23 15:10:49 +00:00
end
2025-03-25 22:13:53 +00:00
local function new_entity(
2025-07-23 23:38:05 +00:00
entity: i53,
record: record,
archetype: archetype
): record
local row = archetype_append(entity, archetype)
2024-04-23 15:10:49 +00:00
record.archetype = archetype
record.row = row
return record
end
2025-03-25 22:13:53 +00:00
local function entity_move(
2025-07-23 23:38:05 +00:00
entity_index: entityindex,
entity: i53,
record: record,
to: archetype
2025-03-25 22:13:53 +00:00
)
2024-04-23 15:10:49 +00:00
local sourceRow = record.row
local from = record.archetype
local dst_row = archetype_append(entity, to)
2025-06-30 20:37:20 +00:00
archetype_move(entity_index, entity, to, dst_row, from, sourceRow)
2024-04-23 15:10:49 +00:00
record.archetype = to
2024-07-14 04:35:13 +00:00
record.row = dst_row
2024-04-23 15:10:49 +00:00
end
2025-07-23 23:38:05 +00:00
local function hash(arr: { i53 }): string
2024-04-28 14:46:40 +00:00
return table.concat(arr, "_")
2024-04-23 15:10:49 +00:00
end
2025-07-23 23:38:05 +00:00
local function fetch(id: i53, columns_map: { [i53]: Column }, row: number): any
2025-06-25 11:31:25 +00:00
local column = columns_map[id]
2025-06-25 11:31:25 +00:00
if not column then
2024-11-23 19:50:15 +00:00
return nil
end
2025-06-25 11:31:25 +00:00
return column[row]
2024-11-23 19:50:15 +00:00
end
2025-08-28 15:10:23 +00:00
local function WORLD_GET(world: world, entity: i53,
2025-07-23 23:38:05 +00:00
a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any
2025-06-25 11:31:25 +00:00
local record = entity_index_try_get(world.entity_index, entity)
2024-11-23 19:50:15 +00:00
if not record then
return nil
end
2024-11-23 19:50:15 +00:00
local archetype = record.archetype
if not archetype then
return nil
end
2025-06-25 11:31:25 +00:00
local columns_map = archetype.columns_map
2024-11-23 19:50:15 +00:00
local row = record.row
2025-06-25 11:31:25 +00:00
local va = fetch(a, columns_map, row)
2024-11-23 19:50:15 +00:00
if not b then
return va
elseif not c then
2025-06-27 15:42:54 +00:00
return va, fetch(b, columns_map, row)
2024-11-23 19:50:15 +00:00
elseif not d then
2025-06-25 11:31:25 +00:00
return va, fetch(b, columns_map, row), fetch(c, columns_map, row)
2024-11-23 19:50:15 +00:00
elseif not e then
2025-06-25 11:31:25 +00:00
return va, fetch(b, columns_map, row), fetch(c, columns_map, row), fetch(d, columns_map, row)
2024-11-23 19:50:15 +00:00
else
error("args exceeded")
end
2024-08-11 13:03:05 +00:00
end
2025-08-28 15:10:23 +00:00
local function WORLD_HAS(world: world, entity: i53, id: i53): boolean
2025-06-25 11:31:25 +00:00
local record = entity_index_try_get(world.entity_index, entity)
if not record then
return false
end
local archetype = record.archetype
if not archetype then
return false
end
2025-06-25 11:31:25 +00:00
return archetype.columns_map[id] ~= nil
end
2025-08-28 15:10:23 +00:00
local function WORLD_TARGET(world: world, entity: i53, relation: i53, index: number?): i53?
2025-06-25 11:31:25 +00:00
local entity_index = world.entity_index
local record = entity_index_try_get(entity_index, entity)
if not record then
2025-06-25 11:31:25 +00:00
return nil
end
2024-08-11 13:03:05 +00:00
local archetype = record.archetype
if not archetype then
return nil
end
2025-07-23 23:38:05 +00:00
local r = ECS_PAIR(relation, EcsWildcard)
2025-06-25 11:31:25 +00:00
local idr = world.component_index[r]
if not idr then
2024-08-16 17:13:30 +00:00
return nil
end
2025-06-25 11:31:25 +00:00
local archetype_id = archetype.id
local count = idr.counts[archetype_id]
if not count then
2024-08-16 17:13:30 +00:00
return nil
end
2025-06-25 11:31:25 +00:00
local nth = index or 0
2025-03-26 02:39:04 +00:00
if nth >= count then
2025-08-02 21:57:50 +00:00
return nil
end
2025-06-25 11:31:25 +00:00
nth = archetype.types[nth + idr.records[archetype_id]]
if not nth then
2024-09-21 22:52:58 +00:00
return nil
end
2025-06-25 11:31:25 +00:00
return entity_index_get_alive(entity_index,
ECS_PAIR_SECOND(nth :: number))
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
2024-08-16 17:13:30 +00:00
end
2025-06-25 11:31:25 +00:00
local function id_record_get(world: World, id: Entity): ComponentRecord?
local component_index = world.component_index
local idr: ComponentRecord = component_index[id]
if idr then
return idr
end
return nil
end
2025-08-28 15:10:23 +00:00
local function id_record_create(
world: world,
component_index: Map<i53, componentrecord>,
id: i53
): componentrecord
2025-03-26 02:39:04 +00:00
local entity_index = world.entity_index
local flags = ECS_ID_MASK
local relation = id
local target = 0
2025-06-25 11:31:25 +00:00
local is_pair = ECS_IS_PAIR(id :: number)
local has_delete = false
local is_exclusive = false
if is_pair then
2025-07-23 23:38:05 +00:00
relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53
ecs_assert(relation and entity_index_is_alive(
entity_index, relation), ECS_INTERNAL_ERROR)
2025-07-23 23:38:05 +00:00
target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53
ecs_assert(target and entity_index_is_alive(
entity_index, target), ECS_INTERNAL_ERROR)
2025-08-28 15:10:23 +00:00
local cleanup_policy_target = WORLD_TARGET(world, relation, EcsOnDeleteTarget, 0)
if cleanup_policy_target == EcsDelete then
has_delete = true
end
2025-08-28 15:10:23 +00:00
if WORLD_HAS(world, relation, EcsExclusive) then
is_exclusive = true
end
2025-07-17 21:45:03 +00:00
end
2025-01-17 10:17:07 +00:00
2025-08-28 15:10:23 +00:00
local cleanup_policy = WORLD_TARGET(world, relation, EcsOnDelete, 0)
2025-07-17 21:45:03 +00:00
if cleanup_policy == EcsDelete then
has_delete = true
end
2025-08-28 15:10:23 +00:00
local on_add, on_change, on_remove = WORLD_GET(world,
relation, EcsOnAdd, EcsOnChange, EcsOnRemove)
2025-08-28 15:10:23 +00:00
local is_tag = not WORLD_HAS(world,
relation, EcsComponent)
if is_tag and is_pair then
2025-08-28 15:10:23 +00:00
is_tag = not WORLD_HAS(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,
if is_exclusive then ECS_ID_IS_EXCLUSIVE else 0
)
2025-08-28 15:10:23 +00:00
local idr = {
size = 0,
2025-06-25 11:31:25 +00:00
records = {},
counts = {},
flags = flags,
on_add = on_add,
on_change = on_change,
on_remove = on_remove,
2025-07-23 23:38:05 +00:00
} :: componentrecord
component_index[id] = idr
return idr
end
2025-08-28 15:10:23 +00:00
local function id_record_ensure(world: world, id: i53): componentrecord
local component_index = world.component_index
local idr: componentrecord? = component_index[id]
if idr then
return idr
end
return id_record_create(world, component_index, id)
end
local function archetype_append_to_records(
2025-07-23 23:38:05 +00:00
idr: componentrecord,
2025-06-25 11:31:25 +00:00
archetype_id: number,
2025-07-23 23:38:05 +00:00
columns_map: { [i53]: Column },
2025-03-25 22:13:53 +00:00
id: i53,
2025-06-25 11:31:25 +00:00
index: number,
column: Column
)
2025-06-25 11:31:25 +00:00
local idr_records = idr.records
local idr_counts = idr.counts
2025-06-25 11:31:25 +00:00
local tr = idr_records[archetype_id]
2024-09-21 22:52:58 +00:00
if not tr then
2025-06-25 11:31:25 +00:00
idr_records[archetype_id] = index
idr_counts[archetype_id] = 1
2025-06-25 11:31:25 +00:00
columns_map[id] = column
2024-09-21 22:52:58 +00:00
else
local max_count = idr_counts[archetype_id] + 1
idr_counts[archetype_id] = max_count
2024-09-21 22:52:58 +00:00
end
end
2025-07-23 23:38:05 +00:00
local function archetype_create(world: world, id_types: { i53 }, ty, prev: i53?): archetype
local archetype_id = (world.max_archetype_id :: number) + 1
world.max_archetype_id = archetype_id
2025-06-25 11:31:25 +00:00
2025-07-23 23:38:05 +00:00
local length = #id_types
local columns = (table.create(length) :: any) :: { Column }
2025-06-25 11:31:25 +00:00
2025-07-23 23:38:05 +00:00
local columns_map: { [i53]: Column } = {}
2025-06-25 11:31:25 +00:00
2025-07-23 23:38:05 +00:00
local archetype: archetype = {
columns = columns,
columns_map = columns_map,
entities = {},
id = archetype_id,
type = ty,
types = id_types
}
2025-06-25 11:31:25 +00:00
2025-07-23 23:38:05 +00:00
for i, component_id in archetype.types do
local idr = id_record_ensure(world, component_id)
idr.size += 1
2025-07-23 23:38:05 +00:00
local is_tag = bit32.btest(idr.flags, ECS_ID_IS_TAG)
local column = if is_tag then NULL_ARRAY else {}
columns[i] = column
2025-07-14 12:04:51 +00:00
2025-07-23 23:38:05 +00:00
archetype_append_to_records(idr, archetype_id, columns_map, component_id :: number, i, column)
2025-07-14 12:04:51 +00:00
2025-07-23 23:38:05 +00:00
if ECS_IS_PAIR(component_id) then
local relation = ECS_PAIR_FIRST(component_id)
local object = ECS_PAIR_SECOND(component_id)
2025-08-05 23:40:40 +00:00
2025-07-23 23:38:05 +00:00
local r = ECS_PAIR(relation, EcsWildcard)
local idr_r = id_record_ensure(world, r)
2025-07-14 12:04:51 +00:00
2025-08-05 23:40:40 +00:00
idr_r.size += 1
2025-07-23 23:38:05 +00:00
archetype_append_to_records(idr_r, archetype_id, columns_map, r, i, column)
2025-08-05 23:40:40 +00:00
local idr_r_wc_pairs = idr_r.wildcard_pairs
if not idr_r_wc_pairs then
idr_r_wc_pairs = {} :: {[i53]: componentrecord }
idr_r.wildcard_pairs = idr_r_wc_pairs
end
idr_r_wc_pairs[component_id] = idr
2025-07-14 12:04:51 +00:00
2025-07-23 23:38:05 +00:00
local t = ECS_PAIR(EcsWildcard, object)
local idr_t = id_record_ensure(world, t)
2025-07-14 12:04:51 +00:00
2025-08-05 23:40:40 +00:00
idr_t.size += 1
2025-07-23 23:38:05 +00:00
archetype_append_to_records(idr_t, archetype_id, columns_map, t, i, column)
2025-06-25 11:31:25 +00:00
end
end
2025-07-10 10:50:59 +00:00
2025-08-22 14:27:09 +00:00
world.archetype_index[ty] = archetype
2025-07-23 23:38:05 +00:00
world.archetypes[archetype_id] = archetype
2025-08-22 14:27:09 +00:00
world.archetype_edges[archetype_id] = {} :: Map<i53, archetype>
2025-07-23 23:38:05 +00:00
2025-07-14 12:04:51 +00:00
for id in columns_map do
local observer_list = find_observers(world, EcsOnArchetypeCreate, id)
if not observer_list then
continue
end
for _, observer in observer_list do
2025-07-23 23:38:05 +00:00
if query_match(observer.query, archetype) then
observer.callback(archetype::any)
2025-07-14 12:04:51 +00:00
end
end
end
return archetype
end
2025-07-23 23:38:05 +00:00
local function world_range(world: world, range_begin: number, range_end: number?)
local entity_index = world.entity_index
entity_index.range_begin = range_begin
entity_index.range_end = range_end
local max_id = entity_index.max_id
if range_begin > max_id then
local dense_array = entity_index.dense_array
local sparse_array = entity_index.sparse_array
for i = max_id + 1, range_begin do
dense_array[i] = i
sparse_array[i] = {
dense = 0
2025-07-23 23:38:05 +00:00
} :: record
end
entity_index.max_id = range_begin - 1
entity_index.alive_count = range_begin - 1
end
end
2025-07-23 23:38:05 +00:00
local function archetype_ensure(world: world, id_types: { i53 }): archetype
if #id_types < 1 then
return world.ROOT_ARCHETYPE
2024-04-23 15:10:49 +00:00
end
local ty = hash(id_types)
2025-01-29 07:28:08 +00:00
local archetype = world.archetype_index[ty]
2024-04-23 15:10:49 +00:00
if archetype then
return archetype
end
return archetype_create(world, id_types, ty)
2024-04-23 15:10:49 +00:00
end
local function find_insert(id_types: { i53 }, toAdd: i53): number
for i, id in id_types do
2024-04-23 15:10:49 +00:00
if id == toAdd then
return -1
end
if id > toAdd then
return i
end
end
return #id_types + 1
2024-04-23 15:10:49 +00:00
end
2025-03-25 22:13:53 +00:00
local function find_archetype_without(
2025-07-23 23:38:05 +00:00
world: world,
node: archetype,
id: i53
): archetype
local id_types = node.types
local at = table.find(id_types, id)
local dst = table.clone(id_types)
table.remove(dst, at)
return archetype_ensure(world, dst)
end
2025-03-25 22:13:53 +00:00
local function create_edge_for_remove(
2025-07-23 23:38:05 +00:00
world: world,
node: archetype,
edge: Map<i53, archetype>,
id: i53
): archetype
2024-09-21 22:52:58 +00:00
local to = find_archetype_without(world, node, id)
local edges = world.archetype_edges
local archetype_id = node.id
edges[archetype_id][id] = to
edges[to.id][id] = node
2024-09-21 22:52:58 +00:00
return to
end
local function archetype_traverse_remove(
2025-07-23 23:38:05 +00:00
world: world,
id: i53,
from: archetype
): archetype
local edges = world.archetype_edges
local edge = edges[from.id]
2025-07-23 23:38:05 +00:00
local to: archetype = edge[id]
if to == nil then
to = find_archetype_without(world, from, id)
edge[id] = to
edges[to.id][id] = from
2024-09-21 22:52:58 +00:00
end
2024-04-23 15:10:49 +00:00
return to
end
2025-07-23 23:38:05 +00:00
local function find_archetype_with(
world: world,
id: i53,
from: archetype
): archetype
local id_types = from.types
local dst = table.clone(id_types)
2025-06-25 11:31:25 +00:00
local at = find_insert(id_types :: { number } , id :: number)
table.insert(dst, at, id)
return archetype_ensure(world, dst)
end
2025-07-23 23:38:05 +00:00
local function archetype_traverse_add(
world: world,
id: i53,
from: archetype
): archetype
from = from or world.ROOT_ARCHETYPE
2025-06-25 11:31:25 +00:00
if from.columns_map[id] then
return from
end
local edges = world.archetype_edges
local edge = edges[from.id]
local to = edge[id]
2024-09-21 22:52:58 +00:00
if not to then
to = find_archetype_with(world, id, from)
edge[id] = to
edges[to.id][id] = from
2024-09-21 22:52:58 +00:00
end
return to
2024-04-23 15:10:49 +00:00
end
2025-06-09 20:18:59 +00:00
local function archetype_fast_delete_last(columns: { Column }, column_count: number)
2024-09-21 22:52:58 +00:00
for i, column in columns do
if column ~= NULL_ARRAY then
column[column_count] = nil
end
end
end
2025-06-09 20:18:59 +00:00
local function archetype_fast_delete(columns: { Column }, column_count: number, row: number)
2024-09-21 22:52:58 +00:00
for i, column in columns do
if column ~= NULL_ARRAY then
column[row] = column[column_count]
column[column_count] = nil
end
end
end
2025-07-23 23:38:05 +00:00
local function archetype_delete(world: world, archetype: archetype, row: number)
2025-01-29 07:28:08 +00:00
local entity_index = world.entity_index
local columns = archetype.columns
local entities = archetype.entities
local column_count = #entities
local last = #entities
local move = entities[last]
2025-01-29 07:28:08 +00:00
-- We assume first that the entity is the last in the archetype
if row ~= last then
2025-07-23 23:38:05 +00:00
local record_to_move = entity_index_try_get_any(entity_index, move)
if record_to_move then
record_to_move.row = row
end
2025-01-29 07:28:08 +00:00
entities[row] = move
end
2025-01-29 07:28:08 +00:00
entities[last] = nil :: any
if row == last then
2025-06-09 20:18:59 +00:00
archetype_fast_delete_last(columns, column_count)
else
2025-06-09 20:18:59 +00:00
archetype_fast_delete(columns, column_count, row)
end
end
2025-07-23 23:38:05 +00:00
local function archetype_destroy(world: world, archetype: archetype)
if archetype == world.ROOT_ARCHETYPE then
return
end
2024-10-01 15:30:51 +00:00
2025-01-29 07:28:08 +00:00
local component_index = world.component_index
local archetype_edges = world.archetype_edges
2025-07-13 03:15:37 +00:00
local edges = archetype_edges[archetype.id]
for id, node in edges do
archetype_edges[node.id][id] = nil
edges[id] = nil
end
2024-09-21 22:52:58 +00:00
local archetype_id = archetype.id
2025-07-17 16:02:33 +00:00
world.archetypes[archetype_id] = nil :: any
world.archetype_index[archetype.type] = nil :: any
2025-06-25 11:31:25 +00:00
local columns_map = archetype.columns_map
2024-09-21 22:52:58 +00:00
2025-06-25 11:31:25 +00:00
for id in columns_map do
local idr = component_index[id]
idr.records[archetype_id] = nil :: any
idr.counts[archetype_id] = nil
idr.size -= 1
if idr.size == 0 then
component_index[id] = nil :: any
end
2024-12-27 03:34:17 +00:00
local observer_list = find_observers(world, EcsOnArchetypeDelete, id)
if not observer_list then
continue
end
for _, observer in observer_list do
if query_match(observer.query, archetype) then
2025-07-23 23:38:05 +00:00
observer.callback(archetype::any)
end
end
end
end
2024-09-09 01:22:20 +00:00
local function NOOP() end
2024-07-14 06:30:20 +00:00
2025-08-22 14:27:09 +00:00
local function query_archetypes(query: query)
local compatible_archetypes = query.compatible_archetypes
if not compatible_archetypes then
compatible_archetypes = {}
query.compatible_archetypes = compatible_archetypes
2025-08-22 20:07:30 +00:00
local world = query.world
local archetypes = world.archetypes
2025-08-22 14:27:09 +00:00
2025-08-22 20:07:30 +00:00
local component_index = world.component_index
2025-08-22 14:27:09 +00:00
local idr: componentrecord?
local with = query.filter_with
for _, id in with do
local map = component_index[id]
if not map then
continue
end
if idr == nil or (map.size :: number) < (idr.size :: number) then
idr = map
end
end
if idr == nil then
return compatible_archetypes
end
local without = query.filter_without
for archetype_id in idr.records do
local archetype = archetypes[archetype_id]
local columns_map = archetype.columns_map
local skip = false
for _, component in with do
if not columns_map[component] then
skip = true
break
end
end
if skip then
continue
end
if without then
for _, component in without do
if columns_map[component] then
skip = true
break
end
end
end
if skip then
continue
end
table.insert(compatible_archetypes, archetype)
end
end
return compatible_archetypes
end
local function query_with(query: query, ...: i53)
local ids = query.ids
local with = { ... }
table.move(ids, 1, #ids, #with + 1, with)
query.filter_with = with
return query
end
local function query_without(query: query, ...: i53)
local without = { ... }
query.filter_without = without
return query
end
2025-01-14 10:09:18 +00:00
2025-06-25 11:31:25 +00:00
local function query_iter_init(query: QueryInner): () -> (number, ...any)
2024-09-21 22:52:58 +00:00
local world_query_iter_next
2025-08-22 14:27:09 +00:00
local compatible_archetypes = query_archetypes(query::any) :: { Archetype }
2024-09-21 22:52:58 +00:00
local lastArchetype = 1
local archetype = compatible_archetypes[1]
if not archetype then
2024-11-10 01:24:58 +00:00
return NOOP :: () -> (number, ...any)
2024-09-09 01:22:20 +00:00
end
local entities = archetype.entities
local i = #entities
2025-06-25 11:31:25 +00:00
local columns_map = archetype.columns_map
2024-09-21 22:52:58 +00:00
local ids = query.ids
2025-11-18 20:02:39 +00:00
local A, B, C, D, E, F, G, H, I = unpack(ids :: { Component })
2024-11-10 01:24:58 +00:00
local a: Column, b: Column, c: Column, d: Column
local e: Column, f: Column, g: Column, h: Column
2025-08-31 01:38:06 +00:00
if not A then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
if i == 0 then
continue
end
entity = entities[i]
end
i -= 1
return entity
end
query.next = world_query_iter_next
return world_query_iter_next
elseif not B then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
elseif not C then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
b = columns_map[B]
elseif not D then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
elseif not E then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
elseif not F then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
elseif not G then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
elseif not H then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
g = columns_map[G]
else
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
g = columns_map[G]
h = columns_map[H]
2024-09-09 01:22:20 +00:00
end
2024-09-09 01:22:20 +00:00
if not B then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
2024-09-09 01:22:20 +00:00
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
2024-07-30 15:17:18 +00:00
2024-09-09 01:22:20 +00:00
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
end
2024-09-09 01:22:20 +00:00
local row = i
i -= 1
2024-07-14 03:38:44 +00:00
return entity, a[row]
2024-09-09 01:22:20 +00:00
end
elseif not C then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
2024-09-09 01:22:20 +00:00
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
2024-09-09 01:22:20 +00:00
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
end
2024-09-09 01:22:20 +00:00
local row = i
i -= 1
2024-07-14 05:30:13 +00:00
return entity, a[row], b[row]
2024-09-09 01:22:20 +00:00
end
elseif not D then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
2024-09-09 01:22:20 +00:00
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
2024-07-30 18:00:40 +00:00
2024-09-09 01:22:20 +00:00
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
end
2024-09-09 01:22:20 +00:00
local row = i
i -= 1
return entity, a[row], b[row], c[row]
2024-09-09 01:22:20 +00:00
end
elseif not E then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
2024-09-09 01:22:20 +00:00
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
2024-09-09 01:22:20 +00:00
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
end
2024-09-09 01:22:20 +00:00
local row = i
i -= 1
return entity, a[row], b[row], c[row], d[row]
2024-09-09 01:22:20 +00:00
end
elseif not F then
2024-09-09 01:22:20 +00:00
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
2024-09-09 01:22:20 +00:00
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
2024-09-09 01:22:20 +00:00
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
end
local row = i
i -= 1
return entity, a[row], b[row], c[row], d[row], e[row]
end
elseif not G then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
end
2024-09-09 01:22:20 +00:00
local row = i
i -= 1
return entity, a[row], b[row], c[row], d[row], e[row], f[row]
end
elseif not H then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
g = columns_map[G]
end
local row = i
i -= 1
return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row]
end
elseif not I then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
g = columns_map[G]
h = columns_map[H]
end
local row = i
i -= 1
return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row]
end
else
local output = {}
2025-06-25 11:31:25 +00:00
local ids_len = #ids
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
g = columns_map[G]
h = columns_map[H]
end
local row = i
i -= 1
2025-06-25 11:31:25 +00:00
for i = 9, ids_len do
2025-07-23 23:38:05 +00:00
output[i - 8] = columns_map[ids[i]::any][row]
end
2025-06-25 11:31:25 +00:00
return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row], unpack(output)
2024-09-09 01:22:20 +00:00
end
end
2024-10-04 21:55:22 +00:00
query.next = world_query_iter_next
2024-09-21 22:52:58 +00:00
return world_query_iter_next
end
2024-11-10 01:24:58 +00:00
local function query_iter(query): () -> (number, ...any)
local query_next = query.next
if not query_next then
query_next = query_iter_init(query)
end
return query_next
end
2025-06-25 11:31:25 +00:00
local function query_cached(query: QueryInner)
2025-01-14 10:09:18 +00:00
local ids = query.ids
local lastArchetype = 1
2025-11-18 20:02:39 +00:00
local A, B, C, D, E, F, G, H, I = unpack(ids :: { Component })
2025-09-18 11:14:09 +00:00
if not A then
A = query.filter_with[1]
end
2025-01-14 10:09:18 +00:00
local a: Column, b: Column, c: Column, d: Column
local e: Column, f: Column, g: Column, h: Column
local world_query_iter_next
2025-06-25 11:31:25 +00:00
local entities: { Entity }
2025-01-14 10:09:18 +00:00
local i: number
2025-06-25 11:31:25 +00:00
local archetype: Archetype
2025-11-18 20:02:39 +00:00
local columns_map: { [Component]: Column }
local archetypes = query_archetypes(query :: any) :: { Archetype }
local archetypes_map = {}
query.archetypes_map = archetypes_map
for j, arche in archetypes do
archetypes_map[arche.id] = j
end
2025-08-22 14:27:09 +00:00
local compatible_archetypes = archetypes :: { Archetype }
2025-01-14 10:09:18 +00:00
2025-12-09 19:34:05 +00:00
local world = (query :: { world: World }).world
-- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively
-- because the event will be emitted for all components of that Archetype.
2025-06-25 11:31:25 +00:00
local observable = world.observable
2025-07-23 23:38:05 +00:00
local on_create_action = observable[EcsOnArchetypeCreate::any]
if not on_create_action then
2025-11-18 20:02:39 +00:00
on_create_action = {} :: Map<Component, { Observer }>
2025-07-23 23:38:05 +00:00
observable[EcsOnArchetypeCreate::any] = on_create_action
end
2025-11-18 20:02:39 +00:00
local query_cache_on_create: { Observer } = on_create_action[A]
if not query_cache_on_create then
query_cache_on_create = {}
2025-01-14 10:09:18 +00:00
on_create_action[A] = query_cache_on_create
end
2025-07-23 23:38:05 +00:00
local on_delete_action = observable[EcsOnArchetypeDelete::any]
if not on_delete_action then
2025-11-18 20:02:39 +00:00
on_delete_action = {} :: Map<Component, { Observer }>
2025-07-23 23:38:05 +00:00
observable[EcsOnArchetypeDelete::any] = on_delete_action
end
2025-11-18 20:02:39 +00:00
local query_cache_on_delete: { Observer } = on_delete_action[A]
if not query_cache_on_delete then
query_cache_on_delete = {}
2025-01-14 10:09:18 +00:00
on_delete_action[A] = query_cache_on_delete
end
2025-11-18 20:02:39 +00:00
local function on_create_callback(archetype: Archetype)
local n = #archetypes + 1
archetypes[n] = archetype
archetypes_map[archetype.id] = n
end
local function on_delete_callback(archetype)
local n = #archetypes
2025-11-18 20:02:39 +00:00
local lastarchetype = archetypes[n]
local archetypeid = archetype.id
local i = archetypes_map[archetypeid]
archetypes[i] = lastarchetype
archetypes[n] = nil
2025-11-18 20:02:39 +00:00
archetypes_map[archetypeid] = nil
archetypes_map[lastarchetype.id] = i
end
2025-11-18 20:02:39 +00:00
local observer_for_create = { query = query, callback = on_create_callback } :: Observer
local observer_for_delete = { query = query, callback = on_delete_callback } :: Observer
table.insert(query_cache_on_create, observer_for_create)
table.insert(query_cache_on_delete, observer_for_delete)
2024-12-27 03:34:17 +00:00
local function cached_query_iter()
lastArchetype = 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return NOOP
end
entities = archetype.entities
i = #entities
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
2025-08-31 01:38:06 +00:00
if not A then
elseif not B then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
2024-12-27 03:34:17 +00:00
elseif not C then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
b = columns_map[B]
2024-12-27 03:34:17 +00:00
elseif not D then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
2024-12-27 03:34:17 +00:00
elseif not E then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
2024-12-27 03:34:17 +00:00
elseif not F then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
2024-12-27 03:34:17 +00:00
elseif not G then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
2024-12-27 03:34:17 +00:00
elseif not H then
2025-06-25 11:31:25 +00:00
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
g = columns_map[G]
else
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
g = columns_map[G]
h = columns_map[H]
2024-12-27 03:34:17 +00:00
end
return world_query_iter_next
end
2025-08-31 01:38:06 +00:00
if not A then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
if i == 0 then
continue
end
entity = entities[i]
end
i -= 1
return entity
end
elseif not B then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
end
local row = i
i -= 1
return entity, a[row]
end
elseif not C then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
end
local row = i
i -= 1
return entity, a[row], b[row]
end
elseif not D then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
end
local row = i
i -= 1
return entity, a[row], b[row], c[row]
end
elseif not E then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
end
local row = i
i -= 1
return entity, a[row], b[row], c[row], d[row]
end
elseif not F then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
end
local row = i
i -= 1
return entity, a[row], b[row], c[row], d[row], e[row]
end
elseif not G then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
2025-01-14 10:09:18 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
end
local row = i
i -= 1
return entity, a[row], b[row], c[row], d[row], e[row], f[row]
end
elseif not H then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
g = columns_map[G]
end
local row = i
i -= 1
return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row]
end
elseif not I then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
g = columns_map[G]
h = columns_map[H]
end
local row = i
i -= 1
return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row]
end
else
2025-06-25 11:31:25 +00:00
local output = {}
local ids_len = #ids
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
2025-08-25 01:01:05 +00:00
if i == 0 then
continue
end
entity = entities[i]
2025-06-25 11:31:25 +00:00
columns_map = archetype.columns_map
a = columns_map[A]
b = columns_map[B]
c = columns_map[C]
d = columns_map[D]
e = columns_map[E]
f = columns_map[F]
g = columns_map[G]
h = columns_map[H]
end
local row = i
i -= 1
2025-06-25 11:31:25 +00:00
for i = 9, ids_len do
2025-07-23 23:38:05 +00:00
output[i - 8] = columns_map[ids[i]::any][row]
end
return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row], unpack(output)
end
end
2025-11-18 20:02:39 +00:00
local eindex = world.entity_index :: entityindex
local function cached_query_has(entity): boolean
local r = entity_index_try_get_fast(eindex, entity)
if not r then
return false
end
local entityarchetype = r.archetype
if not entityarchetype then
return false
end
return archetypes_map[entityarchetype.id] ~= nil
end
local cached_query = query :: any
cached_query.archetypes = query_archetypes
cached_query.__iter = cached_query_iter
cached_query.iter = cached_query_iter
2025-11-18 20:02:39 +00:00
cached_query.has = cached_query_has
setmetatable(cached_query, cached_query)
return cached_query
end
2025-11-18 20:02:39 +00:00
local function query_has(query: QueryInner, entity: i53)
2025-12-09 19:34:05 +00:00
local world = (query::any).world :: world
2025-11-18 20:02:39 +00:00
local r = entity_index_try_get(world.entity_index, entity)
if not r then
return false
end
local archetype = r.archetype
if not archetype then
return false
end
local columns_map = archetype.columns_map
for _, component in query.filter_with :: {number} do
if not columns_map[component] then
return false
end
end
local filter_without = query.filter_without
if filter_without then
for _, component in filter_without :: {number} do
if columns_map[component] then
return false
end
end
end
return true
end
local Query = {}
Query.__index = Query
Query.__iter = query_iter
2024-10-04 21:55:22 +00:00
Query.iter = query_iter_init
Query.without = query_without
Query.with = query_with
Query.archetypes = query_archetypes
Query.cached = query_cached
2025-11-18 20:02:39 +00:00
Query.has = query_has
2025-06-25 11:31:25 +00:00
local function world_query(world: World, ...)
local ids = { ... }
local q = setmetatable({
ids = ids,
2025-08-22 14:27:09 +00:00
filter_with = ids,
world = world,
}, Query)
return q
2024-06-23 23:30:59 +00:00
end
2025-07-23 23:38:05 +00:00
local function world_each(world: world, id: i53): () -> i53
2025-01-29 07:28:08 +00:00
local idr = world.component_index[id]
if not idr then
2025-07-23 23:38:05 +00:00
return NOOP :: () -> i53
end
2025-06-25 11:31:25 +00:00
local records = idr.records
local archetypes = world.archetypes
2025-06-25 11:31:25 +00:00
local archetype_id = next(records, nil) :: number
local archetype = archetypes[archetype_id]
if not archetype then
2025-07-23 23:38:05 +00:00
return NOOP :: () -> i53
end
local entities = archetype.entities
local row = #entities
2025-07-23 23:38:05 +00:00
return function()
local entity = entities[row]
while not entity do
2025-06-25 11:31:25 +00:00
archetype_id = next(records, archetype_id) :: number
if not archetype_id then
2025-07-23 23:38:05 +00:00
return nil :: any
end
archetype = archetypes[archetype_id]
entities = archetype.entities
row = #entities
entity = entities[row]
end
row -= 1
return entity
end
end
2025-07-23 23:38:05 +00:00
local function world_children(world: world, parent: i53)
return world_each(world, ECS_PAIR(EcsChildOf, parent))
end
2025-07-23 23:38:05 +00:00
local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values: { any })
2025-06-25 19:07:58 +00:00
local entity_index = world.entity_index
local r = entity_index_try_get(entity_index, entity)
if not r then
return
end
local from = r.archetype
local component_index = world.component_index
if not from then
local dst_types = table.clone(ids)
table.sort(dst_types)
2025-06-25 19:07:58 +00:00
local to = archetype_ensure(world, dst_types)
new_entity(entity, r, to)
2025-08-29 15:13:13 +00:00
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
2025-10-21 22:36:00 +00:00
2025-06-25 19:07:58 +00:00
for i, id in ids do
local value = values[i]
2025-10-21 22:36:00 +00:00
if value then
r.archetype.columns_map[id][r.row] = value
end
end
for i, id in ids do
2025-06-25 19:07:58 +00:00
local cdr = component_index[id]
local on_add = cdr.on_add
2025-10-21 22:36:00 +00:00
if on_add then
local value = values[i]
on_add(entity, id, value, ROOT_ARCHETYPE)
2025-06-25 19:07:58 +00:00
end
end
return
end
local dst_types = table.clone(from.types)
local emplaced: { [number]: boolean } = {}
for i, id in ids do
local at = find_insert(dst_types :: { number }, id :: number)
if at == -1 then
emplaced[i] = true
continue
end
emplaced[i] = false
table.insert(dst_types, at, id)
end
local to = archetype_ensure(world, dst_types)
if from ~= to then
entity_move(entity_index, entity, r, to)
2025-10-21 22:36:00 +00:00
for i, id in ids do
local value = values[i] :: any
2025-06-25 19:07:58 +00:00
2025-10-21 22:36:00 +00:00
if value ~= nil then
r.archetype.columns_map[id][r.row] = value
end
end
2025-06-25 19:07:58 +00:00
2025-10-21 22:36:00 +00:00
for i, exists in emplaced do
local value = values[i]
local id = ids[i]
local idr = component_index[id]
if exists then
local on_change = idr.on_change
if on_change then
on_change(entity, id, value, from)
end
else
local on_add = idr.on_add
if on_add then
on_add(entity, id, value, from)
end
end
end
else
for i, id in ids do
local value = values[i] :: any
local idr = component_index[id]
local on_change = idr.on_change
2025-10-21 22:36:00 +00:00
if on_change then
on_change(entity, id, value, from)
end
if value ~= nil then
r.archetype.columns_map[id][r.row] = value
2025-06-25 19:07:58 +00:00
end
end
end
end
2025-07-23 23:38:05 +00:00
local function ecs_bulk_remove(world: world, entity: i53, ids: { i53 })
2025-06-25 19:07:58 +00:00
local entity_index = world.entity_index
local r = entity_index_try_get(entity_index, entity)
if not r then
return
end
local from = r.archetype
local component_index = world.component_index
if not from then
return
end
2025-07-23 23:38:05 +00:00
local remove: { [i53]: boolean } = {}
2025-06-25 19:07:58 +00:00
local columns_map = from.columns_map
for i, id in ids do
if not columns_map[id] then
continue
end
remove[id] = true
local idr = component_index[id]
local on_remove = idr.on_remove
2025-06-25 19:07:58 +00:00
if on_remove then
on_remove(entity, id)
end
end
local to = r.archetype
if from ~= to then
from = to
end
2025-07-23 23:38:05 +00:00
local dst_types = table.clone(from.types) :: { i53 }
2025-06-25 19:07:58 +00:00
for id in remove do
local at = table.find(dst_types, id)
table.remove(dst_types, at)
end
to = archetype_ensure(world, dst_types)
2025-07-23 23:38:05 +00:00
2025-06-25 19:07:58 +00:00
if from ~= to then
entity_move(entity_index, entity, r, to)
end
end
2025-06-25 11:31:25 +00:00
local function world_new()
2025-07-23 23:38:05 +00:00
local eindex_dense_array = {} :: { i53 }
local eindex_sparse_array = {} :: { record }
2025-03-25 22:13:53 +00:00
2025-06-25 11:31:25 +00:00
local entity_index = {
dense_array = eindex_dense_array,
sparse_array = eindex_sparse_array,
2025-07-17 16:50:21 +00:00
alive_count = 0,
2025-07-17 16:47:04 +00:00
max_id = 0,
2025-07-23 23:38:05 +00:00
} :: entityindex
2025-03-25 22:13:53 +00:00
2025-11-18 20:02:39 +00:00
-- NOTE(marcus): with the way the component index is accessed, we want to
-- ensure that components range has fast access.
local component_index = table.create(EcsRest) :: Map<i53, componentrecord>
2024-07-03 15:48:32 +00:00
2025-07-23 23:38:05 +00:00
local archetype_index = {} :: { [string]: archetype }
local archetypes = {} :: Map<i53, archetype>
local archetype_edges = {} :: { [number]: { [i53]: archetype } }
2025-06-25 11:31:25 +00:00
local observable = {}
2025-07-27 12:39:43 +00:00
type Signal = { [i53]: { Listener<any> } }
local signals = {
added = {} :: Signal,
changed = {} :: Signal,
removed = {} :: Signal
}
2025-08-25 01:01:05 +00:00
-- We need to cache the moment the world is registered, that way
-- `world:component` will not pollute the global registration of components.
local max_component_id = ecs_max_component_id
2025-06-25 11:31:25 +00:00
local world = {
archetype_edges = archetype_edges,
component_index = component_index,
entity_index = entity_index,
2025-06-25 11:31:25 +00:00
ROOT_ARCHETYPE = nil :: any,
2025-01-29 07:28:08 +00:00
2025-06-25 11:31:25 +00:00
archetypes = archetypes,
archetype_index = archetype_index,
2025-01-29 07:28:08 +00:00
max_archetype_id = 0,
max_component_id = ecs_max_component_id,
2025-01-29 07:28:08 +00:00
2025-06-25 11:31:25 +00:00
observable = observable,
2025-07-27 12:39:43 +00:00
signals = signals,
2025-07-23 23:38:05 +00:00
} :: world
2024-07-14 03:38:44 +00:00
2025-08-28 15:10:23 +00:00
local function entity_index_new_id(entity_index: entityindex): i53
local alive_count = entity_index.alive_count
local max_id = entity_index.max_id
if alive_count < max_id then
alive_count += 1
entity_index.alive_count = alive_count
local id = eindex_dense_array[alive_count]
return id
end
local id = max_id + 1
local range_end = entity_index.range_end
ecs_assert(range_end == nil or id < range_end, ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY)
entity_index.max_id = id
alive_count += 1
entity_index.alive_count = alive_count
eindex_dense_array[alive_count] = id
eindex_sparse_array[id] = { dense = alive_count } :: record
return id
end
2025-07-27 12:39:43 +00:00
2025-06-25 11:31:25 +00:00
local ROOT_ARCHETYPE = archetype_create(world, {}, "")
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
2025-08-28 15:10:23 +00:00
local function entity_index_try_get_any(entity: i53): record?
2025-06-25 11:31:25 +00:00
local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)]
return r
end
local function inner_archetype_move(
2025-07-23 23:38:05 +00:00
entity: i53,
to: archetype,
dst_row: i24,
2025-07-23 23:38:05 +00:00
from: archetype,
src_row: i24
)
local src_columns = from.columns
local dst_entities = to.entities
local src_entities = from.entities
local last = #src_entities
local id_types = from.types
local columns_map = to.columns_map
if src_row ~= last then
for i, column in src_columns do
if column == NULL_ARRAY then
continue
end
local dst_column = columns_map[id_types[i]]
if dst_column then
dst_column[dst_row] = column[src_row]
end
column[src_row] = column[last]
column[last] = nil
end
local e2 = src_entities[last]
src_entities[src_row] = e2
2025-07-23 23:38:05 +00:00
local record2 = eindex_sparse_array[ECS_ENTITY_T_LO(e2)]
record2.row = src_row
else
for i, column in src_columns do
if column == NULL_ARRAY then
continue
end
-- Retrieves the new column index from the source archetype's record from each component
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
local dst_column = columns_map[id_types[i]]
-- Sometimes target column may not exist, e.g. when you remove a component.
if dst_column then
dst_column[dst_row] = column[src_row]
end
column[last] = nil
end
end
src_entities[last] = nil :: any
dst_entities[dst_row] = entity
end
local function inner_entity_move(
2025-07-23 23:38:05 +00:00
entity: i53,
record: record,
to: archetype
)
local sourceRow = record.row
local from = record.archetype
local dst_row = archetype_append(entity, to)
inner_archetype_move(entity, to, dst_row, from, sourceRow)
record.archetype = to
record.row = dst_row
end
2025-08-28 15:10:23 +00:00
-- local function entity_index_try_get(entity: number): Record?
-- local r = entity_index_try_get_any(entity)
2025-06-29 23:35:55 +00:00
-- if r then
-- local r_dense = r.dense
-- if r_dense > entity_index.alive_count then
-- return nil
-- end
-- if eindex_dense_array[r_dense] ~= entity then
-- return nil
-- end
-- end
-- return r
-- end
2025-08-28 15:10:23 +00:00
local function entity_index_try_get_unsafe(entity: i53): record?
2025-07-22 22:38:13 +00:00
local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)]
2025-06-25 11:31:25 +00:00
if r then
local r_dense = r.dense
2025-06-29 23:35:55 +00:00
-- if r_dense > entity_index.alive_count then
-- return nil
-- end
if eindex_dense_array[r_dense] ~= entity then
2025-06-25 11:31:25 +00:00
return nil
end
end
return r
end
2025-08-05 23:40:40 +00:00
local function exclusive_traverse_add(
archetype: archetype,
cr: number,
id: i53
)
local edge = archetype_edges[archetype.id]
local to = edge[id]
if not to then
local dst = table.clone(archetype.types)
dst[cr] = id
to = archetype_ensure(world, dst)
edge[id] = to
end
return to
end
2025-08-28 15:10:23 +00:00
local function world_set(world: world, entity: i53, id: i53, data): ()
local record = entity_index_try_get_unsafe(entity)
2025-07-17 16:02:33 +00:00
if not record then
return
end
2025-07-23 23:38:05 +00:00
local from: archetype = record.archetype
2025-07-17 16:02:33 +00:00
local src = from or ROOT_ARCHETYPE
local column = src.columns_map[id]
if column then
local idr = component_index[id]
column[record.row] = data
-- If the archetypes are the same it can avoid moving the entity
-- and just set the data directly.
local on_change = idr.on_change
if on_change then
2025-08-29 15:13:13 +00:00
on_change(entity, id, data, src)
2025-07-17 16:02:33 +00:00
end
else
2025-07-23 23:38:05 +00:00
local to: archetype
local idr: componentrecord
if ECS_IS_PAIR(id) then
local first = ECS_PAIR_FIRST(id)
local wc = ECS_PAIR(first, EcsWildcard)
idr = component_index[wc]
2025-07-17 16:02:33 +00:00
local edge = archetype_edges[src.id]
to = edge[id]
if to == nil then
2025-08-05 23:40:40 +00:00
if idr and (bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) == true) then
2025-07-17 16:02:33 +00:00
local cr = idr.records[src.id]
if cr then
local on_remove = idr.on_remove
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]
end
2025-08-05 23:40:40 +00:00
to = exclusive_traverse_add(src, cr, id)
2025-07-17 16:02:33 +00:00
end
end
if not to then
2025-07-17 16:02:33 +00:00
to = find_archetype_with(world, id, src)
if not idr then
idr = component_index[wc]
end
edge[id] = to
archetype_edges[(to :: Archetype).id][id] = src
end
2025-07-17 16:02:33 +00:00
else
if bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then
2025-08-05 23:40:40 +00:00
local on_remove = idr.on_remove
if on_remove then
local cr = idr.records[src.id]
if cr then
local id_types = src.types
on_remove(entity, id_types[cr])
2025-08-05 23:40:40 +00:00
local arche = record.archetype
if src ~= arche then
id_types = arche.types
cr = idr.records[arche.id]
to = exclusive_traverse_add(arche, cr, id)
end
end
end
end
2025-07-17 16:02:33 +00:00
end
else
local edges = archetype_edges
local edge = edges[src.id]
2025-08-05 23:40:40 +00:00
to = edge[id]
2025-07-17 16:02:33 +00:00
if not to then
to = find_archetype_with(world, id, src)
edge[id] = to
edges[to.id][id] = src
end
idr = component_index[id]
end
if from then
-- If there was a previous archetype, then the entity needs to move the archetype
2025-08-29 15:13:13 +00:00
inner_entity_move(entity, record, to)
2025-07-17 16:02:33 +00:00
else
new_entity(entity, record, to)
end
column = to.columns_map[id]
column[record.row] = data
local on_add = idr.on_add
if on_add then
2025-08-29 15:13:13 +00:00
on_add(entity, id, data, src)
2025-07-17 16:02:33 +00:00
end
end
end
2025-08-28 15:10:23 +00:00
local function world_add(
2025-07-23 23:38:05 +00:00
world: world,
entity: i53,
id: i53
2025-06-25 11:31:25 +00:00
): ()
2025-08-28 15:10:23 +00:00
local record = entity_index_try_get_unsafe(entity :: number)
2025-06-25 11:31:25 +00:00
if not record then
return
end
local from = record.archetype
2025-07-17 16:02:33 +00:00
local src = from or ROOT_ARCHETYPE
if src.columns_map[id] then
return
end
2025-07-23 23:38:05 +00:00
local to: archetype
local idr: componentrecord
2025-07-23 23:38:05 +00:00
if ECS_IS_PAIR(id) then
local first = ECS_PAIR_FIRST(id)
local wc = ECS_PAIR(first, EcsWildcard)
idr = component_index[wc]
local edge = archetype_edges[src.id]
2025-07-17 16:02:33 +00:00
to = edge[id]
if to == nil then
2025-08-05 23:40:40 +00:00
if idr and (bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) == true) then
local cr = idr.records[src.id]
if cr then
local on_remove = idr.on_remove
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]
end
2025-08-05 23:40:40 +00:00
to = exclusive_traverse_add(src, cr, id)
end
end
if not to then
to = find_archetype_with(world, id, src)
if not idr then
idr = component_index[wc]
end
edge[id] = to
archetype_edges[(to :: Archetype).id][id] = src
end
else
if bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then
2025-08-05 23:40:40 +00:00
local on_remove = idr.on_remove
if on_remove then
local cr = idr.records[src.id]
if cr then
local id_types = src.types
on_remove(entity, id_types[cr])
2025-08-05 23:40:40 +00:00
local arche = record.archetype
if src ~= arche then
id_types = arche.types
cr = idr.records[arche.id]
to = exclusive_traverse_add(arche, cr, id)
end
end
end
end
end
2025-07-17 16:02:33 +00:00
else
local edges = archetype_edges
local edge = edges[src.id]
2025-07-23 23:38:05 +00:00
to = edge[id]
2025-07-17 16:02:33 +00:00
if not to then
to = find_archetype_with(world, id, src)
edge[id] = to
edges[to.id][id] = src
end
2025-07-17 16:02:33 +00:00
idr = component_index[id]
2025-06-25 11:31:25 +00:00
end
2025-07-17 16:02:33 +00:00
2025-06-25 11:31:25 +00:00
if from then
2025-08-29 15:13:13 +00:00
inner_entity_move(entity, record, to)
2025-06-25 11:31:25 +00:00
else
if #to.types > 0 then
new_entity(entity, record, to)
end
end
local on_add = idr.on_add
2025-06-25 11:31:25 +00:00
if on_add then
2025-08-29 15:13:13 +00:00
on_add(entity, id, nil, src)
2025-06-25 11:31:25 +00:00
end
end
2025-08-28 15:10:23 +00:00
local function world_get(world: world, entity: i53,
2025-07-23 23:38:05 +00:00
a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any
2025-08-28 15:10:23 +00:00
local record = entity_index_try_get_unsafe(entity)
2025-06-25 11:31:25 +00:00
if not record then
return nil
end
local archetype = record.archetype
if not archetype then
return nil
end
local columns_map = archetype.columns_map
local row = record.row
local va = fetch(a, columns_map, row)
if not b then
return va
elseif not c then
2025-06-27 15:42:54 +00:00
return va, fetch(b, columns_map, row)
2025-06-25 11:31:25 +00:00
elseif not d then
return va, fetch(b, columns_map, row), fetch(c, columns_map, row)
elseif not e then
return va, fetch(b, columns_map, row), fetch(c, columns_map, row), fetch(d, columns_map, row)
else
error("args exceeded")
end
end
2025-10-21 22:36:00 +00:00
type Listener<T> =
& ((e: i53, id: i53, value: T, oldarchetype: archetype) -> ())
& ((e: i53, id: i53, delete: boolean?) -> ())
2025-07-27 12:39:43 +00:00
world.added = function<T>(_: world, component: i53, fn: Listener<T>)
local listeners = signals.added[component]
if not listeners then
listeners = {}
signals.added[component] = listeners
2025-08-29 15:13:13 +00:00
local function on_add(entity, id, value, oldarchetype)
2025-07-27 12:39:43 +00:00
for _, listener in listeners :: { Listener<T> } do
2025-08-29 15:13:13 +00:00
listener(entity, id, value, oldarchetype)
2025-07-27 12:39:43 +00:00
end
end
2025-08-28 15:10:23 +00:00
local existing_hook = world_get(world, component, EcsOnAdd) :: Listener<T>
2025-07-27 12:39:43 +00:00
if existing_hook then
table.insert(listeners, existing_hook)
end
2025-08-05 23:40:40 +00:00
local idr_pair = component_index[ECS_PAIR(component, EcsWildcard)]
if idr_pair then
for id, cr in idr_pair.wildcard_pairs do
cr.on_add = on_add
end
idr_pair.on_add = on_add
2025-07-27 12:39:43 +00:00
else
2025-08-05 23:40:40 +00:00
local idr = component_index[component]
if idr then
idr.on_add = on_add
end
2025-07-27 12:39:43 +00:00
end
2025-08-28 15:10:23 +00:00
world_set(world, component, EcsOnAdd, on_add)
2025-07-27 12:39:43 +00:00
end
table.insert(listeners, fn)
return function()
local n = #listeners
local i = table.find(listeners, fn)
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world.changed = function<T>(
_: world,
component: i53,
fn: Listener<T>
)
local listeners = signals.changed[component]
if not listeners then
listeners = {}
signals.changed[component] = listeners
2025-08-29 15:13:13 +00:00
local function on_change(entity, id, value, oldarchetype)
2025-07-27 12:39:43 +00:00
for _, listener in listeners :: { Listener<T> } do
2025-08-29 15:13:13 +00:00
listener(entity, id, value, oldarchetype)
2025-07-27 12:39:43 +00:00
end
end
2025-10-21 22:36:00 +00:00
local existing_hook = world_get(world, component, EcsOnChange) :: Listener<T>?
2025-07-27 12:39:43 +00:00
if existing_hook then
table.insert(listeners, existing_hook)
end
2025-08-05 23:40:40 +00:00
local idr_pair = component_index[ECS_PAIR(component, EcsWildcard)]
if idr_pair then
for _, cr in idr_pair.wildcard_pairs do
cr.on_change = on_change
end
idr_pair.on_change = on_change
2025-07-27 12:39:43 +00:00
else
2025-08-05 23:40:40 +00:00
local idr = component_index[component]
if idr then
idr.on_change = on_change
end
2025-07-27 12:39:43 +00:00
end
2025-08-05 23:40:40 +00:00
2025-08-28 15:10:23 +00:00
world_set(world, component, EcsOnChange, on_change)
2025-07-27 12:39:43 +00:00
end
table.insert(listeners, fn)
return function()
local n = #listeners
local i = table.find(listeners, fn)
listeners[i] = listeners[n]
listeners[n] = nil
end
end
2025-10-21 22:36:00 +00:00
world.removed = function<T>(_: world, component: i53, fn: (i53, i53, boolean?) -> ())
2025-07-27 12:39:43 +00:00
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
2025-10-21 22:36:00 +00:00
local function on_remove(entity, id, delete)
2025-08-29 15:13:13 +00:00
for _, listener in listeners :: { (...any) -> () } do
2025-10-21 22:36:00 +00:00
listener(entity, id, delete)
2025-07-27 12:39:43 +00:00
end
end
2025-08-05 23:40:40 +00:00
2025-08-28 15:10:23 +00:00
local existing_hook = world_get(world, component, EcsOnRemove) :: Listener<T>
2025-07-27 12:39:43 +00:00
if existing_hook then
table.insert(listeners, existing_hook)
end
2025-08-05 23:40:40 +00:00
local idr_pair = component_index[ECS_PAIR(component, EcsWildcard)]
if idr_pair then
for _, cr in idr_pair.wildcard_pairs do
cr.on_remove = on_remove
end
idr_pair.on_remove = on_remove
2025-07-27 12:39:43 +00:00
else
2025-08-05 23:40:40 +00:00
local idr = component_index[component]
if idr then
idr.on_remove = on_remove
end
2025-07-27 12:39:43 +00:00
end
2025-08-05 23:40:40 +00:00
2025-08-28 15:10:23 +00:00
world_set(world, component, EcsOnRemove, on_remove)
2025-07-27 12:39:43 +00:00
end
2025-11-18 20:02:39 +00:00
table.insert(listeners, fn::Listener<any>)
2025-07-27 12:39:43 +00:00
return function()
local n = #listeners
local i = table.find(listeners, fn)
listeners[i] = listeners[n]
listeners[n] = nil
end
end
2025-08-28 15:10:23 +00:00
local function world_has(world: World, entity: i53,
2025-06-25 11:31:25 +00:00
a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean
2025-08-28 15:10:23 +00:00
local record = entity_index_try_get_unsafe(entity)
2025-06-25 11:31:25 +00:00
if not record then
return false
end
local archetype = record.archetype
if not archetype then
return false
end
local columns_map = archetype.columns_map
return columns_map[a] ~= nil and
(b == nil or columns_map[b] ~= nil) and
(c == nil or columns_map[c] ~= nil) and
(d == nil or columns_map[d] ~= nil) and
(e == nil or error("args exceeded"))
end
2025-08-28 15:10:23 +00:00
local function world_target(world: world, entity: i53, relation: i53, index: number?): i53?
local record = entity_index_try_get_unsafe(entity)
2025-06-25 11:31:25 +00:00
if not record then
return nil
end
local archetype = record.archetype
if not archetype then
return nil
end
2025-07-23 23:38:05 +00:00
local r = ECS_PAIR(relation, EcsWildcard)
2025-06-25 11:31:25 +00:00
local idr = world.component_index[r]
if not idr then
return nil
end
local archetype_id = archetype.id
local count = idr.counts[archetype_id]
if not count then
return nil
end
local nth = index or 0
if nth >= count then
2025-08-02 21:57:50 +00:00
return nil
2025-06-25 11:31:25 +00:00
end
nth = archetype.types[nth + idr.records[archetype_id]]
if not nth then
return nil
end
return entity_index_get_alive(world.entity_index,
2025-07-23 23:38:05 +00:00
ECS_PAIR_SECOND(nth))
2025-06-25 11:31:25 +00:00
end
2025-08-28 15:10:23 +00:00
local function world_parent(world: world, entity: i53): i53?
return world_target(world, entity, EcsChildOf, 0)
2025-06-25 11:31:25 +00:00
end
2025-08-28 15:10:23 +00:00
local function world_entity(world: world, entity: i53?): i53
2025-06-25 11:31:25 +00:00
if entity then
2025-07-23 23:38:05 +00:00
local index = ECS_ID(entity)
2025-06-25 11:31:25 +00:00
local alive_count = entity_index.alive_count
local r = eindex_sparse_array[index]
if r then
local dense = r.dense
if not dense or r.dense == 0 then
r.dense = index
dense = index
local e_swap = eindex_dense_array[dense]
2025-08-28 15:10:23 +00:00
local r_swap = entity_index_try_get_any(e_swap) :: record
2025-07-06 17:29:36 +00:00
r_swap.dense = dense
alive_count += 1
entity_index.alive_count = alive_count
r.dense = alive_count
2025-07-06 17:29:36 +00:00
eindex_dense_array[dense] = e_swap
eindex_dense_array[alive_count] = entity
2025-07-06 17:29:36 +00:00
return entity
2025-06-25 11:31:25 +00:00
end
2025-07-06 17:29:36 +00:00
local any = eindex_dense_array[dense]
if any ~= entity then
if alive_count <= dense then
local e_swap = eindex_dense_array[dense]
2025-08-28 15:10:23 +00:00
local r_swap = entity_index_try_get_any(e_swap) :: record
2025-07-06 17:29:36 +00:00
r_swap.dense = dense
alive_count += 1
entity_index.alive_count = alive_count
r.dense = alive_count
eindex_dense_array[dense] = e_swap
eindex_dense_array[alive_count] = entity
end
end
2025-06-25 11:31:25 +00:00
return entity
else
2025-07-17 16:47:04 +00:00
for i = entity_index.max_id + 1, index do
2025-07-23 23:38:05 +00:00
eindex_sparse_array[i] = { dense = i } :: record
2025-06-25 11:31:25 +00:00
eindex_dense_array[i] = i
end
entity_index.max_id = index
local e_swap = eindex_dense_array[alive_count]
local r_swap = eindex_sparse_array[alive_count]
r_swap.dense = index
alive_count += 1
entity_index.alive_count = alive_count
r = eindex_sparse_array[index]
r.dense = alive_count
eindex_sparse_array[index] = r
eindex_dense_array[index] = e_swap
eindex_dense_array[alive_count] = entity
return entity
end
end
2025-06-25 11:31:25 +00:00
return entity_index_new_id(entity_index)
end
2025-08-28 15:10:23 +00:00
local function world_remove(world: world, entity: i53, id: i53)
local record = entity_index_try_get_unsafe(entity)
2025-06-25 11:31:25 +00:00
if not record then
return
end
local from = record.archetype
2024-07-14 03:38:44 +00:00
2025-06-25 11:31:25 +00:00
if not from then
return
end
2025-06-25 11:31:25 +00:00
if from.columns_map[id] then
local idr = world.component_index[id]
local on_remove = idr.on_remove
2025-08-05 23:40:40 +00:00
2025-06-25 11:31:25 +00:00
if on_remove then
on_remove(entity, id)
end
2025-01-29 07:28:08 +00:00
2025-06-25 11:31:25 +00:00
local to = archetype_traverse_remove(world, id, record.archetype)
2025-08-29 15:13:13 +00:00
inner_entity_move(entity, record, to)
2025-06-25 11:31:25 +00:00
end
end
2025-08-28 15:10:23 +00:00
local function world_delete(world: world, entity: i53)
local record = entity_index_try_get_unsafe(entity)
2025-06-25 11:31:25 +00:00
if not record then
return
end
2025-06-25 11:31:25 +00:00
local archetype = record.archetype
if archetype then
-- NOTE(marcus): It is important to remove the data and invoke
-- the hooks before the archetype and certain component records are
-- invalidated or else it will have a nasty runtime error.
for _, id in archetype.types do
local cr = component_index[id]
local on_remove = cr.on_remove
if on_remove then
on_remove(entity, id, true)
end
end
archetype_delete(world, record.archetype, record.row)
end
2025-04-26 13:42:20 +00:00
2025-06-25 11:31:25 +00:00
local component_index = world.component_index
local archetypes = world.archetypes
2025-10-21 22:36:00 +00:00
local tgt = ECS_PAIR(EcsWildcard, entity)
local rel = ECS_PAIR(entity, EcsWildcard)
2025-06-25 11:31:25 +00:00
local idr_t = component_index[tgt]
2025-10-21 22:36:00 +00:00
local idr = component_index[entity]
2025-06-25 11:31:25 +00:00
local idr_r = component_index[rel]
--[[
It is important to note that `world_delete` uses a depth-first
traversal that prunes the children of the entity before their
parents. archetypes can be destroyed and removed from component
records while we're still iterating over those records. The
recursive nature of this function entails that archetype ids can be
removed from component records (idr_t.records, idr.records and
idr_r.records) while that collection is still being iterated over.
If we try to look up an archetype by ID after it has been destroyed,
we get nil. This is hard to debug because the removal happens deep
in the opaque call stack. Essentially the entry is removed on a
first come first serve basis.
The solution is to hold onto the borrowed references of the
archetypes in the lexical scope and make assumptions that the data
itself is not invalidated until `archetype_destroy` is called on it.
This is done since it is the only efficient way of doing it, however
we must veer on the edge of incorrectness. This is mostly acceptable
because it is not generally observable in the user-land but it has
caused subtle when something goes wrong.
- Marcus
]]
2025-06-25 11:31:25 +00:00
if idr then
local flags = idr.flags
2025-07-17 21:45:03 +00:00
if (bit32.btest(flags, ECS_ID_DELETE) == true) then
2025-06-25 11:31:25 +00:00
for archetype_id in idr.records do
local idr_archetype = archetypes[archetype_id]
local entities = idr_archetype.entities
local n = #entities
for i = n, 1, -1 do
2025-08-28 15:10:23 +00:00
world_delete(world, entities[i])
2025-06-25 11:31:25 +00:00
end
archetype_destroy(world, idr_archetype)
end
else
local on_remove = idr.on_remove
if on_remove then
for archetype_id in idr.records do
local idr_archetype = archetypes[archetype_id]
local to = archetype_traverse_remove(world, entity, idr_archetype)
local entities = idr_archetype.entities
local n = #entities
for i = n, 1, -1 do
local e = entities[i]
on_remove(e, entity)
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
2025-08-29 15:13:13 +00:00
inner_entity_move(e, r, to)
end
archetype_destroy(world, idr_archetype)
end
else
for archetype_id in idr.records do
local idr_archetype = archetypes[archetype_id]
local to = archetype_traverse_remove(world, entity, idr_archetype)
local entities = idr_archetype.entities
local n = #entities
for i = n, 1, -1 do
local e = entities[i]
entity_move(entity_index, e, eindex_sparse_array[ECS_ID(e :: number)], to)
end
archetype_destroy(world, idr_archetype)
2025-06-25 11:31:25 +00:00
end
end
end
end
if idr_t then
2025-08-29 15:13:13 +00:00
local archetype_ids = idr_t.records
for archetype_id in archetype_ids do
local idr_t_archetype = archetypes[archetype_id]
local idr_t_types = idr_t_archetype.types
local entities = idr_t_archetype.entities
for _, id in idr_t_types do
if not ECS_IS_PAIR(id) then
continue
end
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
2025-06-25 11:31:25 +00:00
for i = #entities, 1, -1 do
local child = entities[i]
2025-08-28 15:10:23 +00:00
world_delete(world, child)
2025-06-25 11:31:25 +00:00
end
2025-08-29 15:13:13 +00:00
break
else
2025-08-29 15:13:13 +00:00
local on_remove = id_record.on_remove
2025-08-05 23:40:40 +00:00
for i = #entities, 1, -1 do
2025-08-29 15:13:13 +00:00
local child = entities[i]
2025-08-29 15:13:13 +00:00
if on_remove then
on_remove(child, id)
end
local r = entity_index_try_get_unsafe(child) :: record
local to = archetype_traverse_remove(world, id, r.archetype)
inner_entity_move(child, r, to)
2025-06-25 11:31:25 +00:00
end
end
end
archetype_destroy(world, idr_t_archetype)
2025-06-25 11:31:25 +00:00
end
end
if idr_r then
local archetype_ids = idr_r.records
local flags = idr_r.flags
2025-07-17 21:45:03 +00:00
local has_delete_policy = bit32.btest(flags, ECS_ID_DELETE)
if has_delete_policy then
2025-06-25 11:31:25 +00:00
for archetype_id in archetype_ids do
local idr_r_archetype = archetypes[archetype_id]
local entities = idr_r_archetype.entities
local n = #entities
for i = n, 1, -1 do
2025-08-28 15:10:23 +00:00
world_delete(world, entities[i])
2025-06-25 11:31:25 +00:00
end
archetype_destroy(world, idr_r_archetype)
end
else
local counts = idr_r.counts
local records = idr_r.records
for archetype_id in archetype_ids do
local idr_r_archetype = archetypes[archetype_id]
2025-12-21 18:21:42 +00:00
-- local node = idr_r_archetype
2025-06-25 11:31:25 +00:00
local entities = idr_r_archetype.entities
local tr = records[archetype_id]
local tr_count = counts[archetype_id]
2025-11-18 20:02:39 +00:00
local idr_r_types = idr_r_archetype.types
2025-12-21 18:21:42 +00:00
local dst = table.clone(idr_r_types)
2025-06-25 11:31:25 +00:00
for i = tr, tr + tr_count - 1 do
2025-10-21 22:36:00 +00:00
local id = idr_r_types[i]
2025-12-21 18:21:42 +00:00
local at = table.find(dst, id)
if at then
table.remove(dst, at)
end
-- node = archetype_traverse_remove(world, id, node)
local on_remove = component_index[id].on_remove
if on_remove then
2025-12-21 18:21:42 +00:00
-- NOTE(marcus): Since hooks can move the entities
-- assumptions about which archetype it jumps to is
-- diminished. We assume that people who delete
-- relation will never have hooks on them.
for _, entity in entities do
on_remove(entity, id)
end
end
2025-06-25 11:31:25 +00:00
end
2025-07-23 23:38:05 +00:00
2025-12-21 18:21:42 +00:00
local node = archetype_ensure(world, dst)
for i = #entities, 1, -1 do
local e = entities[i]
2025-08-28 15:10:23 +00:00
local r = entity_index_try_get_unsafe(e) :: record
2025-08-29 15:13:13 +00:00
inner_entity_move(e, r, node)
2025-06-25 11:31:25 +00:00
end
archetype_destroy(world, idr_r_archetype)
2025-06-25 11:31:25 +00:00
end
end
end
2025-08-29 15:13:13 +00:00
2025-06-25 11:31:25 +00:00
local dense = record.dense
local i_swap = entity_index.alive_count
entity_index.alive_count = i_swap - 1
2025-07-06 15:57:10 +00:00
local e_swap = eindex_dense_array[i_swap]
2025-08-28 15:10:23 +00:00
local r_swap = entity_index_try_get_any(e_swap) :: record
2025-06-25 11:31:25 +00:00
r_swap.dense = dense
record.archetype = nil :: any
record.row = nil :: any
record.dense = i_swap
2025-07-06 15:57:10 +00:00
eindex_dense_array[dense] = e_swap
2025-07-23 23:38:05 +00:00
eindex_dense_array[i_swap] = ECS_GENERATION_INC(entity)
2025-06-25 11:31:25 +00:00
end
2025-08-28 15:10:23 +00:00
local function world_clear(world: world, entity: i53)
local record = entity_index_try_get_unsafe(entity)
if not record then
return
end
local archetype = record.archetype
for _, id in archetype.types do
local idr = component_index[id]
local on_remove = idr.on_remove
if on_remove then
on_remove(entity, id)
end
end
archetype_delete(world, record.archetype, record.row)
record.archetype = nil :: any
record.row = nil :: any
end
local function world_exists(world: world, entity: i53): boolean
return entity_index_try_get_any(entity) ~= nil
2025-06-25 11:31:25 +00:00
end
2025-08-28 15:10:23 +00:00
local function world_contains(world: world, entity: i53): boolean
2025-07-23 23:38:05 +00:00
return entity_index_is_alive(entity_index, entity)
2025-06-25 11:31:25 +00:00
end
2025-08-28 15:10:23 +00:00
local function world_cleanup(world: world)
2025-06-25 11:31:25 +00:00
for _, archetype in archetypes do
if #archetype.entities == 0 then
archetype_destroy(world, archetype)
end
end
local new_archetypes = {}
local new_archetype_map = {}
for index, archetype in archetypes do
new_archetypes[index] = archetype
new_archetype_map[archetype.type] = archetype
end
archetypes = new_archetypes
archetype_index = new_archetype_map
world.archetypes = new_archetypes
world.archetype_index = new_archetype_map
end
2025-08-25 01:01:05 +00:00
local function world_component(world: world): i53
max_component_id += 1
if max_component_id > HI_COMPONENT_ID then
-- IDs are partitioned into ranges because component IDs are not nominal,
-- so it needs to error when IDs intersect into the entity range.
error("Too many components, consider using world:entity() instead to create components.")
end
world.max_component_id = max_component_id
2025-08-28 15:10:23 +00:00
world_add(world, max_component_id, EcsComponent)
2025-08-25 01:01:05 +00:00
return max_component_id
end
2025-08-28 15:10:23 +00:00
world.entity = world_entity
2025-06-25 11:31:25 +00:00
world.query = world_query :: any
2025-08-28 15:10:23 +00:00
world.remove = world_remove
world.clear = world_clear
-- world.purge = world_purge
world.delete = world_delete
2025-06-25 11:31:25 +00:00
world.component = world_component
2025-08-28 15:10:23 +00:00
world.add = world_add
world.set = world_set
world.get = world_get :: any
world.has = world_has :: any
world.target = world_target
world.parent = world_parent
world.contains = world_contains
world.exists = world_exists
world.cleanup = world_cleanup
2025-06-25 11:31:25 +00:00
world.each = world_each
world.children = world_children
world.range = world_range
2025-08-25 01:01:05 +00:00
for i = 1, EcsRest do
entity_index_new_id(entity_index)
2025-06-25 11:31:25 +00:00
end
2025-08-25 01:01:05 +00:00
for i = 1, max_component_id do
2025-08-28 15:10:23 +00:00
world_add(world, i, EcsComponent)
2025-06-25 11:31:25 +00:00
end
2025-08-28 15:10:23 +00:00
world_add(world, EcsName, EcsComponent)
world_add(world, EcsOnChange, EcsComponent)
world_add(world, EcsOnAdd, EcsComponent)
world_add(world, EcsOnRemove, EcsComponent)
world_add(world, EcsWildcard, EcsComponent)
world_add(world, EcsRest, EcsComponent)
2025-06-25 11:31:25 +00:00
2025-08-28 15:10:23 +00:00
world_set(world, EcsOnAdd, EcsName, "jecs.OnAdd")
world_set(world, EcsOnRemove, EcsName, "jecs.OnRemove")
world_set(world, EcsOnChange, EcsName, "jecs.OnChange")
world_set(world, EcsWildcard, EcsName, "jecs.Wildcard")
world_set(world, EcsChildOf, EcsName, "jecs.ChildOf")
world_set(world, EcsComponent, EcsName, "jecs.Component")
2025-07-17 22:37:58 +00:00
2025-08-28 15:10:23 +00:00
world_set(world, EcsOnDelete, EcsName, "jecs.OnDelete")
world_set(world, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget")
2025-07-17 22:37:58 +00:00
2025-08-28 15:10:23 +00:00
world_set(world, EcsDelete, EcsName, "jecs.Delete")
world_set(world, EcsRemove, EcsName, "jecs.Remove")
world_set(world, EcsName, EcsName, "jecs.Name")
world_set(world, EcsRest, EcsRest, "jecs.Rest")
2025-06-25 11:31:25 +00:00
2025-08-28 15:10:23 +00:00
world_add(world, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete))
world_add(world, EcsChildOf, EcsExclusive)
2025-06-25 11:31:25 +00:00
2025-08-28 15:10:23 +00:00
world_add(world, EcsOnDelete, EcsExclusive)
world_add(world, EcsOnDeleteTarget, EcsExclusive)
2025-07-17 22:37:58 +00:00
2025-06-25 11:31:25 +00:00
for i = EcsRest + 1, ecs_max_tag_id do
entity_index_new_id(entity_index)
end
for i, bundle in ecs_metadata do
for ty, value in bundle do
if value == NULL then
2025-08-28 15:10:23 +00:00
world_add(world, i, ty)
2025-06-25 11:31:25 +00:00
else
2025-08-28 15:10:23 +00:00
world_set(world, i, ty, value)
2025-06-25 11:31:25 +00:00
end
end
end
return world
end
2025-07-23 23:38:05 +00:00
local function ecs_is_tag(world: world, entity: i53): boolean
2025-06-25 11:31:25 +00:00
local idr = world.component_index[entity]
if idr then
2025-06-30 20:37:20 +00:00
return bit32.btest(idr.flags, ECS_ID_IS_TAG)
2025-06-25 11:31:25 +00:00
end
2025-08-28 15:10:23 +00:00
return not WORLD_HAS(world, entity, EcsComponent)
2025-06-25 11:31:25 +00:00
end
2025-07-23 23:38:05 +00:00
local function ecs_entity_record(world: world, entity: i53)
return entity_index_try_get(world.entity_index, entity)
end
2024-07-03 00:10:11 +00:00
return {
2025-04-04 22:41:38 +00:00
world = world_new :: () -> World,
2025-08-05 23:40:40 +00:00
World = {
new = world_new
},
component = (ECS_COMPONENT :: any) :: <T>() -> Entity<T>,
2025-11-18 20:02:39 +00:00
tag = (ECS_TAG :: any) :: () -> Entity<nil>,
meta = (ECS_META :: any) :: <T, a>(id: Entity<T>, id: Component<a>, value: a?) -> Entity<T>,
is_tag = (ecs_is_tag :: any) :: <T>(World, Component<T>) -> boolean,
OnAdd = (EcsOnAdd :: any) :: Component<<T>(entity: Entity, id: Id<T>, data: T) -> ()>,
OnRemove = (EcsOnRemove :: any) :: Component<<T>(entity: Entity, id: Id<T>) -> ()>,
OnChange = (EcsOnChange :: any) :: Component<<T>(entity: Entity, id: Id<T>, data: T) -> ()>,
ChildOf = (EcsChildOf :: any) :: Entity<nil>,
Component = (EcsComponent :: any) :: Entity<nil>,
Wildcard = (EcsWildcard :: any) :: Component,
w = (EcsWildcard :: any) :: Component,
OnDelete = (EcsOnDelete :: any) :: Entity<nil>,
OnDeleteTarget = (EcsOnDeleteTarget :: any) :: Entity<nil>,
Delete = (EcsDelete :: any) :: Entity<nil>,
Remove = (EcsRemove :: any) :: Entity<nil>,
Name = (EcsName :: any) :: Component<string>,
Exclusive = (EcsExclusive :: any) :: Entity<nil>,
ArchetypeCreate = (EcsOnArchetypeCreate :: any) :: Entity<nil>,
ArchetypeDelete = (EcsOnArchetypeDelete :: any) :: Entity<nil>,
Rest = (EcsRest :: any) :: Entity<nil>,
pair = ECS_PAIR :: <P, O>(first: Entity<P>, second: Entity<O>) -> Pair<P, O>,
IS_PAIR = ECS_IS_PAIR :: (pair: Component) -> boolean,
2025-10-21 22:36:00 +00:00
ECS_PAIR_FIRST = ECS_PAIR_FIRST :: <P, O>(pair: Id<P>) -> Component<P>,
ECS_PAIR_SECOND = ECS_PAIR_SECOND :: <P, O>(pair: Id<P>) -> Component<O>,
pair_first = ecs_pair_first :: <P, O>(world: World, pair: Id<P>) -> Component<P>,
pair_second = ecs_pair_second :: <P, O>(world: World, pair: Id<P>) -> Component<O>,
entity_index_get_alive = entity_index_get_alive,
archetype_append_to_records = archetype_append_to_records,
2025-11-18 20:02:39 +00:00
id_record_ensure = id_record_ensure :: (World, Component) -> ComponentRecord,
component_record = id_record_get :: (World, Component) -> ComponentRecord?,
2025-12-09 19:34:05 +00:00
record = ecs_entity_record :: (World, Entity<any>) -> Record,
2025-11-18 20:02:39 +00:00
archetype_create = archetype_create :: (World, { Component }, string) -> Archetype,
archetype_ensure = archetype_ensure :: (World, { Component }) -> Archetype,
find_insert = find_insert,
2025-11-18 20:02:39 +00:00
find_archetype_with = find_archetype_with :: (World, Component, Archetype) -> Archetype,
find_archetype_without = find_archetype_without :: (World, Component, Archetype) -> Archetype,
create_edge_for_remove = create_edge_for_remove,
2025-11-18 20:02:39 +00:00
archetype_traverse_add = archetype_traverse_add :: (World, Component, Archetype) -> Archetype,
archetype_traverse_remove = archetype_traverse_remove :: (World, Component, Archetype) -> Archetype,
bulk_insert = ecs_bulk_insert :: (World, Entity, { Component }, { any }) -> (),
bulk_remove = ecs_bulk_remove :: (World, Entity, { Component }) -> (),
2025-07-23 23:38:05 +00:00
entity_move = entity_move :: (EntityIndex, Entity, Record, Archetype) -> (),
2025-02-01 12:07:55 +00:00
2025-07-23 23:38:05 +00:00
entity_index_try_get = entity_index_try_get :: (EntityIndex, Entity) -> Record?,
entity_index_try_get_fast = entity_index_try_get_fast :: (EntityIndex, Entity) -> Record?,
entity_index_try_get_any = entity_index_try_get_any :: (EntityIndex, Entity) -> Record,
entity_index_is_alive = entity_index_is_alive :: (EntityIndex, Entity) -> boolean,
2025-08-28 15:10:23 +00:00
entity_index_new_id = ENTITY_INDEX_NEW_ID :: (EntityIndex) -> Entity,
2025-07-22 22:38:13 +00:00
Query = Query,
query_iter = query_iter,
query_iter_init = query_iter_init,
query_with = query_with,
query_without = query_without,
query_archetypes = query_archetypes,
2025-01-29 07:28:08 +00:00
query_match = query_match,
2025-11-18 20:02:39 +00:00
find_observers = find_observers :: (World, Component, Component) -> { Observer },
2025-07-22 22:38:13 +00:00
-- Inwards facing API for testing
2025-07-23 23:38:05 +00:00
ECS_ID = ECS_ENTITY_T_LO :: (Entity) -> number,
ECS_GENERATION_INC = ECS_GENERATION_INC :: (Entity) -> Entity,
ECS_GENERATION = ECS_GENERATION :: (Entity) -> number,
2025-07-22 22:38:13 +00:00
ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD,
ECS_ID_IS_EXCLUSIVE = ECS_ID_IS_EXCLUSIVE,
ECS_ID_DELETE = ECS_ID_DELETE,
ECS_META_RESET = ECS_META_RESET,
2025-07-23 23:38:05 +00:00
ECS_COMBINE = ECS_COMBINE :: (number, number) -> Entity,
2025-07-22 22:38:13 +00:00
ECS_ENTITY_MASK = ECS_ENTITY_MASK,
2024-07-03 00:10:11 +00:00
}