diff --git a/.forgejo/workflows/syncandrelease.yml b/.forgejo/workflows/syncandrelease.yml index 87bf76e..5915aad 100644 --- a/.forgejo/workflows/syncandrelease.yml +++ b/.forgejo/workflows/syncandrelease.yml @@ -1,4 +1,4 @@ -name: Sync & Release +name: Sync on: workflow_dispatch: @@ -6,11 +6,11 @@ on: - cron: "10 0 * * *" # Runs at 00:10 UTC every day jobs: - sync_and_release: - name: Sync & Release + sync: + name: Sync runs-on: docker container: - image: ghcr.io/catthehacker/ubuntu:act-24.04 + image: ghcr.io/catthehacker/ubuntu:act-22.04 steps: - name: Checkout Repository uses: actions/checkout@v4 @@ -36,16 +36,18 @@ jobs: - name: Publish Luau Target run: pesde publish -y - # Much hacky, much evil - - name: Publish Roblox Target + - name: Manifest Switcharoo run: | mv pesde.toml pesde-luau.toml mv pesde-rbx.toml pesde.toml - pesde install - pesde publish -y + + - name: Publish Roblox Target + run: pesde publish -y + + - name: Manifest Switcharoo + run: | mv pesde.toml pesde-rbx.toml mv pesde-luau.toml pesde.toml - pesde install - name: Read Jecs Version id: read_jecs_version @@ -54,7 +56,6 @@ jobs: echo "JECS_VERSION=$version" >> $GITHUB_OUTPUT - name: Create Pull Request - id: create_pull_request uses: https://git.devmarked.win/actions/create-pull-request@7174d368c2e4450dea17b297819eb28ae93ee645 with: title: Sync to upstream Jecs ${{ steps.read_jecs_version.outputs.JECS_VERSION }} diff --git a/.luaurc b/.luaurc index f856eba..07221f7 100644 --- a/.luaurc +++ b/.luaurc @@ -1,10 +1,8 @@ { "aliases": { "jecs": "jecs", - "testkit": "tools/testkit", - "mirror": "mirror", - "tools": "tools", - "addons": "addons" + "testkit": "test/testkit", + "mirror": "mirror" }, "languageMode": "strict" } diff --git a/.lune/pull.luau b/.lune/pull.luau index 3e50a31..08b9b55 100644 --- a/.lune/pull.luau +++ b/.lune/pull.luau @@ -87,7 +87,7 @@ local function download_list(list: { { path: string, output: string } }) end download_list({ - { path = "jecs.luau", output = "$" }, + { path = "jecs.luau", output = "init.luau" }, { path = ".luaurc", output = "$" }, { path = "LICENSE", output = "$" }, { path = "README.md", output = "$" }, @@ -113,7 +113,7 @@ local pesde_manifest: types.pesde_manifest = { repository = "https://git.devmarked.win/marked/jecs-pesde", license = manifest.package.license, includes = { - "jecs.luau", + "init.luau", "pesde.toml", "README.md", "CHANGELOG.md", @@ -123,7 +123,7 @@ local pesde_manifest: types.pesde_manifest = { target = { environment = "luau", - lib = "jecs.luau", + lib = "init.luau", }, indices = { @@ -139,7 +139,7 @@ local pesde_roblox_manifest: types.pesde_manifest = { repository = "https://git.devmarked.win/marked/jecs-pesde", license = manifest.package.license, includes = { - "jecs.luau", + "init.luau", "pesde.toml", "README.md", "CHANGELOG.md", @@ -149,8 +149,8 @@ local pesde_roblox_manifest: types.pesde_manifest = { target = { environment = "roblox", - lib = "jecs.luau", - build_files = { "jecs.luau" }, + lib = "init.luau", + build_files = { "init.luau" }, }, indices = { diff --git a/CHANGELOG.md b/CHANGELOG.md index fa1d870..52f87cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,53 +10,30 @@ The format is based on [Keep a Changelog][kac], and this project adheres to ## [Unreleased] -## [0.6.0] - 2025-05-10 - - `[world]`: - - Added `world:range` to restrict entity range - - Changed `world:entity` to accept the overload to create an entity at the desired id - - 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 - - Optimized `world:has` for both single component and multiple component presence. - - This comes at the cost that it cannot check the component presence for more than 4 components at a time. If this is important, consider calling to this function multiple times. + - 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.luau b/jecs.luau deleted file mode 100644 index f705f0e..0000000 --- a/jecs.luau +++ /dev/null @@ -1,2712 +0,0 @@ ---!optimize 2 ---!native ---!strict ---draft 4 - -type i53 = number -type i24 = number - -type Ty = { i53 } -type ArchetypeId = number - -type Column = { any } - -type Map = { [K]: V } - -type ecs_archetype_t = { - id: number, - types: Ty, - type: string, - entities: { number }, - columns: { Column }, - records: { [i53]: number }, - counts: { [i53]: number }, -} - -export type Archetype = { - id: number, - types: Ty, - type: string, - entities: { number }, - columns: { Column }, - records: { [Id]: number }, - counts: { [Id]: number }, -} - -type ecs_record_t = { - archetype: ecs_archetype_t, - row: number, - dense: i24, -} - -type ecs_id_record_t = { - cache: { number }, - counts: { number }, - flags: number, - size: number, - hooks: { - on_add: ((entity: i53, id: i53, data: any?) -> ())?, - on_change: ((entity: i53, id: i53, data: any) -> ())?, - on_remove: ((entity: i53, id: i53) -> ())?, - }, -} - -type ecs_id_index_t = Map - -type ecs_archetypes_map_t = { [string]: ecs_archetype_t } - -type ecs_archetypes_t = { ecs_archetype_t } - -type ecs_entity_index_t = { - dense_array: Map, - sparse_array: Map, - alive_count: number, - max_id: number, - range_begin: number?, - range_end: 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 EcsWildcard = HI_COMPONENT_ID + 4 -local EcsChildOf = HI_COMPONENT_ID + 5 -local EcsComponent = HI_COMPONENT_ID + 6 -local EcsOnDelete = HI_COMPONENT_ID + 7 -local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 -local EcsDelete = HI_COMPONENT_ID + 9 -local EcsRemove = HI_COMPONENT_ID + 10 -local EcsName = HI_COMPONENT_ID + 11 -local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12 -local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13 -local EcsRest = HI_COMPONENT_ID + 14 - -local ECS_ID_DELETE = 0b01 -local ECS_ID_IS_TAG = 0b10 -local ECS_ID_MASK = 0b00 - -local ECS_ENTITY_MASK = bit32.lshift(1, 24) -local ECS_GENERATION_MASK = bit32.lshift(1, 16) - -local NULL_ARRAY = table.freeze({}) :: Column -local NULL = newproxy(false) - -local ECS_INTERNAL_ERROR = [[ - This is an internal error, please file a bug report via the following link: - - https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md -]] - -local function ecs_assert(condition, msg: string?) - if not condition then - error(msg) - end -end - -local ecs_metadata: Map> = {} -local ecs_max_component_id = 0 -local ecs_max_tag_id = EcsRest - -local function ECS_COMPONENT() - ecs_max_component_id += 1 - if ecs_max_component_id > HI_COMPONENT_ID then - error("Too many components") - end - return ecs_max_component_id -end - -local function ECS_TAG() - ecs_max_tag_id += 1 - return ecs_max_tag_id -end - -local function ECS_META(id: i53, ty: i53, value: any?) - local bundle = ecs_metadata[id] - if bundle == nil then - bundle = {} - ecs_metadata[id] = bundle - end - bundle[ty] = if value == nil then NULL else value -end - -local function ECS_META_RESET() - ecs_metadata = {} - ecs_max_component_id = 0 - ecs_max_tag_id = EcsRest -end - -local function ECS_COMBINE(id: number, generation: number): i53 - return id + (generation * ECS_ENTITY_MASK) -end -local ECS_PAIR_OFFSET = 2^48 - -local function ECS_IS_PAIR(e: number): boolean - return e > ECS_PAIR_OFFSET -end - -local function ECS_GENERATION_INC(e: i53): i53 - if e > ECS_ENTITY_MASK then - local id = e % ECS_ENTITY_MASK - local generation = e // ECS_ENTITY_MASK - - local next_gen = generation + 1 - if next_gen >= ECS_GENERATION_MASK then - return id - end - - return ECS_COMBINE(id, next_gen) - end - return ECS_COMBINE(e, 1) -end - -local function ECS_ENTITY_T_LO(e: i53): i24 - return e % ECS_ENTITY_MASK -end - -local function ECS_ID(e: i53) - return e % ECS_ENTITY_MASK -end - -local function ECS_GENERATION(e: i53) - return e // ECS_ENTITY_MASK -end - -local function ECS_ENTITY_T_HI(e: i53): i24 - return e // ECS_ENTITY_MASK -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 -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 r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] - - if not r or r.dense == 0 then - return nil - end - - return r -end - -local function entity_index_try_get(entity_index: ecs_entity_index_t, entity: number): ecs_record_t? - local r = entity_index_try_get_any(entity_index, entity) - if r then - local r_dense = r.dense - if r_dense > entity_index.alive_count then - return nil - end - if entity_index.dense_array[r_dense] ~= entity then - return nil - end - end - return r -end - -local function entity_index_try_get_fast(entity_index: ecs_entity_index_t, entity: number): ecs_record_t? - local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] - if r then - if entity_index.dense_array[r.dense] ~= entity then - return nil - end - end - 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(entity_index: ecs_entity_index_t, entity: i53): i53? - local r = entity_index_try_get_any(entity_index, entity) - if r then - return entity_index.dense_array[r.dense] - end - return nil -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 -end - -local ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY = "Entity is outside range" - -local function entity_index_new_id(entity_index: ecs_entity_index_t): i53 - local dense_array = entity_index.dense_array - local alive_count = entity_index.alive_count - local sparse_array = entity_index.sparse_array - local max_id = entity_index.max_id - - if alive_count < max_id then - alive_count += 1 - entity_index.alive_count = alive_count - local id = dense_array[alive_count] - return id - end - - local id = max_id + 1 - local range_end = entity_index.range_end - ecs_assert(range_end == nil or id < range_end, ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY) - - entity_index.max_id = id - alive_count += 1 - entity_index.alive_count = alive_count - dense_array[alive_count] = id - sparse_array[id] = { dense = alive_count } :: ecs_record_t - - 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) -end - -local function ecs_pair_second(world: ecs_world_t, e: i53) - local obj = ECS_PAIR_SECOND(e) - return ecs_get_alive(world, obj) -end - -local function query_match(query: ecs_query_data_t, - archetype: ecs_archetype_t) - local records = archetype.records - local with = query.filter_with - - for _, id in with do - if not records[id] then - return false - end - end - - local without = query.filter_without - if without then - for _, id in without do - if records[id] then - return false - end - end - end - - return true -end - -local function find_observers(world: ecs_world_t, event: i53, - component: i53): { ecs_observer_t }? - local cache = world.observable[event] - if not cache then - return nil - end - 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 src_columns = from.columns - local dst_columns = to.columns - local dst_entities = to.entities - local src_entities = from.entities - - local last = #src_entities - local id_types = from.types - local records = to.records - - for i, column in src_columns do - if column == NULL_ARRAY then - continue - end - -- Retrieves the new column index from the source archetype's record from each component - -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. - local tr = records[id_types[i]] - - -- Sometimes target column may not exist, e.g. when you remove a component. - if tr then - dst_columns[tr][dst_row] = column[src_row] - end - - -- If the entity is the last row in the archetype then swapping it would be meaningless. - if src_row ~= last then - -- Swap rempves columns to ensure there are no holes in the archetype. - column[src_row] = column[last] - end - column[last] = nil - end - - local moved = #src_entities - - -- Move the entity from the source to the destination archetype. - -- Because we have swapped columns we now have to update the records - -- corresponding to the entities' rows that were swapped. - local e1 = src_entities[src_row] - local e2 = src_entities[moved] - - if src_row ~= moved then - src_entities[src_row] = e2 - end - - src_entities[moved] = nil :: any - dst_entities[dst_row] = e1 - - local sparse_array = entity_index.sparse_array - - local record1 = sparse_array[ECS_ENTITY_T_LO(e1)] - local record2 = sparse_array[ECS_ENTITY_T_LO(e2)] - record1.row = dst_row - record2.row = src_row -end - -local function archetype_append( - entity: i53, - archetype: ecs_archetype_t -): 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 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 sourceRow = record.row - local from = record.archetype - local dst_row = archetype_append(entity, to) - archetype_move(entity_index, to, dst_row, from, sourceRow) - record.archetype = to - record.row = dst_row -end - -local function hash(arr: { number }): string - return table.concat(arr, "_") -end - -local function fetch(id: i53, records: { number }, - columns: { Column }, row: number): any - local tr = records[id] - - if not tr then - return nil - end - - 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 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 records = archetype.records - local columns = archetype.columns - local row = record.row - - local va = fetch(a, records, columns, row) - - if not b then - return va - elseif not c then - return va, fetch(b, records, columns, row) - elseif not d then - return va, fetch(b, records, columns, row), fetch(c, records, columns, row) - elseif not e then - return va, fetch(b, records, columns, row), fetch(c, records, columns, row), fetch(d, records, columns, row) - else - error("args exceeded") - end -end - -local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): boolean - local record = entity_index_try_get_fast(world.entity_index, entity) - if not record then - return false - end - - local archetype = record.archetype - if not archetype then - return false - end - - local records = archetype.records - - return records[id] ~= nil -end - -local function ecs_is_tag(world: ecs_world_t, entity: i53): boolean - local idr = world.component_index[entity] - if idr then - return bit32.band(idr.flags, ECS_ID_IS_TAG) ~= 0 - end - return not world_has_one_inline(world, entity, EcsComponent) -end - -local function world_has(world: ecs_world_t, entity: i53, - a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean - - local record = entity_index_try_get_fast(world.entity_index, entity) - if not record then - return false - end - - local archetype = record.archetype - if not archetype then - return false - end - - local records = archetype.records - - return records[a] ~= nil and - (b == nil or records[b] ~= nil) and - (c == nil or records[c] ~= nil) and - (d == nil or records[d] ~= nil) and - (e == nil or error("args exceeded")) -end - -local function world_target(world: ecs_world_t, 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 - return nil - end - - local archetype = record.archetype - if not archetype then - return nil - end - - local r = ECS_PAIR(relation, EcsWildcard) - - local count = archetype.counts[r] - if not count then - return nil - end - - if nth >= count then - nth = nth + count + 1 - end - - nth = archetype.types[nth + archetype.records[r]] - if not nth then - return nil - end - - return entity_index_get_alive(world.entity_index, - ECS_PAIR_SECOND(nth)) -end - -local function ECS_ID_IS_WILDCARD(e: i53): boolean - local first = ECS_ENTITY_T_HI(e) - local second = ECS_ENTITY_T_LO(e) - return first == EcsWildcard or second == EcsWildcard -end - -local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t - local component_index = world.component_index - local entity_index = world.entity_index - local idr: ecs_id_record_t? = component_index[id] - - if idr then - return idr - end - - 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 - 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)) :: i53 - ecs_assert(target and entity_index_is_alive( - entity_index, target), ECS_INTERNAL_ERROR) - end - - local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) - local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0) - - local has_delete = false - - if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then - has_delete = true - end - - local on_add, on_change, on_remove = world_get(world, - relation, EcsOnAdd, EcsOnChange, EcsOnRemove) - - 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) - end - - flags = bit32.bor( - flags, - if has_delete then ECS_ID_DELETE else 0, - if is_tag then ECS_ID_IS_TAG else 0 - ) - - idr = { - size = 0, - cache = {}, - counts = {}, - flags = flags, - hooks = { - on_add = on_add, - on_change = on_change, - on_remove = on_remove, - }, - } :: ecs_id_record_t - - component_index[id] = idr - - return idr -end - -local function archetype_append_to_records( - idr: ecs_id_record_t, - archetype: ecs_archetype_t, - id: i53, - index: number -) - local archetype_id = archetype.id - local archetype_records = archetype.records - local archetype_counts = archetype.counts - local idr_columns = idr.cache - local idr_counts = idr.counts - local tr = idr_columns[archetype_id] - if not tr then - idr_columns[archetype_id] = index - idr_counts[archetype_id] = 1 - - archetype_records[id] = index - archetype_counts[id] = 1 - else - local max_count = idr_counts[archetype_id] + 1 - idr_counts[archetype_id] = max_count - archetype_counts[id] = max_count - end -end - -local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: i53?): ecs_archetype_t - 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 records: { number } = {} - local counts: { number } = {} - - local archetype: ecs_archetype_t = { - columns = columns, - entities = {}, - id = archetype_id, - records = records, - counts = counts, - type = ty, - types = id_types, - } - - 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) - - 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) - - local t = ECS_PAIR(EcsWildcard, object) - local idr_t = id_record_ensure(world, t) - archetype_append_to_records(idr_t, archetype, t, i) - end - - if bit32.band(idr.flags, ECS_ID_IS_TAG) == 0 then - columns[i] = {} - else - columns[i] = NULL_ARRAY - end - end - - for id in records do - local observer_list = find_observers(world, EcsOnArchetypeCreate, id) - if not observer_list then - continue - end - for _, observer in observer_list do - if query_match(observer.query, archetype) then - observer.callback(archetype) - end - end - end - - world.archetype_index[ty] = archetype - world.archetypes[archetype_id] = archetype - world.archetype_edges[archetype.id] = {} :: Map - - return archetype -end - -local function world_range(world: ecs_world_t, range_begin: number, range_end: number?) - local entity_index = world.entity_index - - entity_index.range_begin = range_begin - entity_index.range_end = range_end - - local max_id = entity_index.max_id - - if range_begin > max_id then - local dense_array = entity_index.dense_array - local sparse_array = entity_index.sparse_array - - for i = max_id + 1, range_begin do - dense_array[i] = i - sparse_array[i] = { - dense = 0 - } :: ecs_record_t - end - entity_index.max_id = range_begin - 1 - entity_index.alive_count = range_begin - 1 - end -end - -local function world_entity(world: ecs_world_t, entity: i53?): i53 - local entity_index = world.entity_index - if entity then - local index = ECS_ID(entity) - local max_id = entity_index.max_id - local sparse_array = entity_index.sparse_array - local dense_array = entity_index.dense_array - local alive_count = entity_index.alive_count - local r = sparse_array[index] - if r then - local dense = r.dense - - if not dense or r.dense == 0 then - r.dense = index - dense = index - end - - local any = dense_array[dense] - if dense <= alive_count then - if any ~= entity then - error("Entity ID is already in use with a different generation") - else - return entity - end - end - - local e_swap = dense_array[dense] - local r_swap = entity_index_try_get_any(entity_index, e_swap) :: ecs_record_t - alive_count += 1 - entity_index.alive_count = alive_count - r_swap.dense = dense - r.dense = alive_count - dense_array[dense] = e_swap - dense_array[alive_count] = entity - - return entity - else - for i = max_id + 1, index do - sparse_array[i] = { dense = i } :: ecs_record_t - dense_array[i] = i - end - entity_index.max_id = index - - local e_swap = dense_array[alive_count] - local r_swap = sparse_array[alive_count] - r_swap.dense = index - - alive_count += 1 - entity_index.alive_count = alive_count - - r = sparse_array[index] - - r.dense = alive_count - - sparse_array[index] = r - - dense_array[index] = e_swap - dense_array[alive_count] = entity - - - return entity - end - end - return entity_index_new_id(entity_index, entity) -end - -local function world_parent(world: ecs_world_t, entity: i53) - return world_target(world, entity, EcsChildOf, 0) -end - -local function archetype_ensure(world: ecs_world_t, id_types): ecs_archetype_t - if #id_types < 1 then - return world.ROOT_ARCHETYPE - end - - local ty = hash(id_types) - local archetype = world.archetype_index[ty] - if archetype then - return archetype - end - - return archetype_create(world, id_types, ty) -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 - return i - end - end - return #id_types + 1 -end - -local function find_archetype_without( - world: ecs_world_t, - node: ecs_archetype_t, - id: i53 -): ecs_archetype_t - local id_types = node.types - local at = table.find(id_types, id) - - local dst = table.clone(id_types) - table.remove(dst, at) - - return archetype_ensure(world, dst) -end - - -local function create_edge_for_remove( - world: ecs_world_t, - node: ecs_archetype_t, - edge: Map, - id: i53 -): ecs_archetype_t - 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 - 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: ecs_archetype_t = edge[id] - if to == nil then - to = find_archetype_without(world, from, id) - edge[id] = to - edges[to.id][id] = from - end - - return to -end - -local function find_archetype_with(world, id, from): ecs_archetype_t - 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): ecs_archetype_t - 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 to = edge[id] - 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 world_add( - world: ecs_world_t, - entity: i53, - id: i53 -): () - local entity_index = world.entity_index - local record = entity_index_try_get_fast(entity_index, entity) - if not record then - return - end - - local from = record.archetype - local to = archetype_traverse_add(world, id, from) - if from == to then - return - end - if from then - entity_move(entity_index, entity, record, to) - else - if #to.types > 0 then - new_entity(entity, record, to) - end - end - - local idr = world.component_index[id] - local on_add = idr.hooks.on_add - - if on_add then - on_add(entity, id) - end -end - -local function world_set(world: ecs_world_t, 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 idr = world.component_index[id] - local idr_hooks = idr.hooks - - if from == to then - local tr = (to :: ecs_archetype_t).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, id, data) - end - - return - end - - if from then - -- If there was a previous archetype, then the entity needs to move the archetype - entity_move(entity_index, entity, record, to) - else - if #to.types > 0 then - -- When there is no previous archetype it should create the archetype - new_entity(entity, record, to) - end - 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, id, data) - end -end - -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, - -- so it needs to error when IDs intersect into the entity range. - error("Too many components, consider using world:entity() instead to create components.") - end - world.max_component_id = id - - return id -end - -local function world_remove(world: ecs_world_t, entity: i53, id: i53) - local entity_index = world.entity_index - local record = entity_index_try_get_fast(entity_index, entity) - if not record then - return - end - local from = record.archetype - - if not from then - return - end - - if from.records[id] then - local idr = world.component_index[id] - local on_remove = idr.hooks.on_remove - if on_remove then - on_remove(entity, id) - end - - local to = archetype_traverse_remove(world, id, record.archetype) - - entity_move(entity_index, entity, record, to) - end -end - -local function archetype_fast_delete_last(columns: { Column }, column_count: number, types: { i53 }, entity: i53) - for i, column in columns do - if column ~= NULL_ARRAY then - column[column_count] = nil - end - end -end - -local function archetype_fast_delete(columns: { Column }, column_count: number, row, types, entity) - for i, column in columns do - if column ~= NULL_ARRAY then - column[row] = column[column_count] - column[column_count] = nil - end - end -end - -local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, 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) - 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.hooks.on_remove - if on_remove then - on_remove(delete, id) - end - end - - entities[last] = nil :: any - - if row == last then - archetype_fast_delete_last(columns, column_count, id_types, delete) - else - archetype_fast_delete(columns, column_count, row, id_types, delete) - 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 - end - - if idr_t then - local queue: { i53 } - local ids: Map - - 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 = {} :: { [i53]: boolean } - end - ids[id] = true - removal_queued = true - end - - if not removal_queued then - continue - end - - if not queue then - queue = {} :: { i53 } - 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 - 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 - - for _, e in queue do - for id in ids do - world_remove(world, e, id) - end - end - end -end - -local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t) - 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 - - local archetype_id = archetype.id - world.archetypes[archetype_id] = nil :: any - world.archetype_index[archetype.type] = nil :: any - local records = archetype.records - - for id in records do - local observer_list = find_observers(world, EcsOnArchetypeDelete, id) - if not observer_list then - continue - end - for _, observer in observer_list do - if query_match(observer.query, archetype) then - observer.callback(archetype) - end - end - end - - for id in records do - local idr = component_index[id] - idr.cache[archetype_id] = nil :: any - idr.counts[archetype_id] = nil - idr.size -= 1 - records[id] = nil :: any - if idr.size == 0 then - component_index[id] = nil :: any - end - end -end - -local function world_cleanup(world: ecs_world_t) - local archetypes = world.archetypes - - for _, archetype in archetypes do - if #archetype.entities == 0 then - archetype_destroy(world, archetype) - end - end - - local new_archetypes = table.create(#archetypes) :: { ecs_archetype_t } - local new_archetype_map = {} - - for index, archetype in archetypes do - new_archetypes[index] = archetype - new_archetype_map[archetype.type] = archetype - end - - world.archetypes = new_archetypes - 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 - end - end - - if idr_t then - local children: { i53 } - local ids: Map - - 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 ~= 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 = {} :: { [i53]: boolean } - end - ids[id] = true - removal_queued = true - end - end - - if not removal_queued then - continue - end - if not children then - children = {} :: { i53 } - 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) - end - 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.cache - local flags = idr_r.flags - if (bit32.band(flags, ECS_ID_DELETE) :: number) ~= 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 = {} - local count = 0 - local ids = {} - 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 do - ids[types[tr]] = true - end - local n = #entities - table.move(entities, 1, n, count + 1, children) - count += n - end - - for _, child in children do - for id in ids do - world_remove(world, child, id) - end - end - - for archetype_id in archetype_ids do - archetype_destroy(world, archetypes[archetype_id]) - end - end - end - - local dense_array = entity_index.dense_array - local dense = record.dense - local i_swap = entity_index.alive_count - entity_index.alive_count = i_swap - 1 - - local e_swap = dense_array[i_swap] - local r_swap = entity_index_try_get_any(entity_index, e_swap) :: ecs_record_t - - r_swap.dense = dense - record.archetype = nil :: any - record.row = nil :: any - record.dense = i_swap - - dense_array[dense] = e_swap - dense_array[i_swap] = ECS_GENERATION_INC(entity) -end - -local function world_exists(world: ecs_world_t, entity): boolean - return entity_index_try_get_any(world.entity_index, entity) ~= nil -end - -local function world_contains(world: ecs_world_t, entity): boolean - return entity_index_is_alive(world.entity_index, entity) -end - -local function NOOP() end - -export type QueryInner = { - compatible_archetypes: { Archetype }, - ids: { i53 }, - filter_with: { i53 }, - filter_without: { i53 }, - next: () -> (number, ...any), - world: World, -} - -local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) - local world_query_iter_next - - local compatible_archetypes = query.compatible_archetypes - local lastArchetype = 1 - local archetype = compatible_archetypes[1] - if not archetype then - return NOOP :: () -> (number, ...any) - end - local columns = archetype.columns - local entities = archetype.entities - local i = #entities - local records = archetype.records - - local ids = query.ids - local A, B, C, D, E, F, G, H, I = unpack(ids) - local a: Column, b: Column, c: Column, d: Column - local e: Column, f: Column, g: Column, h: Column - - if not B then - a = columns[records[A]] - elseif not C then - a = columns[records[A]] - b = columns[records[B]] - elseif not D then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - elseif not E then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - elseif not F then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - elseif not G then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - elseif not H then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - elseif not I then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - h = columns[records[H]] - end - - if not B then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - end - - local row = i - i -= 1 - - return entity, a[row] - end - elseif not C then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row] - end - elseif not D then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row] - end - elseif not E then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row] - end - elseif not F then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row] - end - elseif not G then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row] - end - elseif not H then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] - end - elseif not I then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - h = columns[records[H]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] - end - else - local output = {} - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - end - - local row = i - i -= 1 - - for j, id in ids do - output[j] = columns[records[id]][row] - end - - return entity, unpack(output) - end - end - - query.next = world_query_iter_next - return world_query_iter_next -end - -local function query_iter(query): () -> (number, ...any) - local query_next = query.next - if not query_next then - query_next = query_iter_init(query) - end - return query_next -end - -local function query_without(query: ecs_query_data_t, ...: i53) - local without = { ... } - query.filter_without = without - local compatible_archetypes = query.compatible_archetypes - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local records = archetype.records - local matches = true - - for _, id in without do - if records[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: ecs_query_data_t, ...: i53) - local compatible_archetypes = query.compatible_archetypes - local with = { ... } - query.filter_with = with - - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local records = archetype.records - local matches = true - - for _, id in with do - if not records[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: ecs_query_data_t) - 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) - local a: Column, b: Column, c: Column, d: Column - local e: Column, f: Column, g: Column, h: Column - - local world_query_iter_next - local columns: { Column } - local entities: { number } - local i: number - local archetype: ecs_archetype_t - local records: { number } - local archetypes = query.compatible_archetypes - - local world = query.world :: { observable: ecs_observable_t } - -- 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 on_create_action = observable[EcsOnArchetypeCreate] - if not on_create_action then - on_create_action = {} :: Map - observable[EcsOnArchetypeCreate] = on_create_action - end - local query_cache_on_create = on_create_action[A] - if not query_cache_on_create then - query_cache_on_create = {} - on_create_action[A] = query_cache_on_create - end - - local on_delete_action = observable[EcsOnArchetypeDelete] - if not on_delete_action then - on_delete_action = {} :: Map - observable[EcsOnArchetypeDelete] = on_delete_action - end - local query_cache_on_delete = on_delete_action[A] - if not query_cache_on_delete then - query_cache_on_delete = {} - on_delete_action[A] = query_cache_on_delete - end - - local function on_create_callback(archetype) - table.insert(archetypes, archetype) - end - - local function on_delete_callback(archetype) - local i = table.find(archetypes, archetype) :: number - local n = #archetypes - archetypes[i] = archetypes[n] - archetypes[n] = nil - end - - local observer_for_create = { query = query, callback = on_create_callback } - local observer_for_delete = { query = query, callback = on_delete_callback } - - table.insert(query_cache_on_create, observer_for_create) - table.insert(query_cache_on_delete, observer_for_delete) - - local function cached_query_iter() - lastArchetype = 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return NOOP - end - entities = archetype.entities - i = #entities - records = archetype.records - columns = archetype.columns - if not B then - a = columns[records[A]] - elseif not C then - a = columns[records[A]] - b = columns[records[B]] - elseif not D then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - elseif not E then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - elseif not F then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - elseif not G then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - elseif not H then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - elseif not I then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - h = columns[records[H]] - end - - return world_query_iter_next - end - - if not B then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - end - - local row = i - i -= 1 - - return entity, a[row] - end - elseif not C then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row] - end - elseif not D then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row] - end - elseif not E then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row] - end - elseif not F then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row] - end - elseif not G then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row] - end - elseif not H then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] - end - elseif not I then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - h = columns[records[H]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] - end - else - local queryOutput = {} - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - end - - local row = i - i -= 1 - - if not F then - return entity, a[row], b[row], c[row], d[row], e[row] - elseif not G then - return entity, a[row], b[row], c[row], d[row], e[row], f[row] - elseif not H then - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] - elseif not I then - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] - end - - for j, id in ids do - queryOutput[j] = columns[records[id]][row] - end - - return entity, unpack(queryOutput) - end - end - - local cached_query = query :: any - cached_query.archetypes = query_archetypes - cached_query.__iter = cached_query_iter - cached_query.iter = cached_query_iter - setmetatable(cached_query, cached_query) - return cached_query -end - -local Query = {} -Query.__index = Query -Query.__iter = query_iter -Query.iter = query_iter_init -Query.without = query_without -Query.with = query_with -Query.archetypes = query_archetypes -Query.cached = query_cached - -local function world_query(world: ecs_world_t, ...) - local compatible_archetypes = {} - local length = 0 - - local ids = { ... } - - local archetypes = world.archetypes - - local idr: ecs_id_record_t? - local component_index = world.component_index - - local q = setmetatable({ - ids = ids, - compatible_archetypes = compatible_archetypes, - 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.cache do - local compatibleArchetype = archetypes[archetype_id] - if #compatibleArchetype.entities == 0 then - continue - end - local records = compatibleArchetype.records - - local skip = false - - for i, id in ids do - local tr = records[id] - if not tr 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: ecs_world_t, id: i53): () -> () - local idr = world.component_index[id] - if not idr then - return NOOP - end - - local idr_cache = idr.cache - local archetypes = world.archetypes - local archetype_id = next(idr_cache, nil) :: number - local archetype = archetypes[archetype_id] - if not archetype then - return NOOP - end - - local entities = archetype.entities - local row = #entities - - return function(): any - local entity = entities[row] - while not entity do - archetype_id = next(idr_cache, archetype_id) :: number - if not archetype_id then - return - end - archetype = archetypes[archetype_id] - entities = archetype.entities - row = #entities - entity = entities[row] - end - row -= 1 - return entity - end -end - -local function world_children(world: ecs_world_t, parent: i53) - 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, id: Entity, value: T) -> ())?, - on_change: ((entity: Entity, id: Entity, value: T) -> ())?, - on_remove: ((entity: Entity, id: 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, - range_begin: number?, - range_end: number? -} - -local World = {} -World.__index = World - -World.entity = world_entity -World.query = world_query -World.remove = world_remove -World.clear = world_clear -World.delete = world_delete -World.component = world_component -World.add = world_add -World.set = world_set -World.get = world_get -World.has = world_has -World.target = world_target -World.parent = world_parent -World.contains = world_contains -World.exists = world_exists -World.cleanup = world_cleanup -World.each = world_each -World.children = world_children -World.range = world_range - -local function world_new() - local entity_index = { - dense_array = {}, - sparse_array = {}, - alive_count = 0, - max_id = 0, - } :: ecs_entity_index_t - local self = setmetatable({ - archetype_edges = {}, - - archetype_index = {} :: { [string]: Archetype }, - archetypes = {} :: Archetypes, - component_index = {} :: ComponentIndex, - entity_index = entity_index, - ROOT_ARCHETYPE = (nil :: any) :: Archetype, - - max_archetype_id = 0, - max_component_id = ecs_max_component_id, - - observable = {} :: Observable, - }, World) :: any - - self.ROOT_ARCHETYPE = archetype_create(self, {}, "") - - for i = 1, HI_COMPONENT_ID do - local e = entity_index_new_id(entity_index) - world_add(self, e, EcsComponent) - end - - for i = HI_COMPONENT_ID + 1, EcsRest do - -- Initialize built-in components - entity_index_new_id(entity_index) - end - - world_add(self, EcsName, EcsComponent) - world_add(self, EcsOnChange, EcsComponent) - world_add(self, EcsOnAdd, EcsComponent) - world_add(self, EcsOnRemove, EcsComponent) - world_add(self, EcsWildcard, EcsComponent) - world_add(self, EcsRest, EcsComponent) - - world_set(self, EcsOnAdd, EcsName, "jecs.OnAdd") - world_set(self, EcsOnRemove, EcsName, "jecs.OnRemove") - world_set(self, EcsOnChange, EcsName, "jecs.OnChange") - world_set(self, EcsWildcard, EcsName, "jecs.Wildcard") - world_set(self, EcsChildOf, EcsName, "jecs.ChildOf") - world_set(self, EcsComponent, EcsName, "jecs.Component") - world_set(self, EcsOnDelete, EcsName, "jecs.OnDelete") - world_set(self, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget") - world_set(self, EcsDelete, EcsName, "jecs.Delete") - world_set(self, EcsRemove, EcsName, "jecs.Remove") - world_set(self, EcsName, EcsName, "jecs.Name") - world_set(self, EcsRest, EcsRest, "jecs.Rest") - - world_add(self, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) - - for i = EcsRest + 1, ecs_max_tag_id do - entity_index_new_id(entity_index) - end - - for i, bundle in ecs_metadata do - for ty, value in bundle do - if value == NULL then - world_add(self, i, ty) - else - world_set(self, i, ty, value) - end - end - end - - return self -end - -World.new = world_new - -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: Iter, - with: (self: Query, ...Id) -> Query, - without: (self: Query, ...Id) -> Query, - archetypes: (self: Query) -> { Archetype }, - cached: (self: Query) -> Query, - }, - {} :: { - __iter: Iter - } -)) - -export type Observer = { - callback: (archetype: Archetype) -> (), - query: QueryInner, -} - -export type Observable = { - [Id]: { - [Id]: { - { 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, - - --- 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, - --- 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?, - --- 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: Id) -> (), - --- 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: ((World, Entity, A) -> boolean) - & ((World, Entity, A, B) -> boolean) - & ((World, Entity, A, B, C) -> boolean) - & (World, Entity, A, B, C, D) -> 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, - - --- Checks if the entity exists - exists: (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")) --- if not __T then --- return ty:readproperty(types.singleton("__jecs_pair_value")) --- end --- return __T --- end - --- type function ecs_pair_t(first, second) --- if ecs_id_t(first):is("nil") then --- return second --- else --- return first --- end --- end --- - -return { - World = World :: { new: () -> World }, - world = world_new :: () -> World, - component = (ECS_COMPONENT :: any) :: () -> Entity, - tag = (ECS_TAG :: any) :: () -> Entity, - meta = (ECS_META :: any) :: (id: Entity, id: Id, value: T) -> Entity, - is_tag = (ecs_is_tag :: any) :: (World, Id) -> boolean, - - OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>, - OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>, - OnChange = EcsOnChange :: Entity<(entity: Entity, data: any) -> ()>, - ChildOf = EcsChildOf :: Entity, - Component = EcsComponent :: Entity, - Wildcard = EcsWildcard :: Entity, - w = EcsWildcard :: Entity, - OnDelete = EcsOnDelete :: Entity, - OnDeleteTarget = EcsOnDeleteTarget :: Entity, - Delete = EcsDelete :: Entity, - Remove = EcsRemove :: Entity, - Name = EcsName :: Entity, - Rest = EcsRest :: Entity, - - pair = (ECS_PAIR :: any) :: (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, - 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, - entity_index_get_alive = entity_index_get_alive, - - archetype_append_to_records = archetype_append_to_records, - id_record_ensure = id_record_ensure, - archetype_create = archetype_create, - archetype_ensure = archetype_ensure, - find_insert = find_insert, - find_archetype_with = find_archetype_with, - find_archetype_without = find_archetype_without, - create_edge_for_remove = create_edge_for_remove, - 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, - entity_index_is_alive = entity_index_is_alive, - entity_index_new_id = entity_index_new_id, - - query_iter = query_iter, - query_iter_init = query_iter_init, - query_with = query_with, - query_without = query_without, - query_archetypes = query_archetypes, - query_match = query_match, - - find_observers = find_observers, -} diff --git a/pesde-rbx.toml b/pesde-rbx.toml deleted file mode 100644 index d0520fc..0000000 --- a/pesde-rbx.toml +++ /dev/null @@ -1,22 +0,0 @@ -authors = ["jecs authors"] -description = "A minimal copy of jecs published on the official pesde registry" -includes = [ - "jecs.luau", - "pesde.toml", - "README.md", - "CHANGELOG.md", - "LICENSE", - ".luaurc", -] -license = "MIT" -name = "marked/jecs" -repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.6.0" - -[indices] -default = "https://github.com/pesde-pkg/index" - -[target] -build_files = ["jecs.luau"] -environment = "roblox" -lib = "jecs.luau" diff --git a/pesde.lock b/pesde.lock deleted file mode 100644 index 91f9895..0000000 --- a/pesde.lock +++ /dev/null @@ -1,6 +0,0 @@ -# This file is automatically @generated by pesde. -# It is not intended for manual editing. -format = 1 -name = "marked/jecs" -version = "0.6.0" -target = "luau" diff --git a/pesde.toml b/pesde.toml index ecf59dc..67a7bc4 100644 --- a/pesde.toml +++ b/pesde.toml @@ -1,7 +1,7 @@ authors = ["jecs authors"] description = "A minimal copy of jecs published on the official pesde registry" includes = [ - "jecs.luau", + "init.luau", "pesde.toml", "README.md", "CHANGELOG.md", @@ -11,11 +11,11 @@ includes = [ license = "MIT" name = "marked/jecs" repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.6.0" +version = "0.5.5" [indices] default = "https://github.com/pesde-pkg/index" [target] environment = "luau" -lib = "jecs.luau" +lib = "init.luau"