diff --git a/jecs.luau b/jecs.luau index 804c2d4..dd72f87 100755 --- a/jecs.luau +++ b/jecs.luau @@ -926,9 +926,9 @@ local function archetype_create(world: world, id_types: { i53 }, ty, prev: i53?) end end - world.archetype_index[archetype.type] = archetype + world.archetype_index[ty] = archetype world.archetypes[archetype_id] = archetype - world.archetype_edges[archetype.id] = {} :: Map + world.archetype_edges[archetype_id] = {} :: Map for id in columns_map do local observer_list = find_observers(world, EcsOnArchetypeCreate, id) @@ -1175,11 +1175,85 @@ end local function NOOP() end +local function query_archetypes(query: query) + local compatible_archetypes = query.compatible_archetypes + if not compatible_archetypes then + compatible_archetypes = {} + query.compatible_archetypes = compatible_archetypes + + local archetypes = query.world.archetypes + + local component_index = query.world.component_index + + 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 local function query_iter_init(query: QueryInner): () -> (number, ...any) local world_query_iter_next - local compatible_archetypes = query.compatible_archetypes + local compatible_archetypes = query_archetypes(query::any) :: { Archetype } local lastArchetype = 1 local archetype = compatible_archetypes[1] if not archetype then @@ -1252,9 +1326,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1277,9 +1348,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1303,9 +1371,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1330,9 +1395,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1358,9 +1420,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1387,9 +1446,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1417,9 +1473,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1448,9 +1501,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1482,9 +1532,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1520,84 +1567,8 @@ local function query_iter(query): () -> (number, ...any) return query_next end -local function query_without(query: QueryInner, ...: Id) - local without = { ... } - query.filter_without = without :: any - local compatible_archetypes = query.compatible_archetypes - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local columns_map = archetype.columns_map - local matches = true - - for _, id in without do - if columns_map[id] then - matches = false - break - end - end - - if matches then - continue - end - - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil :: any - end - - return query :: any -end - -local function query_with(query: QueryInner, ...: Id) - local compatible_archetypes = query.compatible_archetypes - local with = { ... } :: any - query.filter_with = with - - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local columns_map = archetype.columns_map - local matches = true - - for _, id in with do - if not columns_map[id] then - matches = false - break - end - end - - if matches then - continue - end - - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil :: any - end - - return query :: any -end - --- 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 query_archetypes(query) - return query.compatible_archetypes -end - local function query_cached(query: QueryInner) - local with = query.filter_with local ids = query.ids - if with then - table.move(ids, 1, #ids, #with + 1, with) - else - query.filter_with = ids - end - - local compatible_archetypes = query.compatible_archetypes local lastArchetype = 1 local A, B, C, D, E, F, G, H, I = unpack(ids :: { Id }) @@ -1609,7 +1580,8 @@ local function query_cached(query: QueryInner) local i: number local archetype: Archetype local columns_map: { [Id]: Column } - local archetypes = query.compatible_archetypes + local archetypes = query_archetypes(query :: any) + local compatible_archetypes = archetypes :: { Archetype } local world = query.world -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively @@ -1727,9 +1699,6 @@ local function query_cached(query: QueryInner) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1752,9 +1721,6 @@ local function query_cached(query: QueryInner) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1778,9 +1744,6 @@ local function query_cached(query: QueryInner) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1805,9 +1768,6 @@ local function query_cached(query: QueryInner) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1892,10 +1852,6 @@ local function query_cached(query: QueryInner) entities = archetype.entities i = #entities - if i == 0 then - continue - end - entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] b = columns_map[B] @@ -1923,9 +1879,6 @@ local function query_cached(query: QueryInner) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -1957,9 +1910,6 @@ local function query_cached(query: QueryInner) entities = archetype.entities i = #entities - if i == 0 then - continue - end entity = entities[i] columns_map = archetype.columns_map a = columns_map[A] @@ -2001,65 +1951,16 @@ Query.archetypes = query_archetypes Query.cached = query_cached local function world_query(world: World, ...) - local compatible_archetypes = {} - local length = 0 - local ids = { ... } - local archetypes = world.archetypes - - local idr: ComponentRecord? - local component_index = world.component_index - local q = setmetatable({ ids = ids, - compatible_archetypes = compatible_archetypes, + filter_with = ids, world = world, }, Query) - for _, id in ids do - local map = component_index[id] - if not map then - return q - end - - if idr == nil or (map.size :: number) < (idr.size :: number) then - idr = map - end - end - - if idr == nil then - return q - end - - for archetype_id in idr.records do - local compatibleArchetype = archetypes[archetype_id] - if #compatibleArchetype.entities == 0 then - continue - end - local columns_map = compatibleArchetype.columns_map - - local skip = false - - for i, id in ids do - local column = columns_map[id] - if not column then - skip = true - break - end - end - - if skip then - continue - end - - length += 1 - compatible_archetypes[length] = compatibleArchetype - end - return q end - local function world_each(world: world, id: i53): () -> i53 local idr = world.component_index[id] if not idr then diff --git a/mirror.luau b/mirror.luau index f56e709..35c8c6b 100755 --- a/mirror.luau +++ b/mirror.luau @@ -1,3 +1,4 @@ + --!optimize 2 --!native --!strict @@ -19,21 +20,20 @@ export type Archetype = { type: string, entities: { Entity }, columns: { Column }, - columns_map: { [Id]: Column }, - dead: boolean, + columns_map: { [Id]: Column } } export type QueryInner = { compatible_archetypes: { Archetype }, - ids: { i53 }, - filter_with: { i53 }, - filter_without: { i53 }, - next: () -> (number, ...any), + ids: { Id }, + filter_with: { Id }, + filter_without: { Id }, + next: () -> (Entity, ...any), world: World, } -export type Entity = number | { __T: T } -export type Id = number | { __T: T } +export type Entity ={ __T: T } +export type Id = { __T: T } export type Pair = Id

type ecs_id_t = Id | Pair | Pair<"Tag", T> export type Item = (self: Query) -> (Entity, T...) @@ -54,6 +54,8 @@ export type Query = typeof(setmetatable( & ((Query, Id, Id, Id, Id) -> Query), archetypes: (self: Query) -> { Archetype }, cached: (self: Query) -> Query, + ids: { Id }, + -- world: World }, {} :: { __iter: Iter, @@ -67,8 +69,93 @@ export type Observer = { query: QueryInner, } +type query = { + compatible_archetypes: { archetype }, + ids: { i53 }, + filter_with: { i53 }, + filter_without: { i53 }, + next: () -> (i53, ...any), + world: World, +} + +export type observer = { + callback: (archetype: archetype) -> (), + query: query, +} + +type archetype = { + id: number, + types: { i53 }, + type: string, + entities: { i53 }, + columns: { Column }, + columns_map: { [i53]: Column } +} + +type componentrecord = { + records: { [number]: number }, + counts: { [i53]: number }, + flags: number, + size: number, + + on_add: ((entity: i53, id: i53, value: any?) -> ())?, + on_change: ((entity: i53, id: i53, value: any) -> ())?, + on_remove: ((entity: i53, id: i53) -> ())?, + + wildcard_pairs: { [number]: componentrecord }, +} +type record = { + archetype: archetype, + row: number, + dense: i24, +} +type entityindex = { + dense_array: Map, + sparse_array: Map, + alive_count: number, + max_id: number, + range_begin: number?, + range_end: number?, +} +type world = { + archetype_edges: Map>, + archetype_index: { [string]: archetype }, + archetypes: { [i53]: archetype }, + component_index: Map, + entity_index: entityindex, + ROOT_ARCHETYPE: archetype, + + max_component_id: number, + max_archetype_id: number, + + observable: Map>, + + 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) -> (), + clear: (self: world, id: i53) -> (), + 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, + query: (world, ...i53) -> Query<...any>, + + added: (world, i53, (e: i53, id: i53, value: any?) -> ()) -> () -> (), + changed: (world, i53, (e: i53, id: i53, value: any?) -> ()) -> () -> (), + removed: (world, i53, (e: i53, id: i53) -> ()) -> () -> (), +} + export type World = { - archetype_edges: Map>, + archetype_edges: Map>, archetype_index: { [string]: Archetype }, archetypes: Archetypes, component_index: ComponentIndex, @@ -80,11 +167,15 @@ export type World = { observable: Map>, + added: (World, Entity, (e: Entity, id: Id, value: T?) -> ()) -> () -> (), + removed: (World, Entity, (e: Entity, id: Id) -> ()) -> () -> (), + changed: (World, Entity, (e: Entity, id: Id, value: T) -> ()) -> () -> (), + --- Enforce a check on entities to be created within desired range range: (self: World, range_begin: number, range_end: number?) -> (), --- Creates a new entity - entity: (self: World, id: Entity?) -> Entity, + entity: (self: World, id: (number | Entity)?) -> Entity, --- Creates a new entity located in the first 256 ids. --- These should be used for static components for fast access. component: (self: World) -> Entity, @@ -166,8 +257,8 @@ export type Record = { dense: i24, } export type ComponentRecord = { - records: { [Id]: number }, - counts: { [Id]: number }, + records: { [i24]: number }, + counts: { [i24]: number }, flags: number, size: number, @@ -176,7 +267,7 @@ export type ComponentRecord = { on_remove: ((entity: Entity, id: Entity) -> ())?, } export type ComponentIndex = Map -export type Archetypes = { [Id]: Archetype } +export type Archetypes = { [i24]: Archetype } export type EntityIndex = { dense_array: Map, @@ -250,7 +341,7 @@ end local function ECS_META(id: i53, ty: i53, value: any?) local bundle = ecs_metadata[id] if bundle == nil then - bundle = {} + bundle = {} :: Map ecs_metadata[id] = bundle end bundle[ty] = if value == nil then NULL else value @@ -317,10 +408,10 @@ local function ECS_PAIR_SECOND(e: i53): i24 end local function entity_index_try_get_any( - entity_index: EntityIndex, - entity: Entity -): Record? - local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity::number)] + entity_index: entityindex, + entity: i53 +): record? + local r = entity_index.sparse_array[ECS_ID(entity)] if not r or r.dense == 0 then return nil @@ -329,8 +420,8 @@ local function entity_index_try_get_any( return r end -local function entity_index_try_get(entity_index: EntityIndex, entity: Entity): Record? - local r = entity_index_try_get_any(entity_index, entity :: number) +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 @@ -343,7 +434,7 @@ local function entity_index_try_get(entity_index: EntityIndex, entity: Entity): return r end -local function entity_index_try_get_fast(entity_index: EntityIndex, entity: Entity): Record? +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 @@ -357,11 +448,11 @@ local function entity_index_try_get_fast(entity_index: EntityIndex, entity: Enti return r end -local function entity_index_is_alive(entity_index: EntityIndex, entity: Entity): boolean +local function entity_index_is_alive(entity_index: entityindex, entity: i53): boolean return entity_index_try_get(entity_index, entity) ~= nil end -local function entity_index_get_alive(entity_index: EntityIndex, entity: Entity): Entity? +local function entity_index_get_alive(entity_index: entityindex, entity: i53): i53? local r = entity_index_try_get_any(entity_index, entity :: number) if r then return entity_index.dense_array[r.dense] @@ -369,7 +460,7 @@ local function entity_index_get_alive(entity_index: EntityIndex, entity: Enti return nil end -local function ecs_get_alive(world: World, entity: Entity): Entity +local function ecs_get_alive(world: world, entity: i53): i53 if entity == 0 then return 0 end @@ -394,7 +485,7 @@ end local ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY = "Entity is outside range" -local function entity_index_new_id(entity_index: EntityIndex): Entity +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 @@ -415,22 +506,22 @@ local function entity_index_new_id(entity_index: EntityIndex): Entity alive_count += 1 entity_index.alive_count = alive_count dense_array[alive_count] = id - sparse_array[id] = { dense = alive_count } :: Record + sparse_array[id] = { dense = alive_count } :: record return id end -local function ecs_pair_first(world: World, e: i53) +local function ecs_pair_first(world: world, e: i53) local pred = ECS_PAIR_FIRST(e) return ecs_get_alive(world, pred) end -local function ecs_pair_second(world: World, e: i53) +local function ecs_pair_second(world: world, e: i53) local obj = ECS_PAIR_SECOND(e) return ecs_get_alive(world, obj) end -local function query_match(query: QueryInner, archetype: Archetype) +local function query_match(query: query, archetype: archetype) local columns_map = archetype.columns_map local with = query.filter_with @@ -452,7 +543,7 @@ local function query_match(query: QueryInner, archetype: Archetype) return true end -local function find_observers(world: World, event: Id, component: Id): { Observer }? +local function find_observers(world: world, event: i53, component: i53): { observer }? local cache = world.observable[event] if not cache then return nil @@ -461,11 +552,11 @@ local function find_observers(world: World, event: Id, component: Id): { Observe end local function archetype_move( - entity_index: EntityIndex, - entity: Entity, - to: Archetype, + entity_index: entityindex, + entity: i53, + to: archetype, dst_row: i24, - from: Archetype, + from: archetype, src_row: i24 ) local src_columns = from.columns @@ -506,7 +597,7 @@ local function archetype_move( src_entities[src_row] = e2 local sparse_array = entity_index.sparse_array - local record2 = sparse_array[ECS_ENTITY_T_LO(e2 :: number)] + local record2 = sparse_array[ECS_ID(e2)] record2.row = src_row else for i, column in src_columns do @@ -526,13 +617,13 @@ local function archetype_move( end end - src_entities[last] = nil :: any + src_entities[last] = nil dst_entities[dst_row] = entity end local function archetype_append( - entity: Entity, - archetype: Archetype + entity: i53, + archetype: archetype ): number local entities = archetype.entities local length = #entities + 1 @@ -541,10 +632,10 @@ local function archetype_append( end local function new_entity( - entity: Entity, - record: Record, - archetype: Archetype -): Record + entity: i53, + record: record, + archetype: archetype +): record local row = archetype_append(entity, archetype) record.archetype = archetype record.row = row @@ -552,10 +643,10 @@ local function new_entity( end local function entity_move( - entity_index: EntityIndex, - entity: Entity, - record: Record, - to: Archetype + entity_index: entityindex, + entity: i53, + record: record, + to: archetype ) local sourceRow = record.row local from = record.archetype @@ -565,11 +656,11 @@ local function entity_move( record.row = dst_row end -local function hash(arr: { Entity }): string +local function hash(arr: { i53 }): string return table.concat(arr, "_") end -local function fetch(id: Id, columns_map: { [Entity]: Column }, row: number): any +local function fetch(id: i53, columns_map: { [i53]: Column }, row: number): any local column = columns_map[id] if not column then @@ -579,8 +670,8 @@ local function fetch(id: Id, columns_map: { [Entity]: Column }, row: number): an return column[row] end -local function world_get(world: World, entity: Entity, - a: Id, b: Id?, c: Id?, d: Id?, e: Id?): ...any +local function world_get(world: world, entity: i53, + a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any local record = entity_index_try_get(world.entity_index, entity) if not record then return nil @@ -609,7 +700,7 @@ local function world_get(world: World, entity: Entity, end end -local function world_has_one_inline(world: World, entity: Entity, id: i53): boolean +local function world_has_one_inline(world: world, entity: i53, id: i53): boolean local record = entity_index_try_get(world.entity_index, entity) if not record then return false @@ -623,7 +714,7 @@ local function world_has_one_inline(world: World, entity: Entity, id: i53): bool return archetype.columns_map[id] ~= nil end -local function world_target(world: World, entity: Entity, relation: Id, index: number?): Entity? +local function world_target(world: world, entity: i53, relation: i53, index: number?): i53? local entity_index = world.entity_index local record = entity_index_try_get(entity_index, entity) if not record then @@ -635,7 +726,7 @@ local function world_target(world: World, entity: Entity, relation: Id, index: n return nil end - local r = ECS_PAIR(relation :: number, EcsWildcard) + local r = ECS_PAIR(relation, EcsWildcard) local idr = world.component_index[r] if not idr then @@ -651,7 +742,7 @@ local function world_target(world: World, entity: Entity, relation: Id, index: n local nth = index or 0 if nth >= count then - nth = nth + count + 1 + return nil end nth = archetype.types[nth + idr.records[archetype_id]] @@ -681,10 +772,10 @@ local function id_record_get(world: World, id: Entity): ComponentRecord? return nil end -local function id_record_ensure(world: World, id: Entity): ComponentRecord +local function id_record_ensure(world: world, id: i53): componentrecord local component_index = world.component_index local entity_index = world.entity_index - local idr: ComponentRecord? = component_index[id] + local idr: componentrecord? = component_index[id] if idr then return idr @@ -699,10 +790,10 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord local is_exclusive = false if is_pair then - relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id :: number)) :: i53 + 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) - target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id :: number)) :: i53 + 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) @@ -715,12 +806,12 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord if world_has_one_inline(world, relation, EcsExclusive) then is_exclusive = true end - else - local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) + end - if cleanup_policy == EcsDelete then - has_delete = true - end + local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) + + if cleanup_policy == EcsDelete then + has_delete = true end local on_add, on_change, on_remove = world_get(world, @@ -749,7 +840,7 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord on_add = on_add, on_change = on_change, on_remove = on_remove, - } :: ComponentRecord + } :: componentrecord component_index[id] = idr @@ -757,9 +848,9 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord end local function archetype_append_to_records( - idr: ComponentRecord, + idr: componentrecord, archetype_id: number, - columns_map: { [Id]: Column }, + columns_map: { [i53]: Column }, id: i53, index: number, column: Column @@ -777,59 +868,68 @@ local function archetype_append_to_records( end end -local function archetype_register(world: World, archetype: Archetype) - local archetype_id = archetype.id - local columns_map = archetype.columns_map - local columns = archetype.columns - - for i, component_id in archetype.types do - local idr = id_record_ensure(world, component_id) - local is_tag = bit32.btest(idr.flags, ECS_ID_IS_TAG) - local column = if is_tag then NULL_ARRAY else {} - columns[i] = column - - archetype_append_to_records(idr, archetype_id, columns_map, component_id :: number, i, column) - - if ECS_IS_PAIR(component_id :: number) then - local relation = ECS_PAIR_FIRST(component_id :: number) - local object = ECS_PAIR_SECOND(component_id :: number) - local r = ECS_PAIR(relation, EcsWildcard) - local idr_r = id_record_ensure(world, r) - - archetype_append_to_records(idr_r, archetype_id, columns_map, r, i, column) - - local t = ECS_PAIR(EcsWildcard, object) - local idr_t = id_record_ensure(world, t) - - archetype_append_to_records(idr_t, archetype_id, columns_map, t, i, column) - end - end - - world.archetype_index[archetype.type] = archetype - world.archetypes[archetype_id] = archetype - world.archetype_edges[archetype.id] = {} :: Map -end - -local function archetype_create(world: World, id_types: { Id }, ty, prev: i53?): Archetype +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 local length = #id_types local columns = (table.create(length) :: any) :: { Column } - local columns_map: { [Id]: Column } = {} + local columns_map: { [i53]: Column } = {} - local archetype: Archetype = { + local archetype: archetype = { columns = columns, columns_map = columns_map, entities = {}, id = archetype_id, type = ty, - types = id_types, - dead = false, + types = id_types } - archetype_register(world, archetype, false) + for i, component_id in archetype.types do + local idr = id_record_ensure(world, component_id) + idr.size += 1 + local is_tag = bit32.btest(idr.flags, ECS_ID_IS_TAG) + local column = if is_tag then NULL_ARRAY else {} + columns[i] = column + + archetype_append_to_records(idr, archetype_id, columns_map, component_id :: number, i, column) + + if ECS_IS_PAIR(component_id) then + local relation = ECS_PAIR_FIRST(component_id) + local object = ECS_PAIR_SECOND(component_id) + + local r = ECS_PAIR(relation, EcsWildcard) + local idr_r = id_record_ensure(world, r) + + idr_r.size += 1 + archetype_append_to_records(idr_r, archetype_id, columns_map, r, i, column) + 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 + + local t = ECS_PAIR(EcsWildcard, object) + local idr_t = id_record_ensure(world, t) + + idr_t.size += 1 + archetype_append_to_records(idr_t, archetype_id, columns_map, t, i, column) + + -- Hypothetically this should only capture leaf component records + local idr_t_wc_pairs = idr_t.wildcard_pairs + if not idr_t_wc_pairs then + idr_t_wc_pairs = {} :: {[i53]: componentrecord } + idr_t.wildcard_pairs = idr_t_wc_pairs + end + idr_t_wc_pairs[component_id] = idr + end + end + + world.archetype_index[archetype.type] = archetype + world.archetypes[archetype_id] = archetype + world.archetype_edges[archetype.id] = {} :: Map for id in columns_map do local observer_list = find_observers(world, EcsOnArchetypeCreate, id) @@ -837,17 +937,16 @@ local function archetype_create(world: World, id_types: { Id }, ty, prev: i53?): continue end for _, observer in observer_list do - if query_match(observer.query :: QueryInner, archetype) then - observer.callback(archetype) + if query_match(observer.query, archetype) then + observer.callback(archetype::any) end end end - return archetype end -local function world_range(world: World, range_begin: number, range_end: number?) +local function world_range(world: world, range_begin: number, range_end: number?) local entity_index = world.entity_index entity_index.range_begin = range_begin @@ -863,14 +962,14 @@ local function world_range(world: World, range_begin: number, range_end: number? dense_array[i] = i sparse_array[i] = { dense = 0 - } :: Record + } :: record end entity_index.max_id = range_begin - 1 entity_index.alive_count = range_begin - 1 end end -local function archetype_ensure(world: World, id_types: { Id }): Archetype +local function archetype_ensure(world: world, id_types: { i53 }): archetype if #id_types < 1 then return world.ROOT_ARCHETYPE end @@ -878,10 +977,6 @@ local function archetype_ensure(world: World, id_types: { Id }): Archetype local ty = hash(id_types) local archetype = world.archetype_index[ty] if archetype then - if archetype.dead then - archetype_register(world, archetype) - archetype.dead = false :: any - end return archetype end @@ -901,10 +996,10 @@ local function find_insert(id_types: { i53 }, toAdd: i53): number end local function find_archetype_without( - world: World, - node: Archetype, - id: Id -): Archetype + world: world, + node: archetype, + id: i53 +): archetype local id_types = node.types local at = table.find(id_types, id) @@ -916,11 +1011,11 @@ end local function create_edge_for_remove( - world: World, - node: Archetype, - edge: Map, - id: Id -): Archetype + world: world, + node: archetype, + edge: Map, + id: i53 +): archetype local to = find_archetype_without(world, node, id) local edges = world.archetype_edges local archetype_id = node.id @@ -930,14 +1025,14 @@ local function create_edge_for_remove( end local function archetype_traverse_remove( - world: World, - id: Id, - from: Archetype -): Archetype + world: world, + id: i53, + from: archetype +): archetype local edges = world.archetype_edges local edge = edges[from.id] - local to: Archetype = edge[id] + local to: archetype = edge[id] if to == nil then to = find_archetype_without(world, from, id) edge[id] = to @@ -947,7 +1042,11 @@ local function archetype_traverse_remove( return to end -local function find_archetype_with(world: World, id: Id, from: Archetype): Archetype +local function find_archetype_with( + world: world, + id: i53, + from: archetype +): archetype local id_types = from.types local dst = table.clone(id_types) @@ -958,7 +1057,11 @@ local function find_archetype_with(world: World, id: Id, from: Archetype): Arche return archetype_ensure(world, dst) end -local function archetype_traverse_add(world: World, id: Id, from: Archetype): Archetype +local function archetype_traverse_add( + world: world, + id: i53, + from: archetype +): archetype from = from or world.ROOT_ARCHETYPE if from.columns_map[id] then return from @@ -976,7 +1079,7 @@ local function archetype_traverse_add(world: World, id: Id, from: Archetype): Ar return to end -local function world_component(world: World): i53 +local function world_component(world: world): i53 local id = (world.max_component_id :: number) + 1 if id > HI_COMPONENT_ID then -- IDs are partitioned into ranges because component IDs are not nominal, @@ -1005,36 +1108,24 @@ local function archetype_fast_delete(columns: { Column }, column_count: number, end end -local function archetype_delete(world: World, archetype: Archetype, row: number) +local function archetype_delete(world: world, archetype: archetype, row: number) local entity_index = world.entity_index - local component_index = world.component_index local columns = archetype.columns - local id_types = archetype.types local entities = archetype.entities local column_count = #entities local last = #entities local move = entities[last] -- We assume first that the entity is the last in the archetype - local delete = move if row ~= last then - local record_to_move = entity_index_try_get_any(entity_index, move :: number) + local record_to_move = entity_index_try_get_any(entity_index, move) if record_to_move then record_to_move.row = row end - delete = entities[row] entities[row] = move end - for _, id in id_types do - local idr = component_index[id] - local on_remove = idr.on_remove - if on_remove then - on_remove(delete, id) - end - end - entities[last] = nil :: any if row == last then @@ -1045,7 +1136,7 @@ local function archetype_delete(world: World, archetype: Archetype, row: number) end -local function archetype_destroy(world: World, archetype: Archetype) +local function archetype_destroy(world: world, archetype: archetype) if archetype == world.ROOT_ARCHETYPE then return end @@ -1059,8 +1150,8 @@ local function archetype_destroy(world: World, archetype: Archetype) end local archetype_id = archetype.id - -- world.archetypes[archetype_id] = nil :: any - -- world.archetype_index[archetype.type] = nil :: any + world.archetypes[archetype_id] = nil :: any + world.archetype_index[archetype.type] = nil :: any local columns_map = archetype.columns_map for id in columns_map do @@ -1077,12 +1168,10 @@ local function archetype_destroy(world: World, archetype: Archetype) end for _, observer in observer_list do if query_match(observer.query, archetype) then - observer.callback(archetype) + observer.callback(archetype::any) end end end - - archetype.dead = true end local function NOOP() end @@ -1102,7 +1191,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) local columns_map = archetype.columns_map local ids = query.ids - local A, B, C, D, E, F, G, H, I = unpack(ids) + local A, B, C, D, E, F, G, H, I = unpack(ids :: { Id }) local a: Column, b: Column, c: Column, d: Column local e: Column, f: Column, g: Column, h: Column @@ -1413,7 +1502,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) i -= 1 for i = 9, ids_len do - output[i - 8] = columns_map[i][row] + 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) @@ -1432,9 +1521,9 @@ local function query_iter(query): () -> (number, ...any) return query_next end -local function query_without(query: QueryInner, ...: i53) +local function query_without(query: QueryInner, ...: Id) local without = { ... } - query.filter_without = without + query.filter_without = without :: any local compatible_archetypes = query.compatible_archetypes for i = #compatible_archetypes, 1, -1 do local archetype = compatible_archetypes[i] @@ -1462,9 +1551,9 @@ local function query_without(query: QueryInner, ...: i53) return query :: any end -local function query_with(query: QueryInner, ...: i53) +local function query_with(query: QueryInner, ...: Id) local compatible_archetypes = query.compatible_archetypes - local with = { ... } + local with = { ... } :: any query.filter_with = with for i = #compatible_archetypes, 1, -1 do @@ -1512,7 +1601,7 @@ local function query_cached(query: QueryInner) local compatible_archetypes = query.compatible_archetypes local lastArchetype = 1 - local A, B, C, D, E, F, G, H, I = unpack(ids) + local A, B, C, D, E, F, G, H, I = unpack(ids :: { Id }) local a: Column, b: Column, c: Column, d: Column local e: Column, f: Column, g: Column, h: Column @@ -1527,10 +1616,10 @@ local function query_cached(query: QueryInner) -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively -- because the event will be emitted for all components of that Archetype. local observable = world.observable - local on_create_action = observable[EcsOnArchetypeCreate] + local on_create_action = observable[EcsOnArchetypeCreate::any] if not on_create_action then on_create_action = {} :: Map - observable[EcsOnArchetypeCreate] = on_create_action + observable[EcsOnArchetypeCreate::any] = on_create_action end local query_cache_on_create = on_create_action[A] if not query_cache_on_create then @@ -1538,10 +1627,10 @@ local function query_cached(query: QueryInner) on_create_action[A] = query_cache_on_create end - local on_delete_action = observable[EcsOnArchetypeDelete] + local on_delete_action = observable[EcsOnArchetypeDelete::any] if not on_delete_action then on_delete_action = {} :: Map - observable[EcsOnArchetypeDelete] = on_delete_action + observable[EcsOnArchetypeDelete::any] = on_delete_action end local query_cache_on_delete = on_delete_action[A] if not query_cache_on_delete then @@ -1888,10 +1977,10 @@ local function query_cached(query: QueryInner) i -= 1 for i = 9, ids_len do - output[i - 8] = columns_map[i][row] + 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], unpack(output) + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row], unpack(output) end end @@ -1972,10 +2061,10 @@ local function world_query(world: World, ...) return q end -local function world_each(world: World, id: Id): () -> Entity +local function world_each(world: world, id: i53): () -> i53 local idr = world.component_index[id] if not idr then - return NOOP :: () -> Entity + return NOOP :: () -> i53 end local records = idr.records @@ -1983,18 +2072,18 @@ local function world_each(world: World, id: Id): () -> Entity local archetype_id = next(records, nil) :: number local archetype = archetypes[archetype_id] if not archetype then - return NOOP :: () -> Entity + return NOOP :: () -> i53 end local entities = archetype.entities local row = #entities - return function(): any + return function() local entity = entities[row] while not entity do archetype_id = next(records, archetype_id) :: number if not archetype_id then - return + return nil :: any end archetype = archetypes[archetype_id] entities = archetype.entities @@ -2006,11 +2095,11 @@ local function world_each(world: World, id: Id): () -> Entity end end -local function world_children(world: World, parent: Id) - return world_each(world, ECS_PAIR(EcsChildOf, parent::number)) +local function world_children(world: world, parent: i53) + return world_each(world, ECS_PAIR(EcsChildOf, parent)) end -local function ecs_bulk_insert(world: World, entity: Entity, ids: { Entity }, values: { any }) +local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values: { any }) local entity_index = world.entity_index local r = entity_index_try_get(entity_index, entity) if not r then @@ -2088,7 +2177,7 @@ local function ecs_bulk_insert(world: World, entity: Entity, ids: { Entity }, va end end -local function ecs_bulk_remove(world: World, entity: Entity, ids: { Entity }) +local function ecs_bulk_remove(world: world, entity: i53, ids: { i53 }) local entity_index = world.entity_index local r = entity_index_try_get(entity_index, entity) if not r then @@ -2100,7 +2189,7 @@ local function ecs_bulk_remove(world: World, entity: Entity, ids: { Entity }) return end - local remove: { [Entity]: boolean } = {} + local remove: { [i53]: boolean } = {} local columns_map = from.columns_map @@ -2123,7 +2212,7 @@ local function ecs_bulk_remove(world: World, entity: Entity, ids: { Entity }) from = to end - local dst_types = table.clone(from.types) :: { Entity } + local dst_types = table.clone(from.types) :: { i53 } for id in remove do local at = table.find(dst_types, id) @@ -2131,32 +2220,39 @@ local function ecs_bulk_remove(world: World, entity: Entity, ids: { Entity }) end to = archetype_ensure(world, dst_types) + if from ~= to then entity_move(entity_index, entity, r, to) end end local function world_new() - local eindex_dense_array = {} :: { Entity } - local eindex_sparse_array = {} :: { Record } - local eindex_alive_count = 0 - local eindex_max_id = 0 + local eindex_dense_array = {} :: { i53 } + local eindex_sparse_array = {} :: { record } local entity_index = { dense_array = eindex_dense_array, sparse_array = eindex_sparse_array, - alive_count = eindex_alive_count, - max_id = eindex_max_id, - } :: EntityIndex + alive_count = 0, + max_id = 0, + } :: entityindex - local component_index = {} :: ComponentIndex + local component_index = {} :: Map - local archetype_index = {} :: { [string]: Archetype } - local archetypes = {} :: Archetypes - local archetype_edges = {} :: { [number]: { [Id]: Archetype } } + local archetype_index = {} :: { [string]: archetype } + local archetypes = {} :: Map + local archetype_edges = {} :: { [number]: { [i53]: archetype } } local observable = {} + type Signal = { [i53]: { Listener } } + + local signals = { + added = {} :: Signal, + changed = {} :: Signal, + removed = {} :: Signal + } + local world = { archetype_edges = archetype_edges, @@ -2170,27 +2266,24 @@ local function world_new() max_component_id = ecs_max_component_id, observable = observable, - } :: World + signals = signals, + } :: world + local ROOT_ARCHETYPE = archetype_create(world, {}, "") world.ROOT_ARCHETYPE = ROOT_ARCHETYPE - local function inner_entity_index_try_get_any(entity: number): Record? + local function inner_entity_index_try_get_any(entity: i53): record? local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)] - - if not r or r.dense == 0 then - return nil - end - return r end local function inner_archetype_move( - entity: Entity, - to: Archetype, + entity: i53, + to: archetype, dst_row: i24, - from: Archetype, + from: archetype, src_row: i24 ) local src_columns = from.columns @@ -2219,7 +2312,7 @@ local function world_new() local e2 = src_entities[last] src_entities[src_row] = e2 - local record2 = eindex_sparse_array[ECS_ENTITY_T_LO(e2 :: number)] + local record2 = eindex_sparse_array[ECS_ENTITY_T_LO(e2)] record2.row = src_row else for i, column in src_columns do @@ -2243,10 +2336,10 @@ local function world_new() end local function inner_entity_move( - entity_index: EntityIndex, - entity: Entity, - record: Record, - to: Archetype + entity_index: entityindex, + entity: i53, + record: record, + to: archetype ) local sourceRow = record.row local from = record.archetype @@ -2270,8 +2363,8 @@ local function world_new() -- return r -- end - local function inner_entity_index_try_get_unsafe(entity: number): Record? - local r = inner_entity_index_try_get_any(entity) + local function inner_entity_index_try_get_unsafe(entity: i53): record? + local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)] if r then local r_dense = r.dense -- if r_dense > entity_index.alive_count then @@ -2284,10 +2377,128 @@ local function world_new() return r end - local function inner_world_add( - world: World, - entity: Entity, - id: Id + 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 + + local function inner_world_set(world: world, entity: i53, id: i53, data): () + local record = inner_entity_index_try_get_unsafe(entity) + if not record then + return + end + + local from: archetype = record.archetype + 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 + on_change(entity, id, data) + end + else + 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] + + local edge = archetype_edges[src.id] + to = edge[id] + if to == nil then + 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 + + 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 + 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]) + 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 + else + local edges = archetype_edges + local edge = edges[src.id] + + to = edge[id] + 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 + inner_entity_move(entity_index, entity, record, to) + 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 + on_add(entity, id, data) + end + end + end + + local function inner_world_add( + world: world, + entity: i53, + id: i53 ): () local entity_index = world.entity_index local record = inner_entity_index_try_get_unsafe(entity :: number) @@ -2296,70 +2507,77 @@ local function world_new() end local from = record.archetype - if ECS_IS_PAIR(id::number) then - local src = from or ROOT_ARCHETYPE + local src = from or ROOT_ARCHETYPE + if src.columns_map[id] then + return + end + 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] + local edge = archetype_edges[src.id] - local to = edge[id] - local idr: ComponentRecord - if not to then - local first = ECS_PAIR_FIRST(id::number) - local wc = ECS_PAIR(first, EcsWildcard) - idr = component_index[wc] - if idr and bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then + to = edge[id] + if to == nil then + 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 - local dst = table.clone(id_types) - dst[cr] = id - to = archetype_ensure(world, dst) - else - to = find_archetype_with(world, id, src) - idr = component_index[id] + + to = exclusive_traverse_add(src, cr, id) end - else + end + + if not to then to = find_archetype_with(world, id, src) - idr = component_index[id] - end - edge[id] = to - else - if to.dead then - archetype_register(world, to) + if not idr then + idr = component_index[wc] + end edge[id] = to - archetype_edges[to.id][id] = src - to.dead = false + archetype_edges[(to :: Archetype).id][id] = src end - idr = component_index[id] - end - if from == to then - return - end - if from then - inner_entity_move(entity_index, entity, record, to) else - if #to.types > 0 then - new_entity(entity, record, to) + if bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then + 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]) + 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 + else + local edges = archetype_edges + local edge = edges[src.id] - local on_add = idr.on_add - - if on_add then - on_add(entity, id) + to = edge[id] + 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 - return - end - local to = archetype_traverse_add(world, id, from) - if from == to then - return - end if from then inner_entity_move(entity_index, entity, record, to) else @@ -2368,7 +2586,6 @@ local function world_new() end end - local idr = component_index[id] local on_add = idr.on_add if on_add then @@ -2376,9 +2593,9 @@ local function world_new() end end - local function inner_world_get(world: World, entity: Entity, - a: Id, b: Id?, c: Id?, d: Id?, e: Id?): ...any - local record = inner_entity_index_try_get_unsafe(entity::number) + local function inner_world_get(world: world, entity: i53, + a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any + local record = inner_entity_index_try_get_unsafe(entity) if not record then return nil end @@ -2406,6 +2623,137 @@ local function world_new() end end + type Listener = (e: i53, id: i53, value: T?) -> () + + world.added = function(_: world, component: i53, fn: Listener) + local listeners = signals.added[component] + if not listeners then + listeners = {} + signals.added[component] = listeners + + local function on_add(entity, id, value) + for _, listener in listeners :: { Listener } do + listener(entity, id, value) + end + end + local existing_hook = inner_world_get(world, component, EcsOnAdd) :: Listener + if existing_hook then + table.insert(listeners, existing_hook) + end + + 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 + else + local idr = component_index[component] + if idr then + idr.on_add = on_add + end + end + inner_world_set(world, component, EcsOnAdd, on_add) + 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( + _: world, + component: i53, + fn: Listener + ) + local listeners = signals.changed[component] + if not listeners then + listeners = {} + signals.changed[component] = listeners + local function on_change(entity, id, value: any) + for _, listener in listeners :: { Listener } do + listener(entity, id, value) + end + end + local existing_hook = inner_world_get(world, component, EcsOnChange) :: Listener + if existing_hook then + table.insert(listeners, existing_hook) + end + + 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 + else + local idr = component_index[component] + if idr then + idr.on_change = on_change + end + end + + inner_world_set(world, component, EcsOnChange, on_change) + 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.removed = function(_: world, component: i53, fn: (i53, i53) -> ()) + local listeners = signals.removed[component] + if not listeners then + listeners = {} + signals.removed[component] = listeners + local function on_remove(entity, id) + for _, listener in listeners :: { Listener } do + listener(entity, id) + end + end + + local existing_hook = inner_world_get(world, component, EcsOnRemove) :: Listener + if existing_hook then + table.insert(listeners, existing_hook) + end + + 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 + else + local idr = component_index[component] + if idr then + idr.on_remove = on_remove + end + end + + inner_world_set(world, component, EcsOnRemove, on_remove) + 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 + local function inner_world_has(world: World, entity: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean @@ -2428,8 +2776,8 @@ local function world_new() (e == nil or error("args exceeded")) end - local function inner_world_target(world: World, entity: Entity, relation: Id, index: number?): Entity? - local record = inner_entity_index_try_get_unsafe(entity :: number) + local function inner_world_target(world: world, entity: i53, relation: i53, index: number?): i53? + local record = inner_entity_index_try_get_unsafe(entity) if not record then return nil end @@ -2439,7 +2787,7 @@ local function world_new() return nil end - local r = ECS_PAIR(relation::number, EcsWildcard) + local r = ECS_PAIR(relation, EcsWildcard) local idr = world.component_index[r] if not idr then @@ -2455,7 +2803,7 @@ local function world_new() local nth = index or 0 if nth >= count then - nth = nth + count + 1 + return nil end nth = archetype.types[nth + idr.records[archetype_id]] @@ -2465,112 +2813,16 @@ local function world_new() end return entity_index_get_alive(world.entity_index, - ECS_PAIR_SECOND(nth :: number)) + ECS_PAIR_SECOND(nth)) end - local function inner_world_parent(world: World, entity: Entity): Entity? + local function inner_world_parent(world: world, entity: i53): i53? return inner_world_target(world, entity, EcsChildOf, 0) end - local function inner_archetype_traverse_add(id: Id, from: Archetype): Archetype - from = from or ROOT_ARCHETYPE - if from.columns_map[id] then - return from - end - local edges = archetype_edges - local edge = edges[from.id] - - local to = edge[id] :: Archetype - if not to then - to = find_archetype_with(world, id, from) - edge[id] = to - edges[to.id][id] = from - end - - return to - end - - local function inner_world_set(world: World, entity: Entity, id: Id, data: a): () - local record = inner_entity_index_try_get_unsafe(entity :: number) - if not record then - return - end - - local from: Archetype = record.archetype - 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 - on_change(entity, id, data) - end - else - local to: Archetype - local idr: ComponentRecord - if ECS_IS_PAIR(id::number) then - local edge = archetype_edges[src.id] - to = edge[id] - if not to then - local first = ECS_PAIR_FIRST(id::number) - local wc = ECS_PAIR(first, EcsWildcard) - idr = component_index[wc] - if idr and bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) 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 - local dst = table.clone(id_types) - dst[cr] = id - to = archetype_ensure(world, dst) - else - to = find_archetype_with(world, id, src) - idr = component_index[id] - end - else - to = find_archetype_with(world, id, src) - idr = component_index[id] - end - edge[id] = to - archetype_edges[to.id][id] = src - else - idr = component_index[id] - end - else - to = inner_archetype_traverse_add(id, from) - idr = component_index[id] - end - - if from then - -- If there was a previous archetype, then the entity needs to move the archetype - inner_entity_move(entity_index, entity, record, to) - 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 - on_add(entity, id, data) - end - end - end - - local function inner_world_entity(world: World, entity: Entity?): Entity + local function inner_world_entity(world: world, entity: i53?): i53 if entity then - local index = ECS_ID(entity :: number) + local index = ECS_ID(entity) local alive_count = entity_index.alive_count local r = eindex_sparse_array[index] if r then @@ -2579,19 +2831,16 @@ local function world_new() if not dense or r.dense == 0 then r.dense = index dense = index - local any = eindex_dense_array[dense] - if any == entity then - local e_swap = eindex_dense_array[dense] - local r_swap = inner_entity_index_try_get_any(e_swap :: number) :: Record + local e_swap = eindex_dense_array[dense] + local r_swap = inner_entity_index_try_get_any(e_swap) :: record - r_swap.dense = dense - alive_count += 1 - entity_index.alive_count = alive_count - r.dense = alive_count + 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 + eindex_dense_array[dense] = e_swap + eindex_dense_array[alive_count] = entity return entity end @@ -2599,7 +2848,7 @@ local function world_new() if any ~= entity then if alive_count <= dense then local e_swap = eindex_dense_array[dense] - local r_swap = inner_entity_index_try_get_any(e_swap :: number) :: Record + local r_swap = inner_entity_index_try_get_any(e_swap) :: record r_swap.dense = dense alive_count += 1 @@ -2613,8 +2862,8 @@ local function world_new() return entity else - for i = eindex_max_id + 1, index do - eindex_sparse_array[i] = { dense = i } :: Record + for i = entity_index.max_id + 1, index do + eindex_sparse_array[i] = { dense = i } :: record eindex_dense_array[i] = i end entity_index.max_id = index @@ -2641,8 +2890,8 @@ local function world_new() return entity_index_new_id(entity_index) end - local function inner_world_remove(world: World, entity: Entity, id: Id) - local record = inner_entity_index_try_get_unsafe(entity :: number) + local function inner_world_remove(world: world, entity: i53, id: i53) + local record = inner_entity_index_try_get_unsafe(entity) if not record then return end @@ -2655,6 +2904,7 @@ local function world_new() if from.columns_map[id] then local idr = world.component_index[id] local on_remove = idr.on_remove + if on_remove then on_remove(entity, id) end @@ -2665,11 +2915,11 @@ local function world_new() end end - local function inner_world_clear(world: World, entity: Entity) - local tgt = ECS_PAIR(EcsWildcard, entity::number) + local function inner_world_clear(world: world, entity: i53) + local tgt = ECS_PAIR(EcsWildcard, entity) local idr_t = component_index[tgt] local idr = component_index[entity] - local rel = ECS_PAIR(entity::number, EcsWildcard) + local rel = ECS_PAIR(entity, EcsWildcard) local idr_r = component_index[rel] if idr then @@ -2697,11 +2947,11 @@ local function world_new() local node = idr_t_archetype for _, id in idr_t_types do - if not ECS_IS_PAIR(id::number) then + if not ECS_IS_PAIR(id) then continue end local object = entity_index_get_alive( - entity_index, ECS_PAIR_SECOND(id::number)) + entity_index, ECS_PAIR_SECOND(id)) if object ~= entity then continue end @@ -2716,7 +2966,7 @@ local function world_new() for i = #entities, 1, -1 do local e = entities[i] - local r = inner_entity_index_try_get_unsafe(e::number) :: Record + local r = inner_entity_index_try_get_unsafe(e) :: record inner_entity_move(entity_index, e, r, node) end end @@ -2745,26 +2995,30 @@ local function world_new() end for i = #entities, 1, -1 do local e = entities[i] - local r = inner_entity_index_try_get_unsafe(e::number) :: Record + local r = inner_entity_index_try_get_unsafe(e) :: record inner_entity_move(entity_index, e, r, node) end end end end - local function inner_world_delete(world: World, entity: Entity) - local record = inner_entity_index_try_get_unsafe(entity::number) + local function inner_world_delete(world: world, entity: i53) + local record = inner_entity_index_try_get_unsafe(entity) if not record then return end local archetype = record.archetype - local row = record.row if archetype then - -- In the future should have a destruct mode for - -- deleting archetypes themselves. Maybe requires recycling - archetype_delete(world, archetype, row) + 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) end local component_index = world.component_index @@ -2778,7 +3032,7 @@ local function world_new() if idr then local flags = idr.flags - if bit32.btest(flags, ECS_ID_DELETE) then + if (bit32.btest(flags, ECS_ID_DELETE) == true) then for archetype_id in idr.records do local idr_archetype = archetypes[archetype_id] @@ -2830,62 +3084,61 @@ local function world_new() end end if idr_t then - local archetype_ids = idr_t.records - for archetype_id in archetype_ids do - local idr_t_archetype = archetypes[archetype_id] - local node = idr_t_archetype - local idr_t_types = idr_t_archetype.types - local entities = idr_t_archetype.entities - - local deleted = false - for _, id in idr_t_types do - if not ECS_IS_PAIR(id::number) then - continue - end - local object = entity_index_get_alive( - entity_index, ECS_PAIR_SECOND(id::number)) - 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 + for id, cr in idr_t.wildcard_pairs do + local flags = cr.flags + local flags_delete_mask = bit32.btest(flags, ECS_ID_DELETE) + local on_remove = cr.on_remove + if flags_delete_mask then + for archetype_id in cr.records do + local idr_t_archetype = archetypes[archetype_id] + local entities = idr_t_archetype.entities for i = #entities, 1, -1 do local child = entities[i] inner_world_delete(world, child) end - deleted = true - break - else - node = archetype_traverse_remove(world, id, node) - local on_remove = component_index[id].on_remove - if on_remove then - for _, entity in entities do - on_remove(entity, id) + end + else + for archetype_id in cr.records do + local idr_t_archetype = archetypes[archetype_id] + local entities = idr_t_archetype.entities + -- archetype_traverse_remove is not idempotent meaning + -- this access is actually unsafe because it can + -- incorrectly cache an edge despite a node of the + -- component id on the archetype does not exist. This + -- requires careful testing to ensure correct values are + -- being passed to the arguments. + local to = archetype_traverse_remove(world, id, idr_t_archetype) + + for i = #entities, 1, -1 do + local e = entities[i] + local r = eindex_sparse_array[ECS_ID(e :: number)] + if on_remove then + on_remove(e, id) + + local from = r.archetype + if from ~= idr_t_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, id, from) + end end + + inner_entity_move(entity_index, e, r, to) end end end - if not deleted then - for i = #entities, 1, -1 do - local e = entities[i] - local r = inner_entity_index_try_get_unsafe(e::number) :: Record - inner_entity_move(entity_index, e, r, node) - end + for archetype_id in cr.records do + archetype_destroy(world, archetypes[archetype_id]) end end - - for archetype_id in archetype_ids do - archetype_destroy(world, archetypes[archetype_id]) - end end if idr_r then local archetype_ids = idr_r.records local flags = idr_r.flags - if bit32.btest(flags, ECS_ID_DELETE) then + local has_delete_policy = bit32.btest(flags, ECS_ID_DELETE) + if has_delete_policy then for archetype_id in archetype_ids do local idr_r_archetype = archetypes[archetype_id] local entities = idr_r_archetype.entities @@ -2915,9 +3168,10 @@ local function world_new() end end end + for i = #entities, 1, -1 do local e = entities[i] - local r = inner_entity_index_try_get_unsafe(e::number) :: Record + local r = inner_entity_index_try_get_unsafe(e) :: record inner_entity_move(entity_index, e, r, node) end end @@ -2933,25 +3187,25 @@ local function world_new() entity_index.alive_count = i_swap - 1 local e_swap = eindex_dense_array[i_swap] - local r_swap = inner_entity_index_try_get_any(e_swap :: number) :: Record + local r_swap = inner_entity_index_try_get_any(e_swap) :: record r_swap.dense = dense record.archetype = nil :: any record.row = nil :: any record.dense = i_swap eindex_dense_array[dense] = e_swap - eindex_dense_array[i_swap] = ECS_GENERATION_INC(entity :: number) + eindex_dense_array[i_swap] = ECS_GENERATION_INC(entity) end - local function inner_world_exists(world: World, entity: Entity): boolean - return inner_entity_index_try_get_any(entity :: number) ~= nil + local function inner_world_exists(world: world, entity: i53): boolean + return inner_entity_index_try_get_any(entity) ~= nil end - local function inner_world_contains(world: World, entity: Entity): boolean - return entity_index_is_alive(world.entity_index, entity) + local function inner_world_contains(world: world, entity: i53): boolean + return entity_index_is_alive(entity_index, entity) end - local function inner_world_cleanup(world: World) + local function inner_world_cleanup(world: world) for _, archetype in archetypes do if #archetype.entities == 0 then archetype_destroy(world, archetype) @@ -3015,8 +3269,10 @@ local function world_new() inner_world_set(world, EcsWildcard, EcsName, "jecs.Wildcard") inner_world_set(world, EcsChildOf, EcsName, "jecs.ChildOf") inner_world_set(world, EcsComponent, EcsName, "jecs.Component") + inner_world_set(world, EcsOnDelete, EcsName, "jecs.OnDelete") inner_world_set(world, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget") + inner_world_set(world, EcsDelete, EcsName, "jecs.Delete") inner_world_set(world, EcsRemove, EcsName, "jecs.Remove") inner_world_set(world, EcsName, EcsName, "jecs.Name") @@ -3025,6 +3281,9 @@ local function world_new() inner_world_add(world, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) inner_world_add(world, EcsChildOf, EcsExclusive) + inner_world_add(world, EcsOnDelete, EcsExclusive) + inner_world_add(world, EcsOnDeleteTarget, EcsExclusive) + for i = EcsRest + 1, ecs_max_tag_id do entity_index_new_id(entity_index) end @@ -3060,7 +3319,7 @@ end -- end -- -local function ecs_is_tag(world: World, entity: Entity): boolean +local function ecs_is_tag(world: world, entity: i53): boolean local idr = world.component_index[entity] if idr then return bit32.btest(idr.flags, ECS_ID_IS_TAG) @@ -3068,68 +3327,71 @@ local function ecs_is_tag(world: World, entity: Entity): boolean return not world_has_one_inline(world, entity, EcsComponent) end +local function ecs_entity_record(world: world, entity: i53) + return entity_index_try_get(world.entity_index, entity) +end + return { world = world_new :: () -> World, + World = { + new = world_new + }, component = (ECS_COMPONENT :: any) :: () -> Entity, tag = (ECS_TAG :: any) :: () -> Entity, meta = (ECS_META :: any) :: (id: Entity, id: Id, value: a?) -> Entity, is_tag = (ecs_is_tag :: any) :: (World, Id) -> boolean, - OnAdd = (EcsOnAdd :: any) :: Entity<(entity: Entity, id: Id, data: T) -> ()>, - OnRemove = (EcsOnRemove :: any) :: Entity<(entity: Entity, id: Id) -> ()>, - OnChange = (EcsOnChange :: any) :: Entity<(entity: Entity, id: Id, data: T) -> ()>, + OnAdd = (EcsOnAdd :: any) :: Id<(entity: Entity, id: Id, data: T) -> ()>, + OnRemove = (EcsOnRemove :: any) :: Id<(entity: Entity, id: Id) -> ()>, + OnChange = (EcsOnChange :: any) :: Id<(entity: Entity, id: Id, data: T) -> ()>, ChildOf = (EcsChildOf :: any) :: Entity, Component = (EcsComponent :: any) :: Entity, - Wildcard = (EcsWildcard :: any) :: Entity, - w = (EcsWildcard :: any) :: Entity, + Wildcard = (EcsWildcard :: any) :: Id, + w = (EcsWildcard :: any) :: Id, OnDelete = (EcsOnDelete :: any) :: Entity, OnDeleteTarget = (EcsOnDeleteTarget :: any) :: Entity, Delete = (EcsDelete :: any) :: Entity, Remove = (EcsRemove :: any) :: Entity, - Name = (EcsName :: any) :: Entity, - Exclusive = EcsExclusive :: Entity, - ArchetypeCreate = EcsOnArchetypeCreate, - ArchetypeDelete = EcsOnArchetypeDelete, + Name = (EcsName :: any) :: Id, + Exclusive = (EcsExclusive :: any) :: Entity, + ArchetypeCreate = (EcsOnArchetypeCreate :: any) :: Entity, + ArchetypeDelete = (EcsOnArchetypeDelete :: any) :: Entity, Rest = (EcsRest :: any) :: Entity, - pair = (ECS_PAIR :: any) :: (first: Id

, second: Id) -> Pair, + pair = ECS_PAIR :: (first: Id

, second: Id) -> Pair, - -- Inwards facing API for testing - ECS_ID = ECS_ENTITY_T_LO, - ECS_GENERATION_INC = ECS_GENERATION_INC, - ECS_GENERATION = ECS_GENERATION, - ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD, - ECS_ID_DELETE = ECS_ID_DELETE, - ECS_META_RESET = ECS_META_RESET, - - IS_PAIR = (ECS_IS_PAIR :: any) :: (pair: Pair) -> boolean, + IS_PAIR = ECS_IS_PAIR :: (pair: Pair) -> boolean, ECS_PAIR_FIRST = ECS_PAIR_FIRST :: (pair: Pair) -> Id

, ECS_PAIR_SECOND = ECS_PAIR_SECOND :: (pair: Pair) -> Id, - pair_first = (ecs_pair_first :: any) :: (world: World, pair: Pair) -> Id

, - pair_second = (ecs_pair_second :: any) :: (world: World, pair: Pair) -> Id, + pair_first = ecs_pair_first :: (world: World, pair: Pair) -> Id

, + pair_second = ecs_pair_second :: (world: World, pair: Pair) -> Id, entity_index_get_alive = entity_index_get_alive, archetype_append_to_records = archetype_append_to_records, - id_record_ensure = id_record_ensure, - component_record = id_record_get, - archetype_create = archetype_create, - archetype_ensure = archetype_ensure, + id_record_ensure = id_record_ensure :: (World, Id) -> ComponentRecord, + component_record = id_record_get :: (World, Id) -> ComponentRecord?, + record = ecs_entity_record :: (World, Entity) -> Record, + + archetype_create = archetype_create :: (World, { Id }, string) -> Archetype, + archetype_ensure = archetype_ensure :: (World, { Id }) -> Archetype, find_insert = find_insert, - find_archetype_with = find_archetype_with, - find_archetype_without = find_archetype_without, + find_archetype_with = find_archetype_with :: (World, Id, Archetype) -> Archetype, + find_archetype_without = find_archetype_without :: (World, Id, Archetype) -> Archetype, create_edge_for_remove = create_edge_for_remove, - archetype_traverse_add = archetype_traverse_add, - archetype_traverse_remove = archetype_traverse_remove, - bulk_insert = ecs_bulk_insert, - bulk_remove = ecs_bulk_remove, + archetype_traverse_add = archetype_traverse_add :: (World, Id, Archetype) -> Archetype, + archetype_traverse_remove = archetype_traverse_remove :: (World, Id, Archetype) -> Archetype, + bulk_insert = ecs_bulk_insert :: (World, Entity, { Id }, { any }) -> (), + bulk_remove = ecs_bulk_remove :: (World, Entity, { Id }) -> (), - entity_move = entity_move, + entity_move = entity_move :: (EntityIndex, Entity, Record, Archetype) -> (), - entity_index_try_get = entity_index_try_get, - entity_index_try_get_fast = entity_index_try_get_fast, - entity_index_try_get_any = entity_index_try_get_any, - entity_index_is_alive = entity_index_is_alive, - entity_index_new_id = entity_index_new_id, + 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, + entity_index_new_id = entity_index_new_id :: (EntityIndex) -> Entity, + + Query = Query, query_iter = query_iter, query_iter_init = query_iter_init, @@ -3138,5 +3400,16 @@ return { query_archetypes = query_archetypes, query_match = query_match, - find_observers = find_observers, + find_observers = find_observers :: (World, Id, Id) -> { Observer }, + + -- Inwards facing API for testing + ECS_ID = ECS_ENTITY_T_LO :: (Entity) -> number, + ECS_GENERATION_INC = ECS_GENERATION_INC :: (Entity) -> Entity, + ECS_GENERATION = ECS_GENERATION :: (Entity) -> number, + 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, + ECS_COMBINE = ECS_COMBINE :: (number, number) -> Entity, + ECS_ENTITY_MASK = ECS_ENTITY_MASK, }