diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 3b5652f..e6de591 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -23,7 +23,7 @@ jobs: - name: Install Luau uses: https://github.com/EncodedVenom/install-luau@v4.3 with: - version: "0.668" + version: "latest" verbose: "true" - name: Install forgejo-cli diff --git a/jecs/.luaurc b/jecs/.luaurc index 5518c4c..d1ae244 100644 --- a/jecs/.luaurc +++ b/jecs/.luaurc @@ -4,7 +4,6 @@ "testkit": "tools/testkit", "mirror": "mirror", "tools": "tools", - "addons": "addons", }, "languageMode": "strict" } diff --git a/jecs/CHANGELOG.md b/jecs/CHANGELOG.md index 19a8ef1..52f87cc 100644 --- a/jecs/CHANGELOG.md +++ b/jecs/CHANGELOG.md @@ -11,46 +11,29 @@ The format is based on [Keep a Changelog][kac], and this project adheres to ## [Unreleased] - `[world]`: - - Changed `world:clear` to also look through the component record for the cleared `ID` - - Removes the cleared ID from every entity that has it - - Changed entity ID layouts by putting the index in the lower bits, which should make every world function 1-5 nanoseconds faster - - Fixed `world:delete` not removing every pair with an unalive target - - Specifically happened when you had at least two pairs of different relations with multiple targets each -- `[hooks]`: - - Replaced `OnSet` with `OnChange` - - The former was used to detect emplace/move actions. Now the behaviour for `OnChange` is that it will run only when the value has changed - - Changed `OnAdd` to specifically run after the data has been set for non-zero-sized components. Also returns the value that the component was set to - - This should allow a more lenient window for modifying data - - Changed `OnRemove` to lazily lookup which archetype the entity will move to - - Can now have interior structural changes within `OnRemove` hooks + - 16% faster `world:get` + - `world:has` no longer typechecks components after the 8th one. +- `[typescript]` -## [0.5.0] - 2024-12-26 + - Fixed Entity type to default to `undefined | unknown` instead of just `undefined` -- `[world]`: - - Fixed `world:target` not giving adjacent pairs - - Added `world:each` to find entities with a specific Tag - - Added `world:children` to find children of entity - `[query]`: - - Added `query:cached` - - Adds query cache that updates itself when an archetype matching the query gets created or deleted. -- `[luau]`: - - Changed how entities' types are inferred with user-defined type functions - - Changed `Pair` to return `Second` if `First` is a `Tag`; otherwise, returns `First`. - -## [0.4.0] - 2024-11-17 - -- `[world]`: - - Added recycling to `world:entity` - - If you see much larger entity ids, that is because its generation has been incremented -- `[query]`: - - Removed `query:drain` - - The default behaviour is simply to drain the iterator - - Removed `query:next` - - Just call the iterator function returned by `query:iter` directly if you want to get the next results - - Removed `query:replace` -- `[luau]`: - - Fixed `query:archetypes` not taking `self` - - Changed so that the `jecs.Pair` type now returns the first element's type so you won't need to typecast anymore. + - Fixed bug where `world:clear` did not invoke `jecs.OnRemove` hooks + - Changed `query.__iter` to drain on iteration + - It will initialize once wherever you left iteration off at last time + - Changed `query:iter` to restart the iterator + - Removed `query:drain` and `query:next` + - If you want to get individual results outside of a for-loop, you need to call `query:iter` to initialize the iterator and then call the iterator function manually + ```lua + local it = world:query(A, B, C):iter() + local entity, a, b, c = it() + entity, a, b, c = it() -- get next results + ``` +- `[world` + - Fixed a bug with `world:clear` not invoking `jecs.OnRemove` hooks +- `[typescript]`: + - Changed pair to accept generics + - Improved handling of Tags ## [0.3.2] - 2024-10-01 diff --git a/jecs/addons/observers.luau b/jecs/addons/observers.luau deleted file mode 100644 index a11f67c..0000000 --- a/jecs/addons/observers.luau +++ /dev/null @@ -1,157 +0,0 @@ -local jecs = require("@jecs") -local testkit = require("@testkit") - -local function observers_new(world, description) - local query = description.query - local callback = description.callback - local terms = query.filter_with - if not terms then - local ids = query.ids - query.filter_with = ids - terms = ids - end - - local entity_index = world.entity_index - local function emplaced(entity) - local r = jecs.entity_index_try_get_fast( - entity_index, entity) - - if not r then - return - end - - local archetype = r.archetype - - if jecs.query_match(query, archetype) then - callback(entity) - end - end - - for _, term in terms do - world:added(term, emplaced) - world:changed(term, emplaced) - end -end - -local function world_track(world, ...) - local entity_index = world.entity_index - local terms = { ... } - local q_shim = { filter_with = terms } - - local n = 0 - local dense_array = {} - local sparse_array = {} - - local function emplaced(entity) - local r = jecs.entity_index_try_get_fast( - entity_index, entity) - - if not r then - return - end - - local archetype = r.archetype - - if jecs.query_match(q_shim :: any, archetype) then - n += 1 - dense_array[n] = entity - sparse_array[entity] = n - end - end - - local function removed(entity) - local i = sparse_array[entity] - if i ~= n then - dense_array[i] = dense_array[n] - end - - dense_array[n] = nil - end - - for _, term in terms do - world:added(term, emplaced) - world:changed(term, emplaced) - end - - local function iter() - local i = n - return function() - local row = i - if row == 0 then - return nil - end - i -= 1 - return dense_array[row] :: any - end - end - - local it = { - __iter = iter, - without = function(self, ...) - q_shim.filter_without = { ... } - return self - end - } - return setmetatable(it, it) -end - -local function observers_add(world) - local signals = { - added = {}, - emplaced = {}, - removed = {} - } - world.added = function(_, component, fn) - local listeners = signals.added[component] - if not listeners then - listeners = {} - signals.added[component] = listeners - local idr = jecs.id_record_ensure(world, component) - idr.hooks.on_add = function(entity) - for _, listener in listeners do - listener(entity) - end - end - end - table.insert(listeners, fn) - end - - world.changed = function(_, component, fn) - local listeners = signals.emplaced[component] - if not listeners then - listeners = {} - signals.emplaced[component] = listeners - local idr = jecs.id_record_ensure(world, component) - idr.hooks.on_change = function(entity, value) - for _, listener in listeners do - listener(entity, value) - end - end - end - table.insert(listeners, fn) - end - - world.removed = function(_, component, fn) - local listeners = signals.removed[component] - if not listeners then - listeners = {} - signals.removed[component] = listeners - local idr = jecs.id_record_ensure(world, component) - idr.hooks.on_remove = function(entity) - for _, listener in listeners do - listener(entity) - end - end - end - table.insert(listeners, fn) - end - - world.signals = signals - - world.track = world_track - - world.observer = observers_new - return world -end - -return observers_add diff --git a/jecs/build.txt b/jecs/build.txt index f37d24a..b77aab5 100644 --- a/jecs/build.txt +++ b/jecs/build.txt @@ -1,2 +1,2 @@ -modified = ["addons/observers.luau", ".luaurc", "CHANGELOG.md", "jecs.luau"] -version = "0.5.5-nightly.20250412T181729Z" +modified = [".luaurc", "jecs.luau"] +version = "0.5.5-nightly.20250312T202956Z" diff --git a/jecs/jecs.luau b/jecs/jecs.luau index cc342ce..2e2833c 100644 --- a/jecs/jecs.luau +++ b/jecs/jecs.luau @@ -13,14 +13,20 @@ type Column = { any } type Map = { [K]: V } -type ecs_archetype_t = { +type GraphEdge = { + from: Archetype, + to: Archetype?, id: number, - types: Ty, - type: string, - entities: { number }, - columns: { Column }, - records: { [i53]: number }, - counts: { [i53]: number }, + prev: GraphEdge?, + next: GraphEdge?, +} + +type GraphEdges = Map + +type GraphNode = { + add: GraphEdges, + remove: GraphEdges, + refs: GraphEdge, } export type Archetype = { @@ -29,74 +35,49 @@ export type Archetype = { type: string, entities: { number }, columns: { Column }, - records: { [Id]: number }, - counts: { [Id]: number }, -} + records: { number }, + counts: { number }, +} & GraphNode -type ecs_record_t = { - archetype: ecs_archetype_t, +export type Record = { + archetype: Archetype, row: number, dense: i24, } -type ecs_id_record_t = { +type IdRecord = { cache: { number }, counts: { number }, flags: number, size: number, hooks: { - on_add: ((entity: i53, data: any?) -> ())?, - on_change: ((entity: i53, data: any) -> ())?, + on_add: ((entity: i53) -> ())?, + on_set: ((entity: i53, data: any) -> ())?, on_remove: ((entity: i53) -> ())?, }, } -type ecs_id_index_t = Map +type ComponentIndex = Map -type ecs_archetypes_map_t = { [string]: ecs_archetype_t } +type Archetypes = { [ArchetypeId]: Archetype } -type ecs_archetypes_t = { ecs_archetype_t } +type ArchetypeDiff = { + added: Ty, + removed: Ty, +} -type ecs_entity_index_t = { - dense_array: Map, - sparse_array: Map, +type EntityIndex = { + dense_array: Map, + sparse_array: Map, alive_count: number, max_id: number, } -type ecs_query_data_t = { - compatible_archetypes: { ecs_archetype_t }, - ids: { i53 }, - filter_with: { i53 }, - filter_without: { i53 }, - next: () -> (number, ...any), - world: ecs_world_t, -} - -type ecs_observer_t = { - callback: (archetype: ecs_archetype_t) -> (), - query: ecs_query_data_t, -} - -type ecs_observable_t = Map> - -type ecs_world_t = { - archetype_edges: Map>, - entity_index: ecs_entity_index_t, - component_index: ecs_id_index_t, - archetypes: ecs_archetypes_t, - archetype_index: ecs_archetypes_map_t, - max_archetype_id: number, - max_component_id: number, - ROOT_ARCHETYPE: ecs_archetype_t, - observable: Map>, -} - local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256 -- stylua: ignore start local EcsOnAdd = HI_COMPONENT_ID + 1 local EcsOnRemove = HI_COMPONENT_ID + 2 -local EcsOnChange = HI_COMPONENT_ID + 3 +local EcsOnSet = HI_COMPONENT_ID + 3 local EcsWildcard = HI_COMPONENT_ID + 4 local EcsChildOf = HI_COMPONENT_ID + 5 local EcsComponent = HI_COMPONENT_ID + 6 @@ -109,36 +90,60 @@ local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12 local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13 local EcsRest = HI_COMPONENT_ID + 14 -local ECS_ID_DELETE = 0b01 -local ECS_ID_IS_TAG = 0b10 -local ECS_ID_MASK = 0b00 - +local ECS_PAIR_FLAG = 0x8 +local ECS_ID_FLAGS_MASK = 0x10 local ECS_ENTITY_MASK = bit32.lshift(1, 24) local ECS_GENERATION_MASK = bit32.lshift(1, 16) -local NULL_ARRAY = table.freeze({}) -local ECS_INTERNAL_ERROR = [[ - This is an internal error, please file a bug report via the following link: +local ECS_ID_DELETE = 0b0000_0001 +local ECS_ID_IS_TAG = 0b0000_0010 +local ECS_ID_HAS_ON_ADD = 0b0000_0100 +local ECS_ID_HAS_ON_SET = 0b0000_1000 +local ECS_ID_HAS_ON_REMOVE = 0b0001_0000 +local ECS_ID_MASK = 0b0000_0000 +-- stylua: ignore end +local NULL_ARRAY = table.freeze({}) :: Column - https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md -]] +local function FLAGS_ADD(is_pair: boolean): number + local flags = 0x0 -local function ECS_COMBINE(id: number, generation: number): i53 - return id + (generation * ECS_ENTITY_MASK) + if is_pair then + flags = bit32.bor(flags, ECS_PAIR_FLAG) -- HIGHEST bit in the ID. + end + if false then + flags = bit32.bor(flags, 0x4) -- Set the second flag to true + end + if false then + flags = bit32.bor(flags, 0x2) -- Set the third flag to true + end + if false then + flags = bit32.bor(flags, 0x1) -- LAST BIT in the ID. + end + + return flags +end + +local function ECS_COMBINE(source: number, target: number): i53 + return (source * 268435456) + (target * ECS_ID_FLAGS_MASK) end -local ECS_PAIR_OFFSET = 2^48 local function ECS_IS_PAIR(e: number): boolean - return e > ECS_PAIR_OFFSET + return if e > ECS_ENTITY_MASK then (e % ECS_ID_FLAGS_MASK) // ECS_PAIR_FLAG ~= 0 else false end -local function ECS_GENERATION_INC(e: i53): i53 +-- HIGH 24 bits LOW 24 bits +local function ECS_GENERATION(e: i53): i24 + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0 +end + +local function ECS_GENERATION_INC(e: i53) if e > ECS_ENTITY_MASK then - local id = e % ECS_ENTITY_MASK - local generation = e // ECS_ENTITY_MASK + local flags = e // ECS_ID_FLAGS_MASK + local id = flags // ECS_ENTITY_MASK + local generation = flags % ECS_GENERATION_MASK local next_gen = generation + 1 - if next_gen >= ECS_GENERATION_MASK then + if next_gen > ECS_GENERATION_MASK then return id end @@ -147,37 +152,25 @@ local function ECS_GENERATION_INC(e: i53): i53 return ECS_COMBINE(e, 1) end -local function ECS_ENTITY_T_LO(e: i53): i24 - return e % ECS_ENTITY_MASK -end - -local function ECS_GENERATION(e: i53) - return e // ECS_ENTITY_MASK -end - +-- FIRST gets the high ID local function ECS_ENTITY_T_HI(e: i53): i24 - return e // ECS_ENTITY_MASK + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_ENTITY_MASK else e +end + +-- SECOND +local function ECS_ENTITY_T_LO(e: i53): i24 + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e +end + +local function _STRIP_GENERATION(e: i53): i24 + return ECS_ENTITY_T_LO(e) end local function ECS_PAIR(pred: i53, obj: i53): i53 - pred %= ECS_ENTITY_MASK - obj %= ECS_ENTITY_MASK - - return obj + (pred * ECS_ENTITY_MASK) + ECS_PAIR_OFFSET + return ECS_COMBINE(ECS_ENTITY_T_LO(pred), ECS_ENTITY_T_LO(obj)) + FLAGS_ADD(--[[isPair]] true) :: i53 end -local function ECS_PAIR_FIRST(e: i53): i24 - return (e - ECS_PAIR_OFFSET) // ECS_ENTITY_MASK -end - -local function ECS_PAIR_SECOND(e: i53): i24 - return (e - ECS_PAIR_OFFSET) % ECS_ENTITY_MASK -end - -local function entity_index_try_get_any( - entity_index: ecs_entity_index_t, - entity: number -): ecs_record_t? +local function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record? local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] if not r or r.dense == 0 then @@ -187,7 +180,7 @@ local function entity_index_try_get_any( return r end -local function entity_index_try_get(entity_index: ecs_entity_index_t, entity: number): ecs_record_t? +local function entity_index_try_get(entity_index: EntityIndex, entity: number): Record? local r = entity_index_try_get_any(entity_index, entity) if r then local r_dense = r.dense @@ -201,7 +194,7 @@ local function entity_index_try_get(entity_index: ecs_entity_index_t, entity: nu return r end -local function entity_index_try_get_fast(entity_index: ecs_entity_index_t, entity: number): ecs_record_t? +local function entity_index_try_get_fast(entity_index: EntityIndex, entity: number): Record? local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] if r then if entity_index.dense_array[r.dense] ~= entity then @@ -211,74 +204,49 @@ local function entity_index_try_get_fast(entity_index: ecs_entity_index_t, entit return r end -local function entity_index_is_alive(entity_index: ecs_entity_index_t, entity: i53) - return entity_index_try_get(entity_index, entity) ~= nil -end - -local function entity_index_get_alive(index: ecs_entity_index_t, entity: i53): i53? - local r = entity_index_try_get_any(index, entity) +local function entity_index_get_alive(index: EntityIndex, e: i24): i53 + local r = entity_index_try_get_any(index, e) if r then return index.dense_array[r.dense] end - return nil + return 0 end -local function ecs_get_alive(world, entity) - if entity == 0 then - return 0 - end - - local eindex = world.entity_index - - if entity_index_is_alive(eindex, entity) then - return entity - end - - if entity > ECS_ENTITY_MASK then - return 0 - end - - local current = entity_index_get_alive(eindex, entity) - if not current or not entity_index_is_alive(eindex, current) then - return 0 - end - - return current +local function entity_index_is_alive(entity_index: EntityIndex, entity: number) + return entity_index_try_get(entity_index, entity) ~= nil end -local function entity_index_new_id(entity_index: ecs_entity_index_t): i53 +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 max_id = entity_index.max_id - if alive_count ~= max_id then + if alive_count ~= #dense_array then alive_count += 1 entity_index.alive_count = alive_count local id = dense_array[alive_count] return id end - local id = max_id + 1 + local id = entity_index.max_id + 1 entity_index.max_id = id alive_count += 1 entity_index.alive_count = alive_count dense_array[alive_count] = id - entity_index.sparse_array[id] = { dense = alive_count } :: ecs_record_t + entity_index.sparse_array[id] = { dense = alive_count } :: Record return id end -local function ecs_pair_first(world: ecs_world_t, e: i53) - local pred = ECS_PAIR_FIRST(e) - return ecs_get_alive(world, pred) +-- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits +local function ecs_pair_first(world, e) + return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e)) end -local function ecs_pair_second(world: ecs_world_t, e: i53) - local obj = ECS_PAIR_SECOND(e) - return ecs_get_alive(world, obj) +-- ECS_PAIR_SECOND gets the relationship / pred / LOW bits +local function ecs_pair_second(world, e) + return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_HI(e)) end -local function query_match(query: ecs_query_data_t, - archetype: ecs_archetype_t) +local function query_match(query, archetype: Archetype) local records = archetype.records local with = query.filter_with @@ -300,8 +268,7 @@ local function query_match(query: ecs_query_data_t, return true end -local function find_observers(world: ecs_world_t, event: i53, - component: i53): { ecs_observer_t }? +local function find_observers(world: World, event, component): { Observer }? local cache = world.observable[event] if not cache then return nil @@ -309,13 +276,7 @@ local function find_observers(world: ecs_world_t, event: i53, return cache[component] :: any end -local function archetype_move( - entity_index: ecs_entity_index_t, - to: ecs_archetype_t, - dst_row: i24, - from: ecs_archetype_t, - src_row: i24 -) +local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24) local src_columns = from.columns local dst_columns = to.columns local dst_entities = to.entities @@ -369,33 +330,21 @@ local function archetype_move( record2.row = src_row end -local function archetype_append( - entity: i53, - archetype: ecs_archetype_t -): number +local function archetype_append(entity: number, archetype: Archetype): number local entities = archetype.entities local length = #entities + 1 entities[length] = entity return length end -local function new_entity( - entity: i53, - record: ecs_record_t, - archetype: ecs_archetype_t -): ecs_record_t +local function new_entity(entity: i53, record: Record, archetype: Archetype): Record local row = archetype_append(entity, archetype) record.archetype = archetype record.row = row return record end -local function entity_move( - entity_index: ecs_entity_index_t, - entity: i53, - record: ecs_record_t, - to: ecs_archetype_t -) +local function entity_move(entity_index: EntityIndex, entity: i53, record: Record, to: Archetype) local sourceRow = record.row local from = record.archetype local dst_row = archetype_append(entity, to) @@ -408,8 +357,7 @@ local function hash(arr: { number }): string return table.concat(arr, "_") end -local function fetch(id: i53, records: { number }, - columns: { Column }, row: number): any +local function fetch(id, records: { number }, columns: { Column }, row: number): any local tr = records[id] if not tr then @@ -419,8 +367,7 @@ local function fetch(id: i53, records: { number }, return columns[tr][row] end -local function world_get(world: ecs_world_t, entity: i53, - a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...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_fast(world.entity_index, entity) if not record then return nil @@ -450,7 +397,25 @@ local function world_get(world: ecs_world_t, entity: i53, end end -local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): boolean +local function world_get_one_inline(world: World, entity: i53, id: i53): any + local record = entity_index_try_get_fast(world.entity_index, entity) + if not record then + return nil + end + + local archetype = record.archetype + if not archetype then + return nil + end + + local tr = archetype.records[id] + if not tr then + return nil + end + return archetype.columns[tr][record.row] +end + +local function world_has_one_inline(world: World, entity: number, id: i53): boolean local record = entity_index_try_get_fast(world.entity_index, entity) if not record then return false @@ -466,7 +431,7 @@ local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): b return records[id] ~= nil end -local function world_has(world: ecs_world_t, entity: i53, ...: i53): boolean +local function world_has(world: World, entity: number, ...: i53): boolean local record = entity_index_try_get_fast(world.entity_index, entity) if not record then return false @@ -488,7 +453,7 @@ local function world_has(world: ecs_world_t, entity: i53, ...: i53): boolean return true end -local function world_target(world: ecs_world_t, entity: i53, relation: i24, index: number?): i24? +local function world_target(world: World, entity: i53, relation: i24, index: number?): i24? local nth = index or 0 local record = entity_index_try_get_fast(world.entity_index, entity) if not record then @@ -500,9 +465,13 @@ local function world_target(world: ecs_world_t, entity: i53, relation: i24, inde return nil end - local r = ECS_PAIR(relation, EcsWildcard) + local idr = world.component_index[ECS_PAIR(relation, EcsWildcard)] + if not idr then + return nil + end - local count = archetype.counts[r] + local archetype_id = archetype.id + local count = idr.counts[archetype.id] if not count then return nil end @@ -511,13 +480,15 @@ local function world_target(world: ecs_world_t, entity: i53, relation: i24, inde nth = nth + count + 1 end - nth = archetype.types[nth + archetype.records[r]] + local tr = idr.cache[archetype_id] + + nth = archetype.types[nth + tr] + if not nth then return nil end - return entity_index_get_alive(world.entity_index, - ECS_PAIR_SECOND(nth)) + return ecs_pair_second(world, nth) end local function ECS_ID_IS_WILDCARD(e: i53): boolean @@ -526,23 +497,16 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean return first == EcsWildcard or second == EcsWildcard end -local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t +local function id_record_ensure(world: World, id: number): IdRecord local component_index = world.component_index - local entity_index = world.entity_index - local idr: ecs_id_record_t = component_index[id] + local idr: IdRecord = component_index[id] if not idr then local flags = ECS_ID_MASK local relation = id - local target = 0 local is_pair = ECS_IS_PAIR(id) if is_pair then - relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53 - assert(relation and entity_index_is_alive( - entity_index, relation), ECS_INTERNAL_ERROR) - target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53 - assert(target and entity_index_is_alive( - entity_index, target), ECS_INTERNAL_ERROR) + relation = ecs_pair_first(world, id) end local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) @@ -554,18 +518,19 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t has_delete = true end - local on_add, on_change, on_remove = world_get(world, - relation, EcsOnAdd, EcsOnChange, EcsOnRemove) + local on_add, on_set, on_remove = world_get(world, relation, EcsOnAdd, EcsOnSet, EcsOnRemove) - local is_tag = not world_has_one_inline(world, - relation, EcsComponent) + local is_tag = not world_has_one_inline(world, relation, EcsComponent) if is_tag and is_pair then - is_tag = not world_has_one_inline(world, target, EcsComponent) + is_tag = not world_has_one_inline(world, ecs_pair_second(world, id), EcsComponent) end flags = bit32.bor( flags, + if on_add then ECS_ID_HAS_ON_ADD else 0, + if on_remove then ECS_ID_HAS_ON_REMOVE else 0, + if on_set then ECS_ID_HAS_ON_SET else 0, if has_delete then ECS_ID_DELETE else 0, if is_tag then ECS_ID_IS_TAG else 0 ) @@ -577,7 +542,7 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t flags = flags, hooks = { on_add = on_add, - on_change = on_change, + on_set = on_set, on_remove = on_remove, }, } @@ -589,9 +554,9 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t end local function archetype_append_to_records( - idr: ecs_id_record_t, - archetype: ecs_archetype_t, - id: i53, + idr: IdRecord, + archetype: Archetype, + id: number, index: number ) local archetype_id = archetype.id @@ -613,7 +578,7 @@ local function archetype_append_to_records( end end -local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: i53?): ecs_archetype_t +local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype local archetype_id = (world.max_archetype_id :: number) + 1 world.max_archetype_id = archetype_id @@ -623,7 +588,7 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: local records: { number } = {} local counts: {number} = {} - local archetype: ecs_archetype_t = { + local archetype: Archetype = { columns = columns, entities = {}, id = archetype_id, @@ -631,15 +596,20 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: counts = counts, type = ty, types = id_types, + + add = {}, + remove = {}, + refs = {} :: GraphEdge, } - for i, component_id in id_types do - local idr = id_record_ensure(world, component_id) - archetype_append_to_records(idr, archetype, component_id, i) + for i, componentId in id_types do + local idr = id_record_ensure(world, componentId) + archetype_append_to_records(idr, archetype, componentId, i) + + if ECS_IS_PAIR(componentId) then + local relation = ecs_pair_first(world, componentId) + local object = ecs_pair_second(world, componentId) - 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) archetype_append_to_records(idr_r, archetype, r, i) @@ -670,20 +640,19 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: world.archetype_index[ty] = archetype world.archetypes[archetype_id] = archetype - world.archetype_edges[archetype.id] = {} return archetype end -local function world_entity(world: ecs_world_t): i53 +local function world_entity(world: World): i53 return entity_index_new_id(world.entity_index) end -local function world_parent(world: ecs_world_t, entity: i53) +local function world_parent(world: World, entity: i53) return world_target(world, entity, EcsChildOf, 0) end -local function archetype_ensure(world: ecs_world_t, id_types): ecs_archetype_t +local function archetype_ensure(world: World, id_types): Archetype if #id_types < 1 then return world.ROOT_ARCHETYPE end @@ -700,7 +669,6 @@ end local function find_insert(id_types: { i53 }, toAdd: i53): number for i, id in id_types do if id == toAdd then - error("Duplicate component id") return -1 end if id > toAdd then @@ -710,13 +678,30 @@ local function find_insert(id_types: { i53 }, toAdd: i53): number return #id_types + 1 end -local function find_archetype_without( - world: ecs_world_t, - node: ecs_archetype_t, - id: i53 -): ecs_archetype_t +local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype + local id_types = node.types + -- Component IDs are added incrementally, so inserting and sorting + -- them each time would be expensive. Instead this insertion sort can find the insertion + -- point in the types array. + + local dst = table.clone(node.types) :: { i53 } + local at = find_insert(id_types, id) + if at == -1 then + -- If it finds a duplicate, it just means it is the same archetype so it can return it + -- directly instead of needing to hash types for a lookup to the archetype. + return node + end + table.insert(dst, at, id) + + return archetype_ensure(world, dst) +end + +local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype local id_types = node.types local at = table.find(id_types, id) + if at == nil then + return node + end local dst = table.clone(id_types) table.remove(dst, at) @@ -724,72 +709,94 @@ local function find_archetype_without( return archetype_ensure(world, dst) end +local function archetype_init_edge(archetype: Archetype, edge: GraphEdge, id: i53, to: Archetype) + edge.from = archetype + edge.to = to + edge.id = id +end -local function create_edge_for_remove( - world: ecs_world_t, - node: ecs_archetype_t, - edge: Map, - id: i53 -): ecs_archetype_t +local function archetype_ensure_edge(world, edges: GraphEdges, id): GraphEdge + local edge = edges[id] + if not edge then + edge = {} :: GraphEdge + edges[id] = edge + end + + return edge +end + +local function init_edge_for_add(world, archetype: Archetype, edge: GraphEdge, id, to: Archetype) + archetype_init_edge(archetype, edge, id, to) + archetype_ensure_edge(world, archetype.add, id) + if archetype ~= to then + local to_refs = to.refs + local next_edge = to_refs.next + + to_refs.next = edge + edge.prev = to_refs + edge.next = next_edge + + if next_edge then + next_edge.prev = edge + end + end +end + +local function init_edge_for_remove(world: World, archetype: Archetype, edge: GraphEdge, id: number, to: Archetype) + archetype_init_edge(archetype, edge, id, to) + archetype_ensure_edge(world, archetype.remove, id) + if archetype ~= to then + local to_refs = to.refs + local prev_edge = to_refs.prev + + to_refs.prev = edge + edge.next = to_refs + edge.prev = prev_edge + + if prev_edge then + prev_edge.next = edge + end + end +end + +local function create_edge_for_add(world: World, node: Archetype, edge: GraphEdge, id: i53): Archetype + local to = find_archetype_with(world, node, id) + init_edge_for_add(world, node, edge, id, to) + return to +end + +local function create_edge_for_remove(world: World, node: Archetype, edge: GraphEdge, id: i53): Archetype local to = find_archetype_without(world, node, id) - local edges = world.archetype_edges - local archetype_id = node.id - edges[archetype_id][id] = to - edges[to.id][id] = node + init_edge_for_remove(world, node, edge, id, to) return to end -local function archetype_traverse_remove( - world: ecs_world_t, - id: i53, - from: ecs_archetype_t -): ecs_archetype_t - local edges = world.archetype_edges - local edge = edges[from.id] - - local to = edge[id] - if not to then - to = find_archetype_without(world, from, id) - edge[id] = to - edges[to.id][id] = from - end - - return to :: ecs_archetype_t -end - -local function find_archetype_with(world, id, from) - local id_types = from.types - - local at = find_insert(id_types, id) - local dst = table.clone(id_types) :: { i53 } - table.insert(dst, at, id) - - return archetype_ensure(world, dst) -end - -local function archetype_traverse_add(world, id, from: ecs_archetype_t) +local function archetype_traverse_add(world: World, id: i53, from: Archetype): Archetype from = from or world.ROOT_ARCHETYPE - if from.records[id] then - return from - end - local edges = world.archetype_edges - local edge = edges[from.id] + local edge = archetype_ensure_edge(world, from.add, id) - local to = edge[id] + local to = edge.to if not to then - to = find_archetype_with(world, id, from) - edge[id] = to - edges[to.id][id] = from + to = create_edge_for_add(world, from, edge, id) end - return to + return to :: Archetype end -local function world_add( - world: ecs_world_t, - entity: i53, - id: i53 -): () +local function archetype_traverse_remove(world: World, id: i53, from: Archetype): Archetype + from = from or world.ROOT_ARCHETYPE + + local edge = archetype_ensure_edge(world, from.remove, id) + + local to = edge.to + if not to then + to = create_edge_for_remove(world, from, edge, id) + end + + return to :: Archetype +end + +local function world_add(world: World, entity: i53, id: i53): () local entity_index = world.entity_index local record = entity_index_try_get_fast(entity_index, entity) if not record then @@ -817,28 +824,27 @@ local function world_add( end end -local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown): () +local function world_set(world: World, entity: i53, id: i53, data: unknown): () local entity_index = world.entity_index local record = entity_index_try_get_fast(entity_index, entity) if not record then return end - local from: ecs_archetype_t = record.archetype - local to: ecs_archetype_t = archetype_traverse_add(world, id, from) + local from: Archetype = record.archetype + local to: Archetype = archetype_traverse_add(world, id, from) local idr = world.component_index[id] local idr_hooks = idr.hooks if from == to then + -- If the archetypes are the same it can avoid moving the entity + -- and just set the data directly. local tr = to.records[id] local column = from.columns[tr] 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_hooks.on_change - if on_change then - on_change(entity, data) + local on_set = idr_hooks.on_set + if on_set then + on_set(entity, data) end return @@ -854,14 +860,19 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown end end + local on_add = idr_hooks.on_add + if on_add then + on_add(entity) + end + local tr = to.records[id] local column = to.columns[tr] column[record.row] = data - local on_add = idr_hooks.on_add - if on_add then - on_add(entity, data) + local on_set = idr_hooks.on_set + if on_set then + on_set(entity, data) end end @@ -877,7 +888,7 @@ local function world_component(world: World): i53 return id end -local function world_remove(world: ecs_world_t, entity: i53, id: i53) +local function world_remove(world: World, entity: i53, id: i53) local entity_index = world.entity_index local record = entity_index_try_get_fast(entity_index, entity) if not record then @@ -888,16 +899,15 @@ local function world_remove(world: ecs_world_t, entity: i53, id: i53) if not from then return end + local to = archetype_traverse_remove(world, id, from) - if from.records[id] then + if from ~= to then local idr = world.component_index[id] local on_remove = idr.hooks.on_remove if on_remove then on_remove(entity) end - local to = archetype_traverse_remove(world, id, record.archetype) - entity_move(entity_index, entity, record, to) end end @@ -919,7 +929,7 @@ local function archetype_fast_delete(columns: { Column }, column_count: number, end end -local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, row: number) +local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?) local entity_index = world.entity_index local component_index = world.component_index local columns = archetype.columns @@ -958,118 +968,82 @@ local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, end end -local function world_clear(world: ecs_world_t, entity: i53) - local entity_index = world.entity_index - local component_index = world.component_index - local archetypes = world.archetypes - local tgt = ECS_PAIR(EcsWildcard, entity) - local idr_t = component_index[tgt] - local idr = component_index[entity] - local rel = ECS_PAIR(entity, EcsWildcard) - local idr_r = component_index[rel] - - if idr then - local count = 0 - local queue = {} - for archetype_id in idr.cache do - local idr_archetype = archetypes[archetype_id] - local entities = idr_archetype.entities - local n = #entities - count += n - table.move(entities, 1, n, #queue + 1, queue) - end - for _, e in queue do - world_remove(world, e, entity) - end +local function world_clear(world: World, entity: i53) + --TODO: use sparse_get (stashed) + local record = entity_index_try_get(world.entity_index, entity) + if not record then + return end - if idr_t then - local queue - local ids + local archetype = record.archetype + local row = record.row - local count = 0 - local archetype_ids = idr_t.cache - for archetype_id in archetype_ids do - local idr_t_archetype = archetypes[archetype_id] - local idr_t_types = idr_t_archetype.types - local entities = idr_t_archetype.entities - local removal_queued = false - - for _, id in idr_t_types do - if not ECS_IS_PAIR(id) then - continue - end - local object = entity_index_get_alive( - entity_index, ECS_PAIR_SECOND(id)) - if object ~= entity then - continue - end - if not ids then - ids = {} - end - ids[id] = true - removal_queued = true - end - - if not removal_queued then - continue - end - - if not queue then - queue = {} - end - - local n = #entities - table.move(entities, 1, n, count + 1, queue) - count += n - end - - for id in ids do - for _, child in queue do - world_remove(world, child, id) - end - end + if archetype then + -- In the future should have a destruct mode for + -- deleting archetypes themselves. Maybe requires recycling + archetype_delete(world, archetype, row) end - if idr_r then - local count = 0 - local archetype_ids = idr_r.cache - local ids = {} - local queue = {} - for archetype_id in archetype_ids do - local idr_r_archetype = archetypes[archetype_id] - local entities = idr_r_archetype.entities - local tr = idr_r_archetype.records[rel] - local tr_count = idr_r_archetype.counts[rel] - local types = idr_r_archetype.types - for i = tr, tr + tr_count - 1 do - ids[types[i]] = true - end - local n = #entities - table.move(entities, 1, n, count + 1, queue) - count += n - end + record.archetype = nil :: any + record.row = nil :: any +end - for _, e in queue do - for id in ids do - world_remove(world, e, id) - end - end +local function archetype_disconnect_edge(edge: GraphEdge) + local edge_next = edge.next + local edge_prev = edge.prev + if edge_next then + edge_next.prev = edge_prev + end + if edge_prev then + edge_prev.next = edge_next end end -local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t) +local function archetype_remove_edge(edges: Map, id: i53, edge: GraphEdge) + archetype_disconnect_edge(edge) + edges[id] = nil :: any +end + +local function archetype_clear_edges(archetype: Archetype) + local add: GraphEdges = archetype.add + local remove: GraphEdges = archetype.remove + local node_refs = archetype.refs + for id, edge in add do + archetype_disconnect_edge(edge) + add[id] = nil :: any + end + for id, edge in remove do + archetype_disconnect_edge(edge) + remove[id] = nil :: any + end + + local cur = node_refs.next + while cur do + local edge = cur :: GraphEdge + local next_edge = edge.next + archetype_remove_edge(edge.from.add, edge.id, edge) + cur = next_edge + end + + cur = node_refs.prev + while cur do + local edge: GraphEdge = cur + local next_edge = edge.prev + archetype_remove_edge(edge.from.remove, edge.id, edge) + cur = next_edge + end + + node_refs.next = nil + node_refs.prev = nil +end + +local function archetype_destroy(world: World, archetype: Archetype) if archetype == world.ROOT_ARCHETYPE then return end local component_index = world.component_index - local archetype_edges = world.archetype_edges - - for id, edge in archetype_edges[archetype.id] do - archetype_edges[edge.id][id] = nil - end - + archetype_clear_edges(archetype) local archetype_id = archetype.id world.archetypes[archetype_id] = nil :: any world.archetype_index[archetype.type] = nil :: any @@ -1099,7 +1073,7 @@ local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t) end end -local function world_cleanup(world: ecs_world_t) +local function world_cleanup(world: World) local archetypes = world.archetypes for _, archetype in archetypes do @@ -1108,7 +1082,7 @@ local function world_cleanup(world: ecs_world_t) end end - local new_archetypes = table.create(#archetypes) :: { ecs_archetype_t } + local new_archetypes = table.create(#archetypes) :: { Archetype } local new_archetype_map = {} for index, archetype in archetypes do @@ -1120,157 +1094,114 @@ local function world_cleanup(world: ecs_world_t) world.archetype_index = new_archetype_map end -local function world_delete(world: ecs_world_t, entity: i53) - local entity_index = world.entity_index - local record = entity_index_try_get(entity_index, 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) - end - - local delete = entity - local component_index = world.component_index - local archetypes = world.archetypes - local tgt = ECS_PAIR(EcsWildcard, delete) - local rel = ECS_PAIR(delete, EcsWildcard) - - local idr_t = component_index[tgt] - local idr = component_index[delete] - local idr_r = component_index[rel] - - if idr then - local flags = idr.flags - if bit32.band(flags, ECS_ID_DELETE) ~= 0 then - for archetype_id in idr.cache do - local idr_archetype = archetypes[archetype_id] - - local entities = idr_archetype.entities - local n = #entities - 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 - local idr_archetype = archetypes[archetype_id] - local entities = idr_archetype.entities - local n = #entities - for i = n, 1, -1 do - world_remove(world, entities[i], delete) - end - - archetype_destroy(world, idr_archetype) - end +local world_delete: (world: World, entity: i53, destruct: boolean?) -> () +do + function world_delete(world: World, entity: i53, destruct: boolean?) + local entity_index = world.entity_index + local record = entity_index_try_get(entity_index, entity) + if not record then + return end - end - if idr_t then - local children - local ids + local archetype = record.archetype + local row = record.row - local count = 0 - local archetype_ids = idr_t.cache - for archetype_id in archetype_ids do - local idr_t_archetype = archetypes[archetype_id] - local idr_t_types = idr_t_archetype.types - local entities = idr_t_archetype.entities - local removal_queued = false + if archetype then + -- In the future should have a destruct mode for + -- deleting archetypes themselves. Maybe requires recycling + archetype_delete(world, archetype, row, destruct) + end - for _, id in idr_t_types do - if not ECS_IS_PAIR(id) then - continue - end - local object = entity_index_get_alive( - entity_index, ECS_PAIR_SECOND(id)) - if object ~= delete then - continue - end - local id_record = component_index[id] - local flags = id_record.flags - local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE) - if flags_delete_mask ~= 0 then - for i = #entities, 1, -1 do - local child = entities[i] - world_delete(world, child) + local delete = entity + local component_index = world.component_index + local archetypes: Archetypes = world.archetypes + local tgt = ECS_PAIR(EcsWildcard, delete) + local idr_t = component_index[tgt] + local idr = component_index[delete] + + if idr then + local flags = idr.flags + if bit32.band(flags, ECS_ID_DELETE) ~= 0 then + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + world_delete(world, entities[i]) end - break - else - if not ids then - ids = {} + + archetype_destroy(world, idr_archetype) + end + else + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + world_remove(world, entities[i], delete) end - ids[id] = true - removal_queued = true - end - end - if not removal_queued then - continue - end - if not children then - children = {} - end - local n = #entities - table.move(entities, 1, n, count + 1, children) - count += n - end - - if ids then - for _, child in children do - for id in ids do - world_remove(world, child, id) + archetype_destroy(world, idr_archetype) end end end - for archetype_id in archetype_ids do - archetype_destroy(world, archetypes[archetype_id]) - end - end + local dense_array = entity_index.dense_array - if idr_r then - local archetype_ids = idr_r.cache - local flags = idr_r.flags - if bit32.band(flags, ECS_ID_DELETE) ~= 0 then - for archetype_id in archetype_ids do - local idr_r_archetype = archetypes[archetype_id] - local entities = idr_r_archetype.entities - local n = #entities - for i = n, 1, -1 do - world_delete(world, entities[i]) - end - archetype_destroy(world, idr_r_archetype) - end - else - local children = {} + if idr_t then + local children + local ids local count = 0 - local ids = {} + local archetype_ids = idr_t.cache for archetype_id in archetype_ids do - local idr_r_archetype = archetypes[archetype_id] - local entities = idr_r_archetype.entities - local tr = idr_r_archetype.records[rel] - local tr_count = idr_r_archetype.counts[rel] - local types = idr_r_archetype.types - for i = tr, tr_count - 1 do - ids[types[tr]] = true + local idr_t_archetype = archetypes[archetype_id] + local idr_t_types = idr_t_archetype.types + local entities = idr_t_archetype.entities + local removal_queued = false + + for _, id in idr_t_types do + if not ECS_IS_PAIR(id) then + continue + end + local object = ecs_pair_second(world, id) + if object ~= delete then + continue + end + local id_record = component_index[id] + local flags = id_record.flags + local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE) + if flags_delete_mask ~= 0 then + for i = #entities, 1, -1 do + local child = entities[i] + world_delete(world, child) + end + break + else + if not ids then + ids = {} + end + ids[id] = true + removal_queued = true + end + end + + if not removal_queued then + continue + end + if not children then + children = {} end local n = #entities table.move(entities, 1, n, count + 1, children) count += n end - for _, child in children do + if ids then for id in ids do - world_remove(world, child, id) + for _, child in children do + world_remove(world, child, id) + end end end @@ -1278,31 +1209,30 @@ local function world_delete(world: ecs_world_t, entity: i53) archetype_destroy(world, archetypes[archetype_id]) end end + + 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 + + local last_alive_entity = dense_array[index_of_last_alive_entity] + local r_swap = entity_index_try_get_any(entity_index, last_alive_entity) :: Record + r_swap.dense = index_of_deleted_entity + record.archetype = nil :: any + record.row = nil :: any + record.dense = index_of_last_alive_entity + + dense_array[index_of_deleted_entity] = last_alive_entity + dense_array[index_of_last_alive_entity] = ECS_GENERATION_INC(entity) 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 - - local last_alive_entity = dense_array[index_of_last_alive_entity] - local r_swap = entity_index_try_get_any(entity_index, last_alive_entity) :: ecs_record_t - r_swap.dense = index_of_deleted_entity - record.archetype = nil :: any - record.row = nil :: any - record.dense = index_of_last_alive_entity - - dense_array[index_of_deleted_entity] = last_alive_entity - dense_array[index_of_last_alive_entity] = ECS_GENERATION_INC(entity) end -local function world_contains(world: ecs_world_t, entity): boolean +local function world_contains(world: World, entity): boolean return entity_index_is_alive(world.entity_index, entity) end local function NOOP() end -export type QueryInner = { +type QueryInner = { compatible_archetypes: { Archetype }, ids: { i53 }, filter_with: { i53 }, @@ -1311,7 +1241,7 @@ export type QueryInner = { world: World, } -local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) +local function query_iter_init(query: QueryInner): () -> (number, ...any) local world_query_iter_next local compatible_archetypes = query.compatible_archetypes @@ -1656,7 +1586,7 @@ local function query_iter(query): () -> (number, ...any) return query_next end -local function query_without(query: ecs_query_data_t, ...: i53) +local function query_without(query: QueryInner, ...: i53) local without = { ... } query.filter_without = without local compatible_archetypes = query.compatible_archetypes @@ -1686,7 +1616,7 @@ local function query_without(query: ecs_query_data_t, ...: i53) return query :: any end -local function query_with(query: ecs_query_data_t, ...: i53) +local function query_with(query: QueryInner, ...: i53) local compatible_archetypes = query.compatible_archetypes local with = { ... } query.filter_with = with @@ -1724,7 +1654,7 @@ local function query_archetypes(query) return query.compatible_archetypes end -local function query_cached(query: ecs_query_data_t) +local function query_cached(query: QueryInner) local with = query.filter_with local ids = query.ids if with then @@ -1744,14 +1674,14 @@ local function query_cached(query: ecs_query_data_t) local columns: { Column } local entities: { number } local i: number - local archetype: ecs_archetype_t + local archetype: Archetype local records: { number } local archetypes = query.compatible_archetypes - local world = query.world :: { observable: ecs_observable_t } + 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 observable = world.observable :: ecs_observable_t + local observable = world.observable :: Observable local on_create_action = observable[EcsOnArchetypeCreate] if not on_create_action then on_create_action = {} @@ -1779,7 +1709,7 @@ local function query_cached(query: ecs_query_data_t) end local function on_delete_callback(archetype) - local i = table.find(archetypes, archetype) :: number + local i = table.find(archetypes, archetype) :: number local n = #archetypes archetypes[i] = archetypes[n] archetypes[n] = nil @@ -2145,7 +2075,7 @@ Query.with = query_with Query.archetypes = query_archetypes Query.cached = query_cached -local function world_query(world: ecs_world_t, ...) +local function world_query(world: World, ...) local compatible_archetypes = {} local length = 0 @@ -2153,7 +2083,7 @@ local function world_query(world: ecs_world_t, ...) local archetypes = world.archetypes - local idr: ecs_id_record_t? + local idr: IdRecord? local component_index = world.component_index local q = setmetatable({ @@ -2205,7 +2135,7 @@ local function world_query(world: ecs_world_t, ...) return q end -local function world_each(world: ecs_world_t, id: i53): () -> () +local function world_each(world: World, id): () -> () local idr = world.component_index[id] if not idr then return NOOP @@ -2239,36 +2169,10 @@ local function world_each(world: ecs_world_t, id: i53): () -> () end end -local function world_children(world: ecs_world_t, parent: i53) +local function world_children(world, parent) return world_each(world, ECS_PAIR(EcsChildOf, parent)) end -export type Record = { - archetype: Archetype, - row: number, - dense: i24, -} -export type ComponentRecord = { - cache: { [Id]: number }, - counts: { [Id]: number }, - flags: number, - size: number, - hooks: { - on_add: ((entity: Entity) -> ())?, - on_set: ((entity: Entity, data: any) -> ())?, - on_remove: ((entity: Entity) -> ())?, - }, -} -export type ComponentIndex = Map -export type Archetypes = { [Id]: Archetype } - -export type EntityIndex = { - dense_array: Map, - sparse_array: Map, - alive_count: number, - max_id: number, -} - local World = {} World.__index = World @@ -2289,16 +2193,123 @@ World.cleanup = world_cleanup World.each = world_each World.children = world_children -local function world_new() - local entity_index = { - dense_array = {}, - sparse_array = {}, +if _G.__JECS_DEBUG then + local function dbg_info(n: number): any + return debug.info(n, "s") + end + local function throw(msg: string) + local s = 1 + local root = dbg_info(1) + repeat + s += 1 + until dbg_info(s) ~= root + if warn then + error(msg, s) + else + print(`[jecs] error: {msg}\n`) + end + end + + local function ASSERT(v: T, msg: string) + if v then + return + end + throw(msg) + end + + local function get_name(world, id) + return world_get_one_inline(world, id, EcsName) + end + + local function bname(world: World, id): string + local name: string + if ECS_IS_PAIR(id) then + local first = get_name(world, ecs_pair_first(world, id)) + local second = get_name(world, ecs_pair_second(world, id)) + name = `pair({first}, {second})` + else + return get_name(world, id) + end + if name then + return name + else + return `${id}` + end + end + + local function ID_IS_TAG(world: World, id) + if ECS_IS_PAIR(id) then + id = ecs_pair_first(world, id) + end + return not world_has_one_inline(world, id, EcsComponent) + end + + World.query = function(world: World, ...) + ASSERT((...), "Requires at least a single component") + return world_query(world, ...) + end + + World.set = function(world: World, entity: i53, id: i53, value: any): () + local is_tag = ID_IS_TAG(world, id) + if is_tag and value == nil then + local _1 = bname(world, entity) + local _2 = bname(world, id) + local why = "cannot set component value to nil" + throw(why) + return + elseif value ~= nil and is_tag then + local _1 = bname(world, entity) + local _2 = bname(world, id) + local why = `cannot set a component value because {_2} is a tag` + why ..= `\n[jecs] note: consider using "world:add({_1}, {_2})" instead` + throw(why) + return + end + + world_set(world, entity, id, value) + end + + World.add = function(world: World, entity: i53, id: i53, value: any) + if value ~= nil then + local _1 = bname(world, entity) + local _2 = bname(world, id) + throw("You provided a value when none was expected. " .. `Did you mean to use "world:add({_1}, {_2})"`) + end + + world_add(world, entity, id) + end + + World.get = function(world: World, entity: i53, ...) + local length = select("#", ...) + ASSERT(length < 5, "world:get does not support more than 4 components") + local _1 + for i = 1, length do + local id = select(i, ...) + local id_is_tag = not world_has(world, id, EcsComponent) + if id_is_tag then + local name = get_name(world, id) + if not _1 then + _1 = get_name(world, entity) + end + throw( + `cannot get (#{i}) component {name} value because it is a tag.` + .. `\n[jecs] note: If this was intentional, use "world:has({_1}, {name}) instead"` + ) + end + end + + return world_get(world, entity, ...) + end +end + +function World.new() + local entity_index: EntityIndex = { + dense_array = {} :: { [i24]: i53 }, + sparse_array = {} :: { [i53]: Record }, alive_count = 0, max_id = 0, - } :: ecs_entity_index_t + } local self = setmetatable({ - archetype_edges = {}, - archetype_index = {} :: { [string]: Archetype }, archetypes = {} :: Archetypes, component_index = {} :: ComponentIndex, @@ -2324,7 +2335,7 @@ local function world_new() end world_add(self, EcsName, EcsComponent) - world_add(self, EcsOnChange, EcsComponent) + world_add(self, EcsOnSet, EcsComponent) world_add(self, EcsOnAdd, EcsComponent) world_add(self, EcsOnRemove, EcsComponent) world_add(self, EcsWildcard, EcsComponent) @@ -2332,7 +2343,7 @@ local function world_new() world_set(self, EcsOnAdd, EcsName, "jecs.OnAdd") world_set(self, EcsOnRemove, EcsName, "jecs.OnRemove") - world_set(self, EcsOnChange, EcsName, "jecs.OnChange") + world_set(self, EcsOnSet, EcsName, "jecs.OnSet") world_set(self, EcsWildcard, EcsName, "jecs.Wildcard") world_set(self, EcsChildOf, EcsName, "jecs.ChildOf") world_set(self, EcsComponent, EcsName, "jecs.Component") @@ -2348,14 +2359,23 @@ local function world_new() return self end -World.new = world_new +export type Entity = {__T: T} + +export type Id = + | Entity + | Pair, Entity> + | Pair> + | Pair, Entity> + +export type Pair = number & { + __P: P, + __O: O, +} + +type Item = (self: Query) -> (Entity, T...) + +type Iter = (query: Query) -> () -> (Entity, 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...) -export type Iter = (query: Query) -> () -> (Entity, T...) export type Query = typeof(setmetatable({}, { __iter = (nil :: any) :: Iter, @@ -2372,9 +2392,9 @@ export type Observer = { query: QueryInner, } -export type Observable = { - [Id]: { - [Id]: { +type Observable = { + [i53]: { + [i53]: { { Observer } } } @@ -2393,44 +2413,51 @@ export type World = { observable: any, --- Creates a new entity - entity: (self: World, id: Entity?) -> 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: Id, index: number?) -> 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) -> (), + delete: (self: World, id: Entity) -> (), --- Adds a component to the entity with no value - add: (self: World, id: Entity, component: Id) -> (), + 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) -> (), + set: (self: World, id: Entity, component: Id, data: U) -> (), cleanup: (self: World) -> (), -- Clears an entity from the world - clear: (self: World, id: Entity) -> (), + clear: (self: World, id: Entity) -> (), --- Removes a component from the given entity - remove: (self: World, id: Entity, component: Id) -> (), + 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?), + 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, + 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, + parent: (self: World, entity: Entity) -> Entity, --- Checks if the world contains the given entity - contains:(self: World, entity: Entity) -> boolean, + contains: (self: World, entity: Entity) -> boolean, - each: (self: World, id: Id) -> () -> Entity, + each: (self: World, id: Id) -> () -> Entity, - children: (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) @@ -2461,11 +2488,11 @@ export type World = { return { World = World :: { new: () -> World }, - world = world_new :: () -> World, + world = World.new :: () -> World, OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>, OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>, - OnChange = EcsOnChange :: Entity<(entity: Entity, data: any) -> ()>, + OnSet = EcsOnSet :: Entity<(entity: Entity, data: any) -> ()>, ChildOf = EcsChildOf :: Entity, Component = EcsComponent :: Entity, Wildcard = EcsWildcard :: Entity, @@ -2477,7 +2504,7 @@ return { Name = EcsName :: Entity, Rest = EcsRest :: Entity, - pair = (ECS_PAIR :: any) :: (first: Id

, second: Id) -> Pair, + pair = ECS_PAIR :: (first: P, second: O) -> Pair, -- Inwards facing API for testing ECS_ID = ECS_ENTITY_T_LO, @@ -2499,6 +2526,11 @@ return { find_insert = find_insert, find_archetype_with = find_archetype_with, find_archetype_without = find_archetype_without, + archetype_init_edge = archetype_init_edge, + archetype_ensure_edge = archetype_ensure_edge, + init_edge_for_add = init_edge_for_add, + init_edge_for_remove = init_edge_for_remove, + create_edge_for_add = create_edge_for_add, create_edge_for_remove = create_edge_for_remove, archetype_traverse_add = archetype_traverse_add, archetype_traverse_remove = archetype_traverse_remove, diff --git a/jecs/pesde-rbx.toml b/jecs/pesde-rbx.toml index f1afa27..587c1ac 100644 --- a/jecs/pesde-rbx.toml +++ b/jecs/pesde-rbx.toml @@ -3,7 +3,7 @@ includes = ["init.luau", "pesde.toml", "README.md", "CHANGELOG.md", "LICENSE", " license = "MIT" name = "marked/jecs_nightly" repository = "https://git.devmarked.win/marked/jecs-nightly" -version = "0.5.5-nightly.20250412T181729Z" +version = "0.5.5-nightly.20250312T202956Z" [indices] default = "https://github.com/pesde-pkg/index" diff --git a/jecs/pesde.toml b/jecs/pesde.toml index 2d4fd9a..636c50b 100644 --- a/jecs/pesde.toml +++ b/jecs/pesde.toml @@ -3,7 +3,7 @@ includes = ["init.luau", "pesde.toml", "README.md", "CHANGELOG.md", "LICENSE", " license = "MIT" name = "marked/jecs_nightly" repository = "https://git.devmarked.win/marked/jecs-nightly" -version = "0.5.5-nightly.20250412T181729Z" +version = "0.5.5-nightly.20250312T202956Z" [indices] default = "https://github.com/pesde-pkg/index" diff --git a/jecs/test.txt b/jecs/test.txt index 6b18d81..af62b72 100644 --- a/jecs/test.txt +++ b/jecs/test.txt @@ -1,2 +1,2 @@ passed = true -timestamp = "20250412T181730Z" +timestamp = "20250324T001101Z" diff --git a/jecs/test_fulllog.txt b/jecs/test_fulllog.txt index f588cc4..711c809 100644 --- a/jecs/test_fulllog.txt +++ b/jecs/test_fulllog.txt @@ -1,72 +1,17 @@ -*created e271v0 -*created e272v0 -*created e273v0 -*created e274v0 -*created e275v0 -*created e276v0 -*created e277v0 -*created e278v0 -*created e279v0 -*created e280v0 -|-alive--| -| e271v0 | -|--------| -| e272v0 | -|--------| -| e273v0 | -|--------| -| e274v0 | -|--------| -| e275v0 | -|--------| -| e276v0 | -|--------| -| e277v0 | -|--------| -| e278v0 | -|--------| -| e279v0 | -|--------| -| e280v0 | -|--------| - - -*deleted e270v0 -*deleted e271v0 -*deleted e272v0 -*deleted e273v0 -*deleted e274v0 -*deleted e275v0 -*deleted e274v1 -*deleted e273v1 -*deleted e272v1 -*deleted e271v1 -*deleted e270v1 -----idempotent -1_2 -1_2 -7.1 us  1 kB│ delete children of entity -9.2 us  1 kB│ remove friends of entity -346 ns  0  B│ simple deletion of entity -the great reset -PASS│  - -#repro3 -PASS│ should add the correct ModelBase for parts -PASS│ should add the correct ModelBase for parts -PASS│  - -#adding a recycled target -PASS│  - -#repro2 -PASS│  - -another -PASS│  - +271 272 273 +---------------- delete e2 --------------- +"268439800_268439816_536875256_536875272" +"268439800_268439816_536875256" +"268439816_536875272" +"268439816" +----------------------------- +{} +7.7 us  2 kB│ delete children of entity + 11 us  2 kB│ remove friends of entity +321 ns  0  B│ simple deletion of entity +removing #repro -PASS│  +NONE│  archetype PASS│  @@ -107,7 +52,7 @@ PASS│ should allow wildcards in queries PASS│ should match against multiple pairs PASS│ should only relate alive entities -PASS│ should error when setting invalid pair +NONE│ should error when setting invalid pair PASS│ should find target for ChildOf PASS│ despawning while iterating NONE│ iterator invalidation @@ -124,7 +69,7 @@ world:clear() PASS│ should remove its components -PASS│ remove cleared ID from entities +PASS│ should move last record world:has() PASS│ should find Tag on entity @@ -141,7 +86,6 @@ PASS│ should allow deleting components PASS│ delete entities using another Entity as component with Delete cleanup action PASS│ delete children -PASS│ remove deleted ID from entities PASS│ fast delete PASS│ cycle @@ -177,5 +121,5 @@ PASS│ #2 PASS│ #3 -77/77 test cases passed in 29.411 ms. +69/69 test cases passed in 31.149 ms. 0 fails diff --git a/jecs/wally.toml b/jecs/wally.toml index 544c077..d23918c 100644 --- a/jecs/wally.toml +++ b/jecs/wally.toml @@ -5,4 +5,4 @@ license = "MIT" name = "mark-marks/jecs-nightly" realm = "shared" registry = "https://github.com/UpliftGames/wally-index" -version = "0.5.5-nightly.20250412T181729Z" +version = "0.5.5-nightly.20250312T202956Z" diff --git a/src/sync.luau b/src/sync.luau index 4c32371..2407cb4 100644 --- a/src/sync.luau +++ b/src/sync.luau @@ -27,14 +27,9 @@ local function sync(to: string): result.Identity fs.writeDir(to) end - if not fs.metadata(`{to}/addons`).exists then - fs.writeDir(`{to}/addons`) - end - progress:nextStage() -- fetch local includes = { - "addons/observers.luau", "jecs.luau", "README.md", "CHANGELOG.md", diff --git a/src/test.luau b/src/test.luau index 26aec26..0185fed 100644 --- a/src/test.luau +++ b/src/test.luau @@ -46,23 +46,23 @@ local function test(origin: string): result.Identity progress:nextStage() -- fetch - local includes = { - "tools/testkit.luau", - "tools/runtime_lints.luau", - "tools/lifetime_tracker.luau", - "tools/entity_visualiser.luau", - "test/tests.luau", - } - - for _, file in includes do - local contents = shared.fetch_raw(file) + do + local contents = shared.fetch_raw("tools/testkit.luau") if not contents.ok then progress:stop() - stdio.ewrite(`🔥 Couldn't get the latest source for {file}:\n{contents.err}\n`) - return result(false, `Couldn't get the latest source for {file}.`) + stdio.ewrite(`🔥 Couldn't get the latest source for tools/testkit.luau:\n{contents.err}\n`) + return result(false, "Couldn't get the latest source for tools/testkit.luau.") end - - fs.writeFile(`{origin}/{file}`, contents.val) + fs.writeFile(`{origin}/tools/testkit.luau`, contents.val) + end + do + local contents = shared.fetch_raw("test/tests.luau") + if not contents.ok then + progress:stop() + stdio.ewrite(`🔥 Couldn't get the latest source for test/tests.luau:\n{contents.err}\n`) + return result(false, "Couldn't get the latest source for test/tests.luau.") + end + fs.writeFile(`{origin}/test/tests.luau`, contents.val) end progress:nextStage() -- test