jecs/src/init.luau

1416 lines
37 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
type Ty = { i53 }
2024-04-23 15:10:49 +00:00
type ArchetypeId = number
type Column = { any }
2024-04-23 15:10:49 +00:00
type ArchetypeEdge = {
add: Archetype,
remove: Archetype,
}
2024-04-23 15:10:49 +00:00
type Archetype = {
id: number,
edges: { [i53]: ArchetypeEdge },
2024-04-23 15:10:49 +00:00
types: Ty,
type: string | number,
entities: { number },
columns: { Column },
2024-08-01 00:16:09 +00:00
records: { ArchetypeRecord },
2024-04-23 15:10:49 +00:00
}
type Record = {
archetype: Archetype,
row: number,
dense: i24,
componentRecord: ArchetypeMap,
2024-04-23 15:10:49 +00:00
}
type EntityIndex = { dense: { [i24]: i53 }, sparse: { [i53]: Record } }
2024-04-23 15:10:49 +00:00
2024-08-01 00:16:09 +00:00
type ArchetypeRecord = {
2024-05-16 22:17:53 +00:00
count: number,
column: number
}
2024-05-16 22:17:53 +00:00
type ArchetypeMap = {
2024-06-10 23:39:43 +00:00
cache: { ArchetypeRecord },
2024-05-16 22:17:53 +00:00
first: ArchetypeMap,
second: ArchetypeMap,
parent: ArchetypeMap,
size: number,
2024-05-16 22:17:53 +00:00
}
type ComponentIndex = { [i24]: ArchetypeMap }
2024-05-16 22:17:53 +00:00
type Archetypes = { [ArchetypeId]: Archetype }
2024-04-28 14:46:40 +00:00
type ArchetypeDiff = {
added: Ty,
removed: Ty,
}
2024-07-03 00:10:11 +00:00
local HI_COMPONENT_ID = 256
local EcsOnAdd = HI_COMPONENT_ID + 1
local EcsOnRemove = HI_COMPONENT_ID + 2
local EcsOnSet = HI_COMPONENT_ID + 3
local EcsWildcard = HI_COMPONENT_ID + 4
local EcsChildOf = HI_COMPONENT_ID + 5
2024-07-03 15:48:32 +00:00
local EcsComponent = HI_COMPONENT_ID + 6
local EcsRest = HI_COMPONENT_ID + 7
2024-07-03 00:10:11 +00:00
local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
2024-07-14 05:16:24 +00:00
local function FLAGS_ADD(is_pair: boolean): number
local flags = 0x0
2024-07-14 05:16:24 +00:00
if is_pair then
flags = bit32.bor(flags, ECS_PAIR_FLAG) -- HIGHEST bit in the ID.
end
if false then
2024-07-14 05:16:24 +00:00
flags = bit32.bor(flags, 0x4) -- Set the second flag to true
end
if false then
2024-07-14 05:16:24 +00:00
flags = bit32.bor(flags, 0x2) -- Set the third flag to true
end
if false then
2024-07-14 05:16:24 +00:00
flags = bit32.bor(flags, 0x1) -- LAST BIT in the ID.
end
2024-07-14 05:16:24 +00:00
return flags
end
2024-05-14 15:52:41 +00:00
local function ECS_COMBINE(source: number, target: number): i53
return (source * 268435456) + (target * ECS_ID_FLAGS_MASK)
end
local function ECS_IS_PAIR(e: number): boolean
2024-07-03 00:10:11 +00:00
return if e > ECS_ENTITY_MASK then (e % ECS_ID_FLAGS_MASK) // ECS_PAIR_FLAG ~= 0 else false
end
-- HIGH 24 bits LOW 24 bits
local function ECS_GENERATION(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0
end
local function ECS_GENERATION_INC(e: i53)
2024-06-05 22:38:27 +00:00
if e > ECS_ENTITY_MASK then
local flags = e // ECS_ID_FLAGS_MASK
2024-06-05 22:38:27 +00:00
local id = flags // ECS_ENTITY_MASK
local generation = flags % ECS_GENERATION_MASK
2024-06-05 22:38:27 +00:00
return ECS_COMBINE(id, generation + 1) + flags
end
return ECS_COMBINE(e, 1)
end
2024-05-16 22:17:53 +00:00
-- FIRST gets the high ID
local function ECS_ENTITY_T_HI(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_ENTITY_MASK else e
end
2024-08-02 20:24:05 +00:00
local function ECS_PAIR_FIRST(e)
return ECS_ENTITY_T_HI(e)
end
-- SECOND
2024-06-10 23:39:43 +00:00
local function ECS_ENTITY_T_LO(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e
end
2024-08-02 20:24:05 +00:00
local function ECS_PAIR_SECCOND(e)
return ECS_PAIR_SECCOND(e)
end
local function STRIP_GENERATION(e: i53): i24
return ECS_ENTITY_T_LO(e)
end
local function ECS_PAIR(pred: i53, obj: i53): i53
return ECS_COMBINE(ECS_ENTITY_T_LO(obj), ECS_ENTITY_T_LO(pred)) + FLAGS_ADD(--[[isPair]] true) :: i53
end
2024-07-03 15:48:32 +00:00
local ERROR_ENTITY_NOT_ALIVE = "Entity is not alive"
local ERROR_GENERATION_INVALID = "INVALID GENERATION"
2024-07-15 18:29:06 +00:00
local function entity_index_get_alive(index: EntityIndex, e: i24): i53
2024-07-03 15:48:32 +00:00
local denseArray = index.dense
local id = denseArray[ECS_ENTITY_T_LO(e)]
if id then
local currentGeneration = ECS_GENERATION(id)
local gen = ECS_GENERATION(e)
if gen == currentGeneration then
2024-07-03 15:48:32 +00:00
return id
end
error(ERROR_GENERATION_INVALID)
end
error(ERROR_ENTITY_NOT_ALIVE)
end
2024-07-15 18:29:06 +00:00
local function entity_index_sparse_get(entityIndex, id)
return entityIndex.sparse[entity_index_get_alive(entityIndex, id)]
end
2024-05-14 15:52:41 +00:00
-- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits
2024-08-02 20:24:05 +00:00
local function ecs_pair_relation(world, e)
return entity_index_get_alive(world.entityIndex, ECS_ENTITY_T_HI(e))
end
2024-05-14 15:52:41 +00:00
-- ECS_PAIR_SECOND gets the relationship / pred / LOW bits
2024-08-02 20:24:05 +00:00
local function ecs_pair_object(world, e)
return entity_index_get_alive(world.entityIndex, ECS_ENTITY_T_LO(e))
end
2024-07-14 04:35:13 +00:00
local function entity_index_new_id(entityIndex: EntityIndex, index: i24): i53
2024-06-05 22:38:27 +00:00
--local id = ECS_COMBINE(index, 0)
local id = index
entityIndex.sparse[id] = {
dense = index,
} :: Record
entityIndex.dense[index] = id
return id
end
2024-04-23 15:10:49 +00:00
2024-07-14 04:35:13 +00:00
local function archetype_move(entityIndex: EntityIndex, to: Archetype,
destinationRow: i24, from: Archetype, sourceRow: i24)
local columns = from.columns
local sourceEntities = from.entities
local destinationEntities = to.entities
local destinationColumns = to.columns
2024-08-01 00:16:09 +00:00
local records = to.records
local types = from.types
for i, column in columns do
-- 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.
2024-08-01 00:16:09 +00:00
local tr = records[types[i]]
-- Sometimes target column may not exist, e.g. when you remove a component.
2024-08-01 00:16:09 +00:00
if tr then
destinationColumns[tr.column][destinationRow] = column[sourceRow]
2024-04-23 15:10:49 +00:00
end
-- If the entity is the last row in the archetype then swapping it would be meaningless.
local last = #column
if sourceRow ~= last then
-- Swap rempves columns to ensure there are no holes in the archetype.
column[sourceRow] = column[last]
end
column[last] = nil
2024-04-23 15:10:49 +00:00
end
local sparse = entityIndex.sparse
local movedAway = #sourceEntities
2024-04-30 14:05:31 +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.
local e1 = sourceEntities[sourceRow]
local e2 = sourceEntities[movedAway]
if sourceRow ~= movedAway then
sourceEntities[sourceRow] = e2
end
2024-06-10 23:39:43 +00:00
sourceEntities[movedAway] = nil :: any
destinationEntities[destinationRow] = e1
local record1 = sparse[e1]
local record2 = sparse[e2]
record1.row = destinationRow
record2.row = sourceRow
2024-04-23 15:10:49 +00:00
end
2024-07-14 04:35:13 +00:00
local function archetype_append(entity: number, archetype: Archetype): 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
2024-07-14 04:35:13 +00:00
local function new_entity(entityId: i53, record: Record, archetype: Archetype): Record
local row = archetype_append(entityId, archetype)
2024-04-23 15:10:49 +00:00
record.archetype = archetype
record.row = row
return record
end
2024-07-14 04:35:13 +00:00
local function entity_move(entity_index: EntityIndex, entityId: i53, record: Record, to: Archetype)
2024-04-23 15:10:49 +00:00
local sourceRow = record.row
local from = record.archetype
2024-07-14 04:35:13 +00:00
local dst_row = archetype_append(entityId, to)
archetype_move(entity_index, 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
local function hash(arr: { number }): string
2024-04-28 14:46:40 +00:00
return table.concat(arr, "_")
2024-04-23 15:10:49 +00:00
end
2024-07-14 04:35:13 +00:00
local function id_record_ensure(
componentIndex: ComponentIndex,
componentId: number
): ArchetypeMap
local archetypesMap = componentIndex[componentId]
2024-04-23 15:10:49 +00:00
if not archetypesMap then
archetypesMap = ({ size = 0, cache = {} } :: any) :: ArchetypeMap
componentIndex[componentId] = archetypesMap
2024-04-23 15:10:49 +00:00
end
2024-05-16 22:17:53 +00:00
return archetypesMap
end
local function ECS_ID_IS_WILDCARD(e: i53): boolean
2024-05-16 22:17:53 +00:00
assert(ECS_IS_PAIR(e))
local first = ECS_ENTITY_T_HI(e)
local second = ECS_ENTITY_T_LO(e)
2024-07-03 00:10:11 +00:00
return first == EcsWildcard or second == EcsWildcard
2024-04-23 15:10:49 +00:00
end
2024-07-29 23:11:22 +00:00
local function archetype_create(world: any, types: { i24 }, prev: Archetype?): Archetype
2024-04-23 15:10:49 +00:00
local ty = hash(types)
local id = world.nextArchetypeId + 1
world.nextArchetypeId = id
2024-04-23 15:10:49 +00:00
local length = #types
2024-06-10 23:39:43 +00:00
local columns = (table.create(length) :: any) :: { Column }
2024-05-16 22:17:53 +00:00
local componentIndex = world.componentIndex
2024-04-23 15:10:49 +00:00
2024-08-01 00:16:09 +00:00
local records: { ArchetypeRecord } = {}
for i, componentId in types do
2024-08-01 00:41:03 +00:00
local tr = { column = i, count = 1 }
2024-07-14 04:35:13 +00:00
local idr = id_record_ensure(componentIndex, componentId)
2024-08-01 00:16:09 +00:00
idr.cache[id] = tr
2024-07-14 04:35:13 +00:00
idr.size += 1
2024-08-01 00:16:09 +00:00
records[componentId] = tr
if ECS_IS_PAIR(componentId) then
2024-08-02 20:24:05 +00:00
local relation = ecs_pair_relation(world, componentId)
local object = ecs_pair_object(world, componentId)
local r = ECS_PAIR(relation, EcsWildcard)
2024-07-14 04:35:13 +00:00
local idr_r = id_record_ensure(componentIndex, r)
local o = ECS_PAIR(EcsWildcard, object)
2024-07-14 04:35:13 +00:00
local idr_o = id_record_ensure(componentIndex, o)
2024-08-01 00:16:09 +00:00
records[r] = tr
records[o] = tr
2024-08-01 00:16:09 +00:00
idr_r.cache[id] = tr
idr_o.cache[id] = tr
idr_r.size += 1
idr_o.size += 1
end
2024-05-16 22:17:53 +00:00
columns[i] = {}
2024-04-23 15:10:49 +00:00
end
2024-06-10 23:39:43 +00:00
local archetype: Archetype = {
columns = columns,
edges = {},
entities = {},
id = id,
records = records,
type = ty,
types = types,
2024-04-23 15:10:49 +00:00
}
2024-06-10 23:39:43 +00:00
2024-04-23 15:10:49 +00:00
world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype
return archetype
end
2024-07-03 15:48:32 +00:00
export type World = {
archetypeIndex: { [string]: Archetype },
archetypes: Archetypes,
componentIndex: ComponentIndex,
entityIndex: EntityIndex,
nextArchetypeId: number,
nextComponentId: number,
nextEntityId: number,
ROOT_ARCHETYPE: Archetype
}
2024-07-14 04:35:13 +00:00
local function world_entity(world: World): i53
2024-05-10 18:35:41 +00:00
local entityId = world.nextEntityId + 1
world.nextEntityId = entityId
2024-07-14 04:35:13 +00:00
return entity_index_new_id(world.entityIndex, entityId + EcsRest)
end
2024-07-03 00:10:16 +00:00
-- TODO:
-- should have an additional `nth` parameter which selects the nth target
2024-05-16 22:17:53 +00:00
-- this is important when an entity can have multiple relationships with the same target
2024-07-14 04:35:13 +00:00
local function world_target(world: World, entity: i53, relation: i24--[[, nth: number]]): i24?
2024-08-02 20:24:05 +00:00
local record = world.entityIndex.sparse[entity]
2024-05-16 22:17:53 +00:00
local archetype = record.archetype
if not archetype then
2024-05-16 22:17:53 +00:00
return nil
end
2024-06-05 22:38:27 +00:00
2024-08-02 20:24:05 +00:00
local idr = world.componentIndex[ECS_PAIR(relation, EcsWildcard)]
if not idr then
2024-05-16 22:17:53 +00:00
return nil
end
2024-08-02 20:24:05 +00:00
local tr = idr.cache[archetype.id]
if not tr then
2024-05-16 22:17:53 +00:00
return nil
end
2024-08-02 20:24:05 +00:00
return ecs_pair_object(world, archetype.types[tr.column])
2024-05-16 22:17:53 +00:00
end
2024-07-14 04:35:13 +00:00
local function world_parent(world: World, entity: i53)
return world_target(world, entity, EcsChildOf)
2024-07-03 00:10:16 +00:00
end
2024-07-14 04:35:13 +00:00
local function archetype_ensure(world: World, types, prev): Archetype
2024-04-23 15:10:49 +00:00
if #types < 1 then
return world.ROOT_ARCHETYPE
2024-04-23 15:10:49 +00:00
end
2024-04-23 15:10:49 +00:00
local ty = hash(types)
local archetype = world.archetypeIndex[ty]
if archetype then
return archetype
end
2024-07-29 23:11:22 +00:00
return archetype_create(world, types, prev)
2024-04-23 15:10:49 +00:00
end
2024-07-14 04:35:13 +00:00
local function find_insert(types: { i53 }, toAdd: i53): number
for i, id in 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 #types + 1
2024-04-23 15:10:49 +00:00
end
2024-07-15 18:29:06 +00:00
local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype
2024-04-23 15:10:49 +00:00
local types = node.types
-- Component IDs are added incrementally, so inserting and sorting
-- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array.
2024-08-02 20:24:05 +00:00
local dst = table.clone(node.types) :: { i53 }
2024-07-15 18:29:06 +00:00
local at = find_insert(types, id)
2024-04-23 15:10:49 +00:00
if at == -1 then
-- If it finds a duplicate, it just means it is the same archetype so it can return it
-- directly instead of needing to hash types for a lookup to the archetype.
2024-04-23 15:10:49 +00:00
return node
end
2024-08-02 20:24:05 +00:00
table.insert(dst, at, id)
2024-08-02 20:24:05 +00:00
return archetype_ensure(world, dst, node)
2024-04-23 15:10:49 +00:00
end
2024-08-02 20:24:05 +00:00
local function edge_ensure(archetype: Archetype, id: i53): ArchetypeEdge
local edges = archetype.edges
2024-08-02 20:24:05 +00:00
local edge = edges[id]
if not edge then
edge = {} :: any
2024-08-02 20:24:05 +00:00
edges[id] = edge
2024-04-23 15:10:49 +00:00
end
return edge
2024-04-23 15:10:49 +00:00
end
2024-08-02 20:24:05 +00:00
local function archetype_traverse_add(world: World, id: i53, from: Archetype): Archetype
from = from or world.ROOT_ARCHETYPE
2024-04-23 15:10:49 +00:00
2024-08-02 20:24:05 +00:00
local edge = edge_ensure(from, id)
local add = edge.add
if not add then
-- Save an edge using the component ID to the archetype to allow
-- faster traversals to adjacent archetypes.
2024-08-02 20:24:05 +00:00
add = find_archetype_with(world, from, id)
edge.add = add :: never
2024-04-23 15:10:49 +00:00
end
return add
2024-04-23 15:10:49 +00:00
end
2024-08-02 20:24:05 +00:00
local function world_add(world: World, entity: i53, id: i53)
local entityIndex = world.entityIndex
2024-08-02 20:24:05 +00:00
local record = entityIndex.sparse[entity]
local from = record.archetype
2024-08-02 20:24:05 +00:00
local to = archetype_traverse_add(world, id, from)
if from == to then
return
end
if from then
2024-08-02 20:24:05 +00:00
entity_move(entityIndex, entity, record, to)
else
if #to.types > 0 then
2024-08-02 20:24:05 +00:00
new_entity(entity, record, to)
end
end
end
-- Symmetric like `World.add` but idempotent
2024-08-02 20:24:05 +00:00
local function world_set(world: World, entity: i53, id: i53, data: unknown)
local record = world.entityIndex.sparse[entity]
local from = record.archetype
2024-08-02 20:24:05 +00:00
local to = archetype_traverse_add(world, id, from)
if from == to then
-- If the archetypes are the same it can avoid moving the entity
-- and just set the data directly.
2024-08-02 20:24:05 +00:00
local tr = to.records[id]
from.columns[tr.column][record.row] = data
-- Should fire an OnSet event here.
2024-04-30 15:52:44 +00:00
return
end
if from then
-- If there was a previous archetype, then the entity needs to move the archetype
2024-08-02 20:24:05 +00:00
entity_move(world.entityIndex, entity, record, to)
2024-04-23 15:10:49 +00:00
else
if #to.types > 0 then
-- When there is no previous archetype it should create the archetype
2024-08-02 20:24:05 +00:00
new_entity(entity, record, to)
2024-04-23 15:10:49 +00:00
end
end
2024-08-02 20:24:05 +00:00
local tr = to.records[id]
to.columns[tr.column][record.row] = data
2024-04-23 15:10:49 +00:00
end
2024-07-14 04:35:13 +00:00
local function world_component(world: World): i53
2024-07-03 15:48:32 +00:00
local componentId = world.nextComponentId + 1
if componentId > 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.nextComponentId = componentId
2024-07-14 04:35:13 +00:00
local id = entity_index_new_id(world.entityIndex, componentId)
world_add(world, id, EcsComponent)
2024-07-03 15:48:32 +00:00
return id
end
2024-08-02 20:24:05 +00:00
local function archetype_traverse_remove(world: World, id: i53, from: Archetype): Archetype
local edge = edge_ensure(from, id)
2024-04-23 15:10:49 +00:00
local remove = edge.remove
if not remove then
2024-06-15 18:03:12 +00:00
local to = table.clone(from.types) :: { i53 }
2024-08-02 20:24:05 +00:00
local at = table.find(to, id)
if not at then
return from
end
table.remove(to, at)
2024-07-14 04:35:13 +00:00
remove = archetype_ensure(world, to, from)
2024-08-02 20:24:05 +00:00
edge.remove = remove :: any
2024-04-23 15:10:49 +00:00
end
return remove
2024-04-23 15:10:49 +00:00
end
2024-08-02 20:24:05 +00:00
local function world_remove(world: World, entity: i53, id: i53)
local entity_index = world.entityIndex
local record = entity_index.sparse[entity]
local from = record.archetype
if not from then
2024-07-26 14:15:12 +00:00
return
end
2024-08-02 20:24:05 +00:00
local to = archetype_traverse_remove(world, id, from)
2024-04-23 15:10:49 +00:00
2024-08-02 20:24:05 +00:00
if from and not (from == to) then
entity_move(entity_index, entity, record, to)
2024-04-23 15:10:49 +00:00
end
end
2024-07-03 15:48:32 +00:00
-- should reuse this logic in World.set instead of swap removing in transition archetype
2024-07-14 05:30:13 +00:00
local function columns_destruct(columns: { Column }, count: number, row: number)
2024-07-03 15:48:32 +00:00
if row == count then
for _, column in columns do
column[count] = nil
end
else
for _, column in columns do
column[row] = column[count]
column[count] = nil
end
end
end
2024-07-14 05:30:13 +00:00
local function archetype_delete(world: World, id: i53)
2024-07-03 15:48:32 +00:00
local componentIndex = world.componentIndex
2024-08-02 20:24:05 +00:00
local idr = componentIndex[id]
2024-07-03 15:48:32 +00:00
local archetypes = world.archetypes
2024-08-02 20:24:05 +00:00
if idr then
for archetypeId in idr.cache do
2024-07-03 15:48:32 +00:00
for _, entity in archetypes[archetypeId].entities do
2024-07-14 04:35:13 +00:00
world_remove(world, entity, id)
2024-07-03 15:48:32 +00:00
end
end
componentIndex[id] = nil :: any
end
end
2024-07-14 04:35:13 +00:00
local function world_delete(world: World, entityId: i53)
2024-07-03 15:48:32 +00:00
local record = world.entityIndex.sparse[entityId]
if not record then
return
end
local entityIndex = world.entityIndex
local sparse, dense = entityIndex.sparse, entityIndex.dense
local archetype = record.archetype
local row = record.row
2024-07-14 05:30:13 +00:00
archetype_delete(world, entityId)
2024-07-03 15:48:32 +00:00
-- TODO: should traverse linked )component records to pairs including entityId
2024-07-14 05:30:13 +00:00
archetype_delete(world, ECS_PAIR(entityId, EcsWildcard))
archetype_delete(world, ECS_PAIR(EcsWildcard, entityId))
2024-07-03 15:48:32 +00:00
if archetype then
local entities = archetype.entities
local last = #entities
if row ~= last then
local entityToMove = entities[last]
dense[record.dense] = entityToMove
sparse[entityToMove] = record
end
entities[row], entities[last] = entities[last], nil :: any
local columns = archetype.columns
2024-07-14 05:30:13 +00:00
columns_destruct(columns, last, row)
2024-07-03 15:48:32 +00:00
end
sparse[entityId] = nil :: any
dense[#dense] = nil :: any
end
2024-08-02 20:24:05 +00:00
local function world_clear(world: World, entity: i53)
2024-07-03 15:48:32 +00:00
--TODO: use sparse_get (stashed)
2024-08-02 20:24:05 +00:00
local record = world.entityIndex.sparse[entity]
if not record then
2024-07-03 15:48:32 +00:00
return
end
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
local archetype = record.archetype
if archetype == nil or archetype == ROOT_ARCHETYPE then
2024-07-03 15:48:32 +00:00
return
end
2024-08-02 20:24:05 +00:00
entity_move(world.entityIndex, entity, record, ROOT_ARCHETYPE)
2024-07-03 15:48:32 +00:00
end
2024-07-15 18:29:06 +00:00
local world_get: (world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) -> (...any)
do
-- Keeping the function as small as possible to enable inlining
2024-08-01 00:16:09 +00:00
local function fetch(id: i24, records: { ArchetypeRecord }, columns, row): any
2024-07-15 18:29:06 +00:00
local tr = records[id]
2024-04-23 15:10:49 +00:00
2024-07-15 18:29:06 +00:00
if not tr then
return nil
end
2024-04-23 15:10:49 +00:00
2024-08-01 00:16:09 +00:00
return columns[tr.column][row]
2024-07-15 18:29:06 +00:00
end
2024-04-23 15:10:49 +00:00
2024-07-15 18:29:06 +00:00
function world_get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any
local id = entityId
local record = world.entityIndex.sparse[id]
if not record then
return nil
end
2024-04-23 15:10:49 +00:00
2024-07-15 18:29:06 +00:00
local archetype = record.archetype
if not archetype then
return nil
end
2024-04-23 15:10:49 +00:00
2024-07-15 18:31:51 +00:00
local tr = archetype.records
local columns = archetype.columns
2024-07-15 18:29:06 +00:00
local row = record.row
local va = fetch(a, tr, columns, row)
if b == nil then
return va
elseif c == nil then
return va, fetch(b, tr, columns, row)
elseif d == nil then
return va, fetch(b, tr, columns, row), fetch(c, tr, columns, row)
elseif e == nil then
return va, fetch(b, tr, columns, row), fetch(c, tr, columns, row), fetch(d, tr, columns, row)
else
error("args exceeded")
end
end
2024-04-23 15:10:49 +00:00
end
2024-08-02 20:24:05 +00:00
local world_has: (world: World, entity: number, ...i53) -> boolean
2024-07-23 02:44:56 +00:00
do
2024-08-02 20:24:05 +00:00
function world_has(world, entity, ...)
local record = world.entityIndex.sparse[entity]
2024-07-23 02:44:56 +00:00
if not record then
return false
end
local archetype = record.archetype
if not archetype then
return false
end
local tr = archetype.records
for i = 1, select("#", ...) do
if not tr[select(i, ...)] then
return false
end
end
return true
end
end
2024-07-14 06:30:20 +00:00
type Item = () -> (number, ...any)
export type Query = typeof({
__iter = function(): Item
return function()
return 1
end
end,
}) & {
next: Item,
without: (Query) -> Query,
replace: (Query, (...any) -> (...any)) -> ()
2024-04-30 23:45:42 +00:00
}
2024-04-23 15:10:49 +00:00
2024-06-21 22:15:03 +00:00
type CompatibleArchetype = { archetype: Archetype, indices: { number } }
local noop: Item = function()
return nil :: any
end
2024-07-14 06:30:20 +00:00
local Arm = function(self: Query, ...)
return self
end
local EmptyQuery: Query = {
__iter = function(): Item
return noop
end,
drain = Arm,
next = noop :: Item,
replace = noop :: (Query, ...any) -> (),
with = Arm,
without = Arm,
}
2024-07-14 06:30:20 +00:00
setmetatable(EmptyQuery, EmptyQuery)
local function world_query(world, ...)
-- breaking
if (...) == nil then
error("Missing components")
end
local compatible_archetypes = {}
local length = 0
2024-07-14 03:38:44 +00:00
local components = { ... } :: any
local A, B, C, D, E, F, G, H, I = ...
2024-07-30 15:17:18 +00:00
local a, b, c, d, e, f, g, h
local archetypes = world.archetypes
local firstArchetypeMap: ArchetypeMap
local componentIndex = world.componentIndex
for _, componentId in components do
local map = componentIndex[componentId]
if not map then
return EmptyQuery
end
if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
firstArchetypeMap = map
end
end
for id in firstArchetypeMap.cache do
local compatibleArchetype = archetypes[id]
local archetypeRecords = compatibleArchetype.records
local skip = false
for i, componentId in components do
local index = archetypeRecords[componentId]
if not index then
skip = true
break
end
end
if skip then
continue
end
length += 1
compatible_archetypes[length] = compatibleArchetype
end
local init = false
local drain = false
local ids = components
local world_query_iter_next
local lastArchetype = 1
local archetype
local columns
local entities
local i
local queryOutput
local function world_query_iter_create()
if not B then
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == 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
entityId = entities[i]
columns = archetype.columns
local records = archetype.records
a = columns[records[A].column]
end
local row = i
i-=1
return entityId, a[row]
end
elseif not C then
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == 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
entityId = entities[i]
columns = archetype.columns
local records = archetype.records
a = columns[records[A].column]
b = columns[records[B].column]
end
local row = i
i-=1
return entityId, a[row], b[row]
end
elseif not D then
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == 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
entityId = entities[i]
columns = archetype.columns
local records = archetype.records
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
end
local row = i
i-=1
return entityId, a[row], b[row], c[row]
end
elseif not E then
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == 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
entityId = entities[i]
columns = archetype.columns
local records = archetype.records
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
end
local row = i
i-=1
return entityId, a[row], b[row], c[row], d[row]
end
else
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == 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
entityId = entities[i]
columns = archetype.columns
local records = archetype.records
if not F then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
elseif not G then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
elseif not H then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
g = columns[records[G].column]
elseif not I then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
g = columns[records[G].column]
h = columns[records[H].column]
end
end
local row = i
i-=1
if not F then
return entityId, a[row], b[row], c[row], d[row], e[row]
elseif not G then
return entityId, a[row], b[row], c[row], d[row], e[row], f[row]
elseif not H then
return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row]
elseif not I then
return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row]
end
local field = archetype.records
for j, id in ids do
queryOutput[j] = columns[field[id].column][row]
end
return entityId, unpack(queryOutput)
end
end
end
2024-07-30 17:36:53 +00:00
2024-07-30 15:17:18 +00:00
local function query_init(query)
2024-07-30 17:36:53 +00:00
if init and drain then
return
end
init = true
2024-07-30 15:17:18 +00:00
lastArchetype = 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return false
2024-07-30 15:17:18 +00:00
end
queryOutput = {}
entities = archetype.entities
i = #entities
columns = archetype.columns
local records = archetype.records
if not B then
2024-08-01 00:16:09 +00:00
a = columns[records[A].column]
2024-07-30 15:17:18 +00:00
elseif not C then
2024-08-01 00:16:09 +00:00
a = columns[records[A].column]
b = columns[records[B].column]
2024-07-30 15:17:18 +00:00
elseif not D then
2024-08-01 00:16:09 +00:00
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
2024-07-30 15:17:18 +00:00
elseif not E then
2024-08-01 00:16:09 +00:00
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
2024-07-30 15:17:18 +00:00
elseif not F then
2024-08-01 00:16:09 +00:00
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
2024-07-30 15:17:18 +00:00
elseif not G then
2024-08-01 00:16:09 +00:00
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
2024-07-30 15:17:18 +00:00
elseif not H then
2024-08-01 00:16:09 +00:00
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
g = columns[records[G].column]
elseif not I then
2024-08-01 00:16:09 +00:00
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
2024-08-01 00:16:09 +00:00
e = columns[records[E].column]
f = columns[records[F].column]
g = columns[records[G].column]
h = columns[records[H].column]
2024-07-30 15:17:18 +00:00
end
return true
2024-07-30 15:17:18 +00:00
end
local function world_query_without(self, ...)
2024-07-30 18:00:40 +00:00
local withoutComponents = { ... }
for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i]
local records = archetype.records
local shouldRemove = false
for _, componentId in withoutComponents do
if records[componentId] then
shouldRemove = true
break
end
end
if shouldRemove then
local last = #compatible_archetypes
if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last]
end
compatible_archetypes[last] = nil
2024-07-28 12:31:47 +00:00
end
2024-07-30 18:00:40 +00:00
end
2024-07-14 03:38:44 +00:00
2024-07-30 18:00:40 +00:00
return self
2024-07-14 03:38:44 +00:00
end
2024-07-14 05:30:13 +00:00
local function world_query_replace_values(row, columns, ...)
for i, column in columns do
column[row] = select(i, ...)
end
2024-07-14 05:30:13 +00:00
end
2024-07-30 15:17:18 +00:00
local function world_query_replace(query, fn: (...any) -> (...any))
2024-07-30 18:00:40 +00:00
query_init(query)
for i, archetype in compatible_archetypes do
local columns = archetype.columns
local tr = archetype.records
for row in archetype.entities do
2024-07-31 16:48:34 +00:00
if not B then
2024-08-01 00:16:09 +00:00
local va = columns[tr[A].column]
2024-07-30 18:00:40 +00:00
local pa = fn(va[row])
va[row] = pa
2024-07-31 16:48:34 +00:00
elseif not C then
2024-08-01 00:16:09 +00:00
local va = columns[tr[A].column]
local vb = columns[tr[B].column]
2024-07-30 18:00:40 +00:00
va[row], vb[row] = fn(va[row], vb[row])
2024-07-31 16:48:34 +00:00
elseif not D then
2024-08-01 00:16:09 +00:00
local va = columns[tr[A].column]
local vb = columns[tr[B].column]
local vc = columns[tr[C].column]
2024-07-30 18:00:40 +00:00
va[row], vb[row], vc[row] = fn(va[row], vb[row], vc[row])
2024-07-31 16:48:34 +00:00
elseif not E then
2024-08-01 00:16:09 +00:00
local va = columns[tr[A].column]
local vb = columns[tr[B].column]
local vc = columns[tr[C].column]
local vd = columns[tr[D].column]
2024-07-30 18:00:40 +00:00
va[row], vb[row], vc[row], vd[row] = fn(
va[row], vb[row], vc[row], vd[row])
else
local field = archetype.records
for j, id in ids do
2024-08-01 00:16:09 +00:00
queryOutput[j] = columns[field[id].column][row]
end
2024-07-30 18:00:40 +00:00
world_query_replace_values(row, columns,
fn(unpack(queryOutput)))
end
end
2024-07-14 03:38:44 +00:00
end
end
2024-07-26 14:21:13 +00:00
local function world_query_with(query, ...)
2024-07-30 18:00:40 +00:00
local ids = { ... }
for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i]
local records = archetype.records
local shouldRemove = false
for _, id in ids do
if not records[id] then
shouldRemove = true
break
end
end
if shouldRemove then
local last = #compatible_archetypes
if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last]
end
compatible_archetypes[last] = nil
end
end
2024-07-26 14:21:13 +00:00
2024-07-30 18:00:40 +00:00
query_init(query)
2024-07-30 17:36:53 +00:00
2024-07-30 18:00:40 +00:00
return query
2024-07-26 14:21:13 +00:00
end
2024-07-28 12:36:53 +00:00
-- Meant for directly iterating over archetypes to minimize
-- function call overhead. Should not be used unless iterating over
-- hundreds of thousands of entities in bulk.
local function world_query_archetypes()
return compatible_archetypes
end
2024-07-30 15:17:18 +00:00
local function world_query_drain(query)
drain = true
if query_init(query) then
return query
end
return EmptyQuery
2024-07-30 15:17:18 +00:00
end
local function world_query_iter(query)
2024-07-30 17:36:53 +00:00
query_init(query)
return world_query_iter_next
end
local function world_query_next()
2024-07-30 15:17:18 +00:00
if not drain then
2024-07-30 17:36:53 +00:00
error("Did you forget to call query:drain()?")
2024-07-30 15:17:18 +00:00
end
2024-07-30 17:36:53 +00:00
return world_query_iter_next()
2024-07-30 15:17:18 +00:00
end
if #compatible_archetypes == 0 then
return EmptyQuery
end
2024-07-28 10:33:38 +00:00
local it = {
__iter = world_query_iter,
2024-07-30 15:17:18 +00:00
drain = world_query_drain,
2024-07-28 10:33:38 +00:00
next = world_query_next,
with = world_query_with,
without = world_query_without,
replace = world_query_replace,
2024-07-28 12:36:53 +00:00
archetypes = world_query_archetypes
2024-07-28 10:33:38 +00:00
} :: any
setmetatable(it, it)
drain = false
init = false
ids = components
world_query_iter_create()
return it
2024-06-23 23:30:59 +00:00
end
type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53)
-- __nominal_type_dont_use could not be any or T as it causes a type error
-- or produces a union
2024-06-10 23:39:43 +00:00
export type Entity<T = any> = number & { __nominal_type_dont_use: T }
2024-06-09 21:58:01 +00:00
export type Pair = number
2024-06-09 21:58:01 +00:00
export type QueryShim<T...> = typeof(setmetatable({
without = function(...): QueryShim<T...>
return nil :: any
2024-06-10 23:39:43 +00:00
end,
2024-06-09 21:58:01 +00:00
}, {
__iter = function(): () -> (number, T...)
return nil :: any
2024-06-10 23:39:43 +00:00
end,
2024-06-09 21:58:01 +00:00
}))
2024-07-03 15:48:32 +00:00
export type WorldShim = typeof(setmetatable(
{} :: {
--- Creates a new entity
2024-06-09 21:58:01 +00:00
entity: (WorldShim) -> Entity,
--- Creates a new entity located in the first 256 ids.
--- These should be used for static components for fast access.
2024-06-09 21:58:01 +00:00
component: <T>(WorldShim) -> Entity<T>,
--- Gets the target of an relationship. For example, when a user calls
--- `world:target(id, ChildOf(parent))`, you will obtain the parent entity.
2024-06-09 21:58:01 +00:00
target: (WorldShim, id: Entity, relation: Entity) -> Entity?,
--- Deletes an entity and all it's related components and relationships.
delete: (WorldShim, id: Entity) -> (),
2024-06-10 23:39:43 +00:00
--- Adds a component to the entity with no value
2024-06-09 21:58:01 +00:00
add: <T>(WorldShim, id: Entity, component: Entity<T>) -> (),
--- Assigns a value to a component on the given entity
2024-06-09 21:58:01 +00:00
set: <T>(WorldShim, id: Entity, component: Entity<T>, data: T) -> (),
-- Clears an entity from the world
clear: (WorldShim, id: Entity) -> (),
--- Removes a component from the given entity
2024-06-09 21:58:01 +00:00
remove: (WorldShim, id: Entity, component: Entity) -> (),
--- Retrieves the value of up to 4 components. These values may be nil.
get: (<A>(WorldShim, id: Entity, Entity<A>) -> A)
2024-06-09 21:58:01 +00:00
& (<A, B>(WorldShim, id: Entity, Entity<A>, Entity<B>) -> (A, B))
& (<A, B, C>(WorldShim, id: Entity, Entity<A>, Entity<B>, Entity<C>) -> (A, B, C))
& <A, B, C, D>(WorldShim, id: Entity, Entity<A>, Entity<B>, Entity<C>, Entity<D>) -> (A, B, C, D),
2024-06-10 23:39:43 +00:00
has: (WorldShim, Entity, ...Entity) -> boolean,
--- Searches the world for entities that match a given query
2024-06-09 21:58:01 +00:00
query: (<A>(WorldShim, Entity<A>) -> QueryShim<A>)
& (<A, B>(WorldShim, Entity<A>, Entity<B>) -> QueryShim<A, B>)
& (<A, B, C>(WorldShim, Entity<A>, Entity<B>, Entity<C>) -> QueryShim<A, B, C>)
& (<A, B, C, D>(WorldShim, Entity<A>, Entity<B>, Entity<C>, Entity<D>) -> QueryShim<A, B, C, D>)
2024-06-10 23:39:43 +00:00
& (<A, B, C, D, E>(
WorldShim,
Entity<A>,
Entity<B>,
Entity<C>,
Entity<D>,
Entity<E>
) -> QueryShim<A, B, C, D, E>)
& (<A, B, C, D, E, F>(
WorldShim,
Entity<A>,
Entity<B>,
Entity<C>,
Entity<D>,
Entity<E>,
Entity<F>
) -> QueryShim<A, B, C, D, E, F>)
& (<A, B, C, D, E, F, G>(
WorldShim,
Entity<A>,
Entity<B>,
Entity<C>,
Entity<D>,
Entity<E>,
Entity<F>,
Entity<G>
) -> QueryShim<A, B, C, D, E, F, G>)
& (<A, B, C, D, E, F, G, H>(
WorldShim,
Entity<A>,
Entity<B>,
Entity<C>,
Entity<D>,
Entity<E>,
Entity<F>,
Entity<G>,
Entity<H>
) -> QueryShim<A, B, C, D, E, F, G, H>)
& (<A, B, C, D, E, F, G, H, I>(
WorldShim,
Entity<A>,
Entity<B>,
Entity<C>,
Entity<D>,
Entity<E>,
Entity<F>,
Entity<G>,
Entity<H>,
Entity<I>
) -> QueryShim<A, B, C, D, E, F, G, H, I>)
& (<A, B, C, D, E, F, G, H, I, J>(
WorldShim,
Entity<A>,
Entity<B>,
Entity<C>,
Entity<D>,
Entity<E>,
Entity<F>,
Entity<G>,
Entity<H>,
Entity<I>,
Entity<J>
) -> QueryShim<A, B, C, D, E, F, G, H, I, J>)
& (<A, B, C, D, E, F, G, H, I, J, K>(
WorldShim,
Entity<A>,
Entity<B>,
Entity<C>,
Entity<D>,
Entity<E>,
Entity<F>,
Entity<G>,
Entity<H>,
Entity<I>,
Entity<J>,
Entity<K>,
...Entity<any>
) -> QueryShim<A, B, C, D, E, F, G, H, I, J, K>),
},
{} :: {
2024-06-10 23:39:43 +00:00
__iter: (world: WorldShim) -> () -> (number, { [unknown]: unknown? }),
}
))
2024-07-03 15:48:32 +00:00
local World = {}
World.__index = World
2024-07-14 04:35:13 +00:00
World.entity = world_entity
World.query = world_query
World.remove = world_remove
World.clear = world_clear
World.delete = world_delete
World.component = world_component
World.add = world_add
World.set = world_set
World.get = world_get
2024-07-23 02:44:56 +00:00
World.has = world_has
2024-07-14 04:35:13 +00:00
World.target = world_target
World.parent = world_parent
2024-07-03 15:48:32 +00:00
2024-07-14 03:38:44 +00:00
function World.new()
local self = setmetatable({
2024-07-28 12:17:44 +00:00
archetypeIndex = {} :: { [string]: Archetype },
archetypes = {} :: Archetypes,
componentIndex = {} :: ComponentIndex,
entityIndex = {
dense = {} :: { [i24]: i53 },
sparse = {} :: { [i53]: Record },
} :: EntityIndex,
hooks = {
[EcsOnAdd] = {},
},
nextArchetypeId = 0,
nextComponentId = 0,
nextEntityId = 0,
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
}, World)
2024-07-14 03:38:44 +00:00
2024-07-29 23:11:22 +00:00
self.ROOT_ARCHETYPE = archetype_create(self, {})
2024-07-14 03:38:44 +00:00
2024-07-28 12:17:44 +00:00
for i = HI_COMPONENT_ID + 1, EcsRest do
-- Initialize built-in components
entity_index_new_id(self.entityIndex, i)
end
2024-07-14 03:38:44 +00:00
return self
end
2024-07-03 00:10:11 +00:00
return {
2024-07-14 03:38:44 +00:00
World = World :: { new: () -> WorldShim } ,
2024-07-03 15:48:32 +00:00
OnAdd = EcsOnAdd :: Entity,
OnRemove = EcsOnRemove :: Entity,
OnSet = EcsOnSet :: Entity,
2024-07-03 00:23:16 +00:00
ChildOf = EcsChildOf,
2024-07-03 15:48:32 +00:00
Component = EcsComponent,
Wildcard = EcsWildcard :: Entity,
w = EcsWildcard :: Entity,
2024-07-29 23:11:22 +00:00
Rest = EcsRest :: Entity,
2024-07-15 18:29:06 +00:00
pair = (ECS_PAIR :: any) :: <R, T>(pred: Entity, obj: Entity) -> number,
-- Inwards facing API for testing
IS_PAIR = ECS_IS_PAIR,
2024-05-14 15:52:41 +00:00
ECS_ID = ECS_ENTITY_T_LO,
ECS_GENERATION_INC = ECS_GENERATION_INC,
ECS_GENERATION = ECS_GENERATION,
2024-07-15 18:29:06 +00:00
ecs_pair_relation = ecs_pair_relation,
ecs_pair_object = ecs_pair_object,
entity_index_get_alive = entity_index_get_alive,
2024-07-03 00:10:11 +00:00
}