From 0bb183ec849c038dbbcc5bc1f53d7a8fc69c92d6 Mon Sep 17 00:00:00 2001 From: forgejo-actions <+forgejo-actions@users.noreply.github.com> Date: Wed, 26 Feb 2025 00:11:00 +0000 Subject: [PATCH] Sync to upstream Jecs 0.5.4 --- CHANGELOG.md | 3 +- init.luau | 331 ++++++++++++++++++++++++++------------------------- pesde.toml | 2 +- 3 files changed, 172 insertions(+), 164 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3180c6..52f87cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog][kac], and this project adheres to - `[world]`: - 16% faster `world:get` + - `world:has` no longer typechecks components after the 8th one. - `[typescript]` - Fixed Entity type to default to `undefined | unknown` instead of just `undefined` @@ -165,7 +166,7 @@ The format is based on [Keep a Changelog][kac], and this project adheres to - Separates ranges for components and entity IDs. - - IDs created with `world:component()` will promote array lookups rather than map lookups in the `componentIndex` which is a significant boost + - IDs created with `world:component()` will promote array lookups rather than map lookups in the `component_index` which is a significant boost - No longer caches the column pointers directly and instead the column indices which stay persistent even when data is reallocated during swap-removals - This was an issue with the iterator being invalidated when you move an entity to a different archetype. diff --git a/init.luau b/init.luau index d80cd00..8395a5b 100644 --- a/init.luau +++ b/init.luau @@ -275,7 +275,7 @@ local function query_match(query, archetype: Archetype) end local function find_observers(world: World, event, component): { Observer }? - local cache = world.observerable[event] + local cache = world.observable[event] if not cache then return nil end @@ -471,7 +471,7 @@ local function world_target(world: World, entity: i53, relation: i24, index: num return nil end - local idr = world.componentIndex[ECS_PAIR(relation, EcsWildcard)] + local idr = world.component_index[ECS_PAIR(relation, EcsWildcard)] if not idr then return nil end @@ -502,8 +502,8 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean end local function id_record_ensure(world: World, id: number): IdRecord - local componentIndex = world.componentIndex - local idr = componentIndex[id] + local component_index = world.component_index + local idr: IdRecord = component_index[id] if not idr then local flags = ECS_ID_MASK @@ -548,8 +548,9 @@ local function id_record_ensure(world: World, id: number): IdRecord on_set = on_set, on_remove = on_remove, }, - } :: IdRecord - componentIndex[id] = idr + } + + component_index[id] = idr end return idr @@ -574,8 +575,8 @@ local function archetype_append_to_records( end local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype - local archetype_id = (world.nextArchetypeId :: number) + 1 - world.nextArchetypeId = archetype_id + 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 } @@ -631,7 +632,7 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) end end - world.archetypeIndex[ty] = archetype + world.archetype_index[ty] = archetype world.archetypes[archetype_id] = archetype return archetype @@ -651,7 +652,7 @@ local function archetype_ensure(world: World, id_types): Archetype end local ty = hash(id_types) - local archetype = world.archetypeIndex[ty] + local archetype = world.archetype_index[ty] if archetype then return archetype end @@ -813,7 +814,7 @@ local function world_add(world: World, entity: i53, id: i53): () end end - local idr = world.componentIndex[id] + local idr = world.component_index[id] local on_add = idr.hooks.on_add if on_add then @@ -830,15 +831,10 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () local from: Archetype = record.archetype local to: Archetype = archetype_traverse_add(world, id, from) - local idr = world.componentIndex[id] - local flags = idr.flags - local is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0 + local idr = world.component_index[id] local idr_hooks = idr.hooks if from == to then - if is_tag then - return - end -- If the archetypes are the same it can avoid moving the entity -- and just set the data directly. local tr = to.records[id] @@ -867,10 +863,6 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () on_add(entity) end - if is_tag then - return - end - local tr = to.records[id] local column = to.columns[tr.column] @@ -878,20 +870,20 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () local on_set = idr_hooks.on_set if on_set then - invoke_hook(on_set, entity, data) + on_set(entity, data) end end local function world_component(world: World): i53 - local componentId = (world.nextComponentId :: number) + 1 - if componentId > HI_COMPONENT_ID then + 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, -- 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 + world.max_component_id = id - return componentId + return id end local function world_remove(world: World, entity: i53, id: i53) @@ -907,8 +899,8 @@ local function world_remove(world: World, entity: i53, id: i53) end local to = archetype_traverse_remove(world, id, from) - if from and not (from == to) then - local idr = world.componentIndex[id] + if from ~= to then + local idr = world.component_index[id] local on_remove = idr.hooks.on_remove if on_remove then on_remove(entity) @@ -936,28 +928,26 @@ local function archetype_fast_delete(columns: { Column }, column_count: number, end local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?) - local entityIndex = world.entity_index + 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] - local delete = entities[row] - entities[row] = move - entities[last] = nil :: any + -- We assume first that the entity is the last in the archetype + local delete = move if row ~= last then - -- TODO: should be "entity_index_sparse_get(entityIndex, move)" - local record_to_move = entity_index_try_get_any(entityIndex, move) + local record_to_move = entity_index_try_get_any(entity_index, move) if record_to_move then record_to_move.row = row end + + entities[row] = move end - -- TODO: if last == 0 then deactivate table - - local component_index = world.componentIndex for _, id in id_types do local idr = component_index[id] local on_remove = idr.hooks.on_remove @@ -966,6 +956,8 @@ local function archetype_delete(world: World, archetype: Archetype, row: number, end end + entities[last] = nil :: any + if row == last then archetype_fast_delete_last(columns, column_count, id_types, delete) else @@ -1047,11 +1039,11 @@ local function archetype_destroy(world: World, archetype: Archetype) return end - local component_index = world.componentIndex + local component_index = world.component_index archetype_clear_edges(archetype) local archetype_id = archetype.id world.archetypes[archetype_id] = nil :: any - world.archetypeIndex[archetype.type] = nil :: any + world.archetype_index[archetype.type] = nil :: any local records = archetype.records for id in records do @@ -1095,7 +1087,7 @@ local function world_cleanup(world: World) end world.archetypes = new_archetypes - world.archetypeIndex = new_archetype_map + world.archetype_index = new_archetype_map end local world_delete: (world: World, entity: i53, destruct: boolean?) -> () @@ -1117,7 +1109,7 @@ do end local delete = entity - local component_index = world.componentIndex + local component_index = world.component_index local archetypes: Archetypes = world.archetypes local tgt = ECS_PAIR(EcsWildcard, delete) local idr_t = component_index[tgt] @@ -1134,6 +1126,8 @@ do for i = n, 1, -1 do world_delete(world, entities[i]) end + + archetype_destroy(world, idr_archetype) end else for archetype_id in idr.cache do @@ -1143,15 +1137,15 @@ do for i = n, 1, -1 do world_remove(world, entities[i], delete) end - end - for archetype_id in idr.cache do - local idr_archetype = archetypes[archetype_id] archetype_destroy(world, idr_archetype) end end end + local sparse_array = entity_index.sparse_array + local dense_array = entity_index.dense_array + if idr_t then for archetype_id in idr_t.cache do local children = {} @@ -1163,6 +1157,8 @@ do table.insert(children, child) end + local n = #children + for _, id in idr_t_types do if not ECS_IS_PAIR(id) then continue @@ -1173,23 +1169,21 @@ do local flags = id_record.flags local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE) if flags_delete_mask ~= 0 then - for _, child in children do - -- Cascade deletions of it has Delete as component trait - world_delete(world, child, destruct) + for i = n, 1, -1 do + world_delete(world, children[i]) end break else local on_remove = id_record.hooks.on_remove local to = archetype_traverse_remove(world, id, idr_t_archetype) - if on_remove then - for _, child in children do + local empty = #to.types == 0 + for i = n, 1, -1 do + local child = children[i] + if on_remove then on_remove(child) - local r = entity_index_try_get_fast(entity_index, child) :: Record - entity_move(entity_index, child, r, to) end - else - for _, child in children do - local r = entity_index_try_get_fast(entity_index, child) :: Record + local r = sparse_array[ECS_ENTITY_T_LO(child)] + if not empty then entity_move(entity_index, child, r, to) end end @@ -1201,7 +1195,6 @@ do end end - local dense_array = entity_index.dense_array local index_of_deleted_entity = record.dense local index_of_last_alive_entity = entity_index.alive_count entity_index.alive_count = index_of_last_alive_entity - 1 @@ -1607,14 +1600,14 @@ local function query_cached(query: QueryInner) local records: { ArchetypeRecord } local archetypes = query.compatible_archetypes - local world = query.world :: World + local world = query.world :: { observable: Observable } -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively -- because the event will be emitted for all components of that Archetype. - local observerable = world.observerable - local on_create_action = observerable[EcsOnArchetypeCreate] + local observable = world.observable :: Observable + local on_create_action = observable[EcsOnArchetypeCreate] if not on_create_action then on_create_action = {} - observerable[EcsOnArchetypeCreate] = on_create_action + observable[EcsOnArchetypeCreate] = on_create_action end local query_cache_on_create = on_create_action[A] if not query_cache_on_create then @@ -1622,10 +1615,10 @@ local function query_cached(query: QueryInner) on_create_action[A] = query_cache_on_create end - local on_delete_action = observerable[EcsOnArchetypeDelete] + local on_delete_action = observable[EcsOnArchetypeDelete] if not on_delete_action then on_delete_action = {} - observerable[EcsOnArchetypeDelete] = on_delete_action + observable[EcsOnArchetypeDelete] = on_delete_action end local query_cache_on_delete = on_delete_action[A] if not query_cache_on_delete then @@ -1919,7 +1912,7 @@ local function world_query(world: World, ...) local archetypes = world.archetypes local idr: IdRecord? - local componentIndex = world.componentIndex + local component_index = world.component_index local q = setmetatable({ ids = ids, @@ -1928,7 +1921,7 @@ local function world_query(world: World, ...) }, Query) for _, id in ids do - local map = componentIndex[id] + local map = component_index[id] if not map then return q end @@ -1971,7 +1964,7 @@ local function world_query(world: World, ...) end local function world_each(world: World, id): () -> () - local idr = world.componentIndex[id] + local idr = world.component_index[id] if not idr then return NOOP end @@ -2145,15 +2138,16 @@ function World.new() max_id = 0, } local self = setmetatable({ - archetypeIndex = {} :: { [string]: Archetype }, + archetype_index = {} :: { [string]: Archetype }, archetypes = {} :: Archetypes, - componentIndex = {} :: ComponentIndex, + component_index = {} :: ComponentIndex, entity_index = entity_index, - nextArchetypeId = 0 :: number, - nextComponentId = 0 :: number, - nextEntityId = 0 :: number, ROOT_ARCHETYPE = (nil :: any) :: Archetype, - observerable = {}, + + max_archetype_id = 0, + max_component_id = 0, + + observable = {} :: Observable, }, World) :: any self.ROOT_ARCHETYPE = archetype_create(self, {}, "") @@ -2193,7 +2187,9 @@ function World.new() return self end -export type Id = +export type Entity = {__T: T} + +export type Id = | Entity | Pair, Entity> | Pair> @@ -2204,6 +2200,103 @@ export type Pair = number & { __O: O, } +type Item = (self: Query) -> (Entity, T...) + +type Iter = (query: Query) -> () -> (Entity, T...) + + +export type Query = typeof(setmetatable({}, { + __iter = (nil :: any) :: Iter, +})) & { + iter: Iter, + with: (self: Query, ...Id) -> Query, + without: (self: Query, ...Id) -> Query, + archetypes: (self: Query) -> { Archetype }, + cached: (self: Query) -> Query, +} + +export type Observer = { + callback: (archetype: Archetype) -> (), + query: QueryInner, +} + +type Observable = { + [i53]: { + [i53]: { + { Observer } + } + } +} + +export type World = { + archetype_index: { [string]: Archetype }, + archetypes: Archetypes, + component_index: ComponentIndex, + entity_index: EntityIndex, + ROOT_ARCHETYPE: Archetype, + + max_component_id: number, + max_archetype_id: number, + + observable: any, + + --- Creates a new entity + entity: (self: World) -> 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, + --- Gets the target of an relationship. For example, when a user calls + --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. + target: (self: World, id: Entity, relation: Entity, index: number?) -> Entity?, + --- Deletes an entity and all it's related components and relationships. + delete: (self: World, id: Entity) -> (), + + --- Adds a component to the entity with no value + add: (self: World, id: Entity, component: Id) -> (), + --- Assigns a value to a component on the given entity + set: (self: World, id: Entity, component: Id, data: U) -> (), + + cleanup: (self: World) -> (), + -- Clears an entity from the world + clear: (self: World, id: Entity) -> (), + --- Removes a component from the given entity + remove: (self: World, id: Entity, component: Id) -> (), + --- Retrieves the value of up to 4 components. These values may be nil. + get: ((self: World, id: Entity, Id) -> A?) + & ((self: World, id: Entity, Id, Id) -> (A?, B?)) + & ((self: World, id: Entity, Id, Id, Id) -> (A?, B?, C?)) + & (self: World, id: Entity, Id, Id, Id, Id) -> (A?, B?, C?, D?), + + --- Returns whether the entity has the ID. + has: ((self: World, entity: Entity, ...Id) -> boolean) + & ((self: World, entity: Entity, Id, Id) -> boolean) + & ((self: World, entity: Entity, Id, Id, Id) -> boolean) + & ((self: World, entity: Entity, Id, Id, Id, Id) -> boolean) + & ((self: World, entity: Entity, Id, Id, Id, Id, Id) -> boolean) + & ((self: World, entity: Entity, Id, Id, Id, Id, Id, Id) -> boolean) + & ((self: World, entity: Entity, Id, Id, Id, Id, Id, Id, Id) -> boolean) + & ((self: World, entity: Entity, Id, Id, Id, Id, Id, Id, Id, ...unknown) -> boolean), + + --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. + parent: (self: World, entity: Entity) -> Entity, + + --- Checks if the world contains the given entity + contains: (self: World, entity: Entity) -> boolean, + + each: (self: World, id: Id) -> () -> Entity, + + children: (self: World, id: Id) -> () -> Entity, + + --- Searches the world for entities that match a given query + query: ((World, Id) -> Query) + & ((World, Id, Id) -> Query) + & ((World, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id, Id, Id, ...Id) -> Query) +} -- type function ecs_id_t(entity) -- local ty = entity:components()[2] -- local __T = ty:readproperty(types.singleton("__T")) @@ -2221,97 +2314,6 @@ export type Pair = number & { -- end -- end -type Item = (self: Query) -> (Entity, T...) - -export type Entity = number & { __T: T } - -type Iter = (query: Query) -> () -> (Entity, T...) - -export type Query = typeof(setmetatable({}, { - __iter = (nil :: any) :: Iter, -})) & { - iter: Iter, - with: (self: Query, ...Id) -> Query, - without: (self: Query, ...Id) -> Query, - archetypes: (self: Query) -> { Archetype }, - cached: (self: Query) -> Query, -} - -type Observer = { - callback: (archetype: Archetype) -> (), - query: QueryInner, -} - -export type World = { - archetypeIndex: { [string]: Archetype }, - archetypes: Archetypes, - componentIndex: ComponentIndex, - entity_index: EntityIndex, - ROOT_ARCHETYPE: Archetype, - - nextComponentId: number, - nextEntityId: number, - nextArchetypeId: number, - - observerable: { - [i53]: { - [i53]: { - { query: QueryInner, callback: (Archetype) -> () } - } - } - }, -} & { - --- Creates a new entity - entity: (self: World) -> 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, - --- Gets the target of an relationship. For example, when a user calls - --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. - target: (self: World, id: Entity, relation: Entity, index: number?) -> Entity?, - --- Deletes an entity and all it's related components and relationships. - delete: (self: World, id: Entity) -> (), - - --- Adds a component to the entity with no value - add: (self: World, id: Entity, component: Id) -> (), - --- Assigns a value to a component on the given entity - set: (self: World, id: Entity, component: Id, data: T) -> (), - - cleanup: (self: World) -> (), - -- Clears an entity from the world - clear: (self: World, id: Entity) -> (), - --- Removes a component from the given entity - remove: (self: World, id: Entity, component: Id) -> (), - --- Retrieves the value of up to 4 components. These values may be nil. - get: ((self: World, id: any, Id) -> A?) - & ((self: World, id: Entity, Id, Id) -> (A?, B?)) - & ((self: World, id: Entity, Id, Id, Id) -> (A?, B?, C?)) - & (self: World, id: Entity, Id, Id, Id, Id) -> (A?, B?, C?, D?), - - --- Returns whether the entity has the ID. - has: (self: World, entity: Entity, ...Id) -> boolean, - - --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. - parent: (self: World, entity: Entity) -> Entity, - - --- Checks if the world contains the given entity - contains: (self: World, entity: Entity) -> boolean, - - each: (self: World, id: Id) -> () -> Entity, - - children: (self: World, id: Id) -> () -> Entity, - - --- Searches the world for entities that match a given query - query: ((World, Id) -> Query) - & ((World, Id, Id) -> Query) - & ((World, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id, Id, Id, Id, ...Id) -> Query) -} - return { World = World :: { new: () -> World }, @@ -2358,6 +2360,8 @@ return { archetype_traverse_add = archetype_traverse_add, archetype_traverse_remove = archetype_traverse_remove, + entity_move = entity_move, + entity_index_try_get = entity_index_try_get, entity_index_try_get_any = entity_index_try_get_any, entity_index_try_get_fast = entity_index_try_get_fast, @@ -2369,4 +2373,7 @@ return { query_with = query_with, query_without = query_without, query_archetypes = query_archetypes, + query_match = query_match, + + find_observers = find_observers, } diff --git a/pesde.toml b/pesde.toml index 26b82af..94d42bd 100644 --- a/pesde.toml +++ b/pesde.toml @@ -11,7 +11,7 @@ includes = [ license = "MIT" name = "marked/jecs" repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.5.3" +version = "0.5.4" [indices] default = "https://github.com/pesde-pkg/index"