From 6e1bbbaedcfa7b01464509eaf464c0254072b7b9 Mon Sep 17 00:00:00 2001 From: marked Date: Wed, 12 Mar 2025 21:03:42 +0100 Subject: [PATCH 01/10] Bump workflow to ubuntu 24.04 --- .forgejo/workflows/syncandrelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/syncandrelease.yml b/.forgejo/workflows/syncandrelease.yml index 5915aad..945b23a 100644 --- a/.forgejo/workflows/syncandrelease.yml +++ b/.forgejo/workflows/syncandrelease.yml @@ -10,7 +10,7 @@ jobs: name: Sync runs-on: docker container: - image: ghcr.io/catthehacker/ubuntu:act-22.04 + image: ghcr.io/catthehacker/ubuntu:act-24.04 steps: - name: Checkout Repository uses: actions/checkout@v4 From 6f56208348282f224ed38d1f9955b13512ba72c3 Mon Sep 17 00:00:00 2001 From: marked Date: Wed, 12 Mar 2025 21:07:15 +0100 Subject: [PATCH 02/10] Evilness --- .forgejo/workflows/syncandrelease.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.forgejo/workflows/syncandrelease.yml b/.forgejo/workflows/syncandrelease.yml index 945b23a..c47b63f 100644 --- a/.forgejo/workflows/syncandrelease.yml +++ b/.forgejo/workflows/syncandrelease.yml @@ -36,18 +36,16 @@ jobs: - name: Publish Luau Target run: pesde publish -y - - name: Manifest Switcharoo + # Much hacky, much evil + - name: Publish Roblox Target run: | mv pesde.toml pesde-luau.toml mv pesde-rbx.toml pesde.toml - - - name: Publish Roblox Target - run: pesde publish -y - - - name: Manifest Switcharoo - run: | + pesde install + pesde publish -y mv pesde.toml pesde-rbx.toml mv pesde-luau.toml pesde.toml + pesde install - name: Read Jecs Version id: read_jecs_version @@ -56,6 +54,7 @@ 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 }} From c9380ff2b41c276e7c93119979786fa172e8eae1 Mon Sep 17 00:00:00 2001 From: marked Date: Wed, 12 Mar 2025 21:08:37 +0100 Subject: [PATCH 03/10] Rename action --- .forgejo/workflows/syncandrelease.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.forgejo/workflows/syncandrelease.yml b/.forgejo/workflows/syncandrelease.yml index c47b63f..87bf76e 100644 --- a/.forgejo/workflows/syncandrelease.yml +++ b/.forgejo/workflows/syncandrelease.yml @@ -1,4 +1,4 @@ -name: Sync +name: Sync & Release on: workflow_dispatch: @@ -6,8 +6,8 @@ on: - cron: "10 0 * * *" # Runs at 00:10 UTC every day jobs: - sync: - name: Sync + sync_and_release: + name: Sync & Release runs-on: docker container: image: ghcr.io/catthehacker/ubuntu:act-24.04 From aabec33829d8c5816ada24bbeff00ebd5473636c Mon Sep 17 00:00:00 2001 From: marked Date: Wed, 12 Mar 2025 21:09:14 +0100 Subject: [PATCH 04/10] Sync to upstream Jecs 0.5.5 (#8) Reviewed-on: https://git.devmarked.win/marked/jecs-pesde/pulls/8 --- pesde-rbx.toml | 22 ++++++++++++++++++++++ pesde.lock | 6 ++++++ 2 files changed, 28 insertions(+) create mode 100644 pesde-rbx.toml create mode 100644 pesde.lock diff --git a/pesde-rbx.toml b/pesde-rbx.toml new file mode 100644 index 0000000..c465287 --- /dev/null +++ b/pesde-rbx.toml @@ -0,0 +1,22 @@ +authors = ["jecs authors"] +description = "A minimal copy of jecs published on the official pesde registry" +includes = [ + "init.luau", + "pesde.toml", + "README.md", + "CHANGELOG.md", + "LICENSE", + ".luaurc", +] +license = "MIT" +name = "marked/jecs" +repository = "https://git.devmarked.win/marked/jecs-pesde" +version = "0.5.5" + +[indices] +default = "https://github.com/pesde-pkg/index" + +[target] +build_files = ["init.luau"] +environment = "roblox" +lib = "init.luau" diff --git a/pesde.lock b/pesde.lock new file mode 100644 index 0000000..a1e2363 --- /dev/null +++ b/pesde.lock @@ -0,0 +1,6 @@ +# This file is automatically @generated by pesde. +# It is not intended for manual editing. +format = 1 +name = "marked/jecs" +version = "0.5.5" +target = "luau" From d756446628bbbd59a0b7c377f5d48c17ad5780bb Mon Sep 17 00:00:00 2001 From: marked Date: Wed, 12 Mar 2025 21:35:54 +0100 Subject: [PATCH 05/10] Test in prod --- .lune/pull.luau | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.lune/pull.luau b/.lune/pull.luau index 08b9b55..3e50a31 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 = "init.luau" }, + { path = "jecs.luau", output = "$" }, { 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 = { - "init.luau", + "jecs.luau", "pesde.toml", "README.md", "CHANGELOG.md", @@ -123,7 +123,7 @@ local pesde_manifest: types.pesde_manifest = { target = { environment = "luau", - lib = "init.luau", + lib = "jecs.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 = { - "init.luau", + "jecs.luau", "pesde.toml", "README.md", "CHANGELOG.md", @@ -149,8 +149,8 @@ local pesde_roblox_manifest: types.pesde_manifest = { target = { environment = "roblox", - lib = "init.luau", - build_files = { "init.luau" }, + lib = "jecs.luau", + build_files = { "jecs.luau" }, }, indices = { From b19deb7f0f1c65be36fbe90dffbb6057310fd58f Mon Sep 17 00:00:00 2001 From: marked Date: Wed, 12 Mar 2025 21:36:28 +0100 Subject: [PATCH 06/10] Sync to upstream Jecs 0.5.5 (#9) Reviewed-on: https://git.devmarked.win/marked/jecs-pesde/pulls/9 --- jecs.luau | 2564 ++++++++++++++++++++++++++++++++++++++++++++++++ pesde-rbx.toml | 6 +- pesde.toml | 4 +- 3 files changed, 2569 insertions(+), 5 deletions(-) create mode 100644 jecs.luau diff --git a/jecs.luau b/jecs.luau new file mode 100644 index 0000000..e53d366 --- /dev/null +++ b/jecs.luau @@ -0,0 +1,2564 @@ +--!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 GraphEdge = { + from: Archetype, + to: Archetype?, + id: number, + prev: GraphEdge?, + next: GraphEdge?, +} + +type GraphEdges = Map + +type GraphNode = { + add: GraphEdges, + remove: GraphEdges, + refs: GraphEdge, +} + +export type Archetype = { + id: number, + types: Ty, + type: string, + entities: { number }, + columns: { Column }, + records: { number }, + counts: { number }, +} & GraphNode + +export type Record = { + archetype: Archetype, + row: number, + dense: i24, +} + +type IdRecord = { + columns: { number }, + counts: { number }, + flags: number, + size: number, + hooks: { + on_add: ((entity: i53) -> ())?, + on_set: ((entity: i53, data: any) -> ())?, + on_remove: ((entity: i53) -> ())?, + }, +} + +type ComponentIndex = Map + +type Archetypes = { [ArchetypeId]: Archetype } + +type ArchetypeDiff = { + added: Ty, + removed: Ty, +} + +type EntityIndex = { + dense_array: Map, + sparse_array: Map, + alive_count: number, + max_id: number, +} + +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 EcsOnSet = 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_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 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 + +local function FLAGS_ADD(is_pair: boolean): number + local flags = 0x0 + + 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 function ECS_IS_PAIR(e: number): boolean + return if e > ECS_ENTITY_MASK then (e % ECS_ID_FLAGS_MASK) // ECS_PAIR_FLAG ~= 0 else false +end + +-- HIGH 24 bits LOW 24 bits +local function ECS_GENERATION(e: i53): i24 + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0 +end + +local function ECS_GENERATION_INC(e: i53) + if e > ECS_ENTITY_MASK then + 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 + return id + end + + return ECS_COMBINE(id, next_gen) + end + return ECS_COMBINE(e, 1) +end + +-- FIRST gets the high ID +local function ECS_ENTITY_T_HI(e: i53): i24 + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_ENTITY_MASK else e +end + +-- 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 + return ECS_COMBINE(ECS_ENTITY_T_LO(pred), ECS_ENTITY_T_LO(obj)) + FLAGS_ADD(--[[isPair]] true) :: i53 +end + +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 then + return nil + end + + if not r or r.dense == 0 then + return nil + end + + return r +end + +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 + 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: 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 + return nil + end + end + return r +end + +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 0 +end + +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: EntityIndex): i53 + local dense_array = entity_index.dense_array + local alive_count = entity_index.alive_count + 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 = 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 } :: Record + + return id +end + +-- 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 + +-- 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, archetype: Archetype) + 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: World, event, component): { Observer }? + local cache = world.observable[event] + if not cache then + return nil + end + return cache[component] :: any +end + +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 + 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: 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: 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: EntityIndex, entity: i53, record: Record, to: Archetype) + 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, 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: 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 + 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_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 + end + + local archetype = record.archetype + if not archetype then + return false + end + + local records = archetype.records + + return records[id] ~= nil +end + +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 + end + + local archetype = record.archetype + if not archetype then + return false + end + + local records = archetype.records + + for i = 1, select("#", ...) do + if not records[select(i, ...)] then + return false + end + end + + return true +end + +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 + return nil + end + + local archetype = record.archetype + if not archetype then + return nil + end + + local idr = world.component_index[ECS_PAIR(relation, EcsWildcard)] + if not idr then + return nil + end + + local archetype_id = archetype.id + local count = idr.counts[archetype.id] + if not count then + return nil + end + + if nth >= count then + nth = nth + count + 1 + end + + local tr = idr.columns[archetype_id] + + nth = archetype.types[nth + tr] + + if not nth then + return nil + end + + return ecs_pair_second(world, 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: World, id: number): IdRecord + local component_index = world.component_index + local idr: IdRecord = component_index[id] + + if not idr then + local flags = ECS_ID_MASK + local relation = id + local is_pair = ECS_IS_PAIR(id) + if is_pair then + relation = ecs_pair_first(world, id) + 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_set, on_remove = world_get(world, relation, EcsOnAdd, EcsOnSet, 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, 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 + ) + + idr = { + size = 0, + columns = {}, + counts = {}, + flags = flags, + hooks = { + on_add = on_add, + on_set = on_set, + on_remove = on_remove, + }, + } + + component_index[id] = idr + end + + return idr +end + +local function archetype_append_to_records( + idr: IdRecord, + archetype: Archetype, + id: number, + index: number +) + local archetype_id = archetype.id + local archetype_records = archetype.records + local archetype_counts = archetype.counts + local idr_columns = idr.columns + 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: World, id_types: { i24 }, ty, prev: i53?): Archetype + local archetype_id = (world.max_archetype_id :: number) + 1 + world.max_archetype_id = archetype_id + + local length = #id_types + local columns = (table.create(length) :: any) :: { Column } + + local records: { number } = {} + local counts: {number} = {} + + local archetype: Archetype = { + columns = columns, + entities = {}, + id = archetype_id, + records = records, + counts = counts, + type = ty, + types = id_types, + + add = {}, + remove = {}, + refs = {} :: GraphEdge, + } + + 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) + + 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 + + return archetype +end + +local function world_entity(world: World): i53 + return entity_index_new_id(world.entity_index) +end + +local function world_parent(world: World, entity: i53) + return world_target(world, entity, EcsChildOf, 0) +end + +local function archetype_ensure(world: World, id_types): Archetype + 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 + return -1 + end + if id > toAdd then + return i + end + end + return #id_types + 1 +end + +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) + + 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 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) + init_edge_for_remove(world, node, edge, id, to) + return to +end + +local function archetype_traverse_add(world: World, id: i53, from: Archetype): Archetype + from = from or world.ROOT_ARCHETYPE + local edge = archetype_ensure_edge(world, from.add, id) + + local to = edge.to + if not to then + to = create_edge_for_add(world, from, edge, id) + end + + return to :: Archetype +end + +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 + 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) + end +end + +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: 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 + local on_set = idr_hooks.on_set + if on_set then + on_set(entity, 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 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_set = idr_hooks.on_set + if on_set then + on_set(entity, 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: 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 + return + end + local from = record.archetype + + if not from then + return + end + local to = archetype_traverse_remove(world, id, from) + + 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 + + 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: World, archetype: Archetype, row: number, destruct: boolean?) + 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) + 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: World, entity: i53) + --TODO: use sparse_get (stashed) + local record = entity_index_try_get(world.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 + + record.archetype = nil :: any + record.row = nil :: any +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_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 + archetype_clear_edges(archetype) + 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.columns[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: World) + 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) :: { Archetype } + 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 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 + + 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, destruct) + end + + 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.columns 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.columns 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 + + local sparse_array = entity_index.sparse_array + local dense_array = entity_index.dense_array + + if idr_t then + for archetype_id in idr_t.columns do + local children = {} + local idr_t_archetype = archetypes[archetype_id] + + local idr_t_types = idr_t_archetype.types + + for _, child in idr_t_archetype.entities do + table.insert(children, child) + end + + local n = #children + + 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 + 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 = n, 1, -1 do + world_delete(world, children[i]) + end + break + else + local on_remove = id_record.hooks.on_remove + local to = archetype_traverse_remove(world, id, idr_t_archetype) + local empty = #to.types == 0 + for i = n, 1, -1 do + local child = children[i] + if on_remove then + on_remove(child) + end + local r = sparse_array[ECS_ENTITY_T_LO(child)] + if not empty then + entity_move(entity_index, child, r, to) + end + end + end + end + end + + archetype_destroy(world, idr_t_archetype) + 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 +end + +local function world_contains(world: World, entity): boolean + return entity_index_is_alive(world.entity_index, entity) +end + +local function NOOP() end + +local function ARM(query, ...) + return query +end + +local EMPTY_LIST = {} +local EMPTY_QUERY = { + __iter = function() + return NOOP + end, + iter = function() + return NOOP + end, + with = ARM, + without = ARM, + archetypes = function() + return EMPTY_LIST + end, +} + +setmetatable(EMPTY_QUERY, EMPTY_QUERY) + +type QueryInner = { + compatible_archetypes: { Archetype }, + ids: { i53 }, + filter_with: { i53 }, + filter_without: { i53 }, + next: () -> (number, ...any), + world: World, +} + +local function query_iter_init(query: QueryInner): () -> (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: QueryInner, ...: 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: QueryInner, ...: 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: QueryInner) + local with = query.filter_with + local ids = query.ids + if with then + table.move(ids, 1, #ids, #with + 1, with) + else + query.filter_with = ids + end + + local compatible_archetypes = query.compatible_archetypes + local lastArchetype = 1 + + local A, B, C, D, E, F, G, H, I = unpack(ids) + 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: Archetype + local records: { number } + local archetypes = query.compatible_archetypes + + 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 :: Observable + local on_create_action = observable[EcsOnArchetypeCreate] + if not on_create_action then + on_create_action = {} + observable[EcsOnArchetypeCreate] = on_create_action + end + local query_cache_on_create = on_create_action[A] + if not query_cache_on_create then + 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 = {} + 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: World, ...) + local compatible_archetypes = {} + local length = 0 + + local ids = { ... } + + local archetypes = world.archetypes + + local idr: IdRecord? + 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 < idr.size then + idr = map + end + end + + if not idr then + return q + end + + for archetype_id in idr.columns 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: World, id): () -> () + local idr = world.component_index[id] + if not idr then + return NOOP + end + + local idr_columns = idr.columns + local archetypes = world.archetypes + local archetype_id = next(idr_columns, 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_columns, 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, parent) + return world_each(world, ECS_PAIR(EcsChildOf, parent)) +end + +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.cleanup = world_cleanup +World.each = world_each +World.children = world_children + +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, + } + local self = setmetatable({ + 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 = 0, + + 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, EcsOnSet, 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, 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") + 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)) + + return self +end + +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 Query = typeof(setmetatable({}, { + __iter = (nil :: any) :: Iter, +})) & { + iter: Iter, + with: (self: Query, ...Id) -> Query, + without: (self: Query, ...Id) -> Query, + archetypes: (self: Query) -> { Archetype }, + cached: (self: Query) -> Query, +} + +export type Observer = { + callback: (archetype: Archetype) -> (), + query: QueryInner, +} + +type Observable = { + [i53]: { + [i53]: { + { Observer } + } + } +} + +export type World = { + archetype_index: { [string]: Archetype }, + archetypes: Archetypes, + component_index: ComponentIndex, + entity_index: EntityIndex, + ROOT_ARCHETYPE: Archetype, + + max_component_id: number, + max_archetype_id: number, + + observable: any, + + --- Creates a new entity + entity: (self: World) -> Entity, + --- Creates a new entity located in the first 256 ids. + --- These should be used for static components for fast access. + component: (self: World) -> Entity, + --- Gets the target of an relationship. For example, when a user calls + --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. + target: (self: World, id: Entity, relation: Entity, index: number?) -> Entity?, + --- Deletes an entity and all it's related components and relationships. + delete: (self: World, id: Entity) -> (), + + --- Adds a component to the entity with no value + add: (self: World, id: Entity, component: Id) -> (), + --- Assigns a value to a component on the given entity + set: (self: World, id: Entity, component: Id, data: U) -> (), + + cleanup: (self: World) -> (), + -- Clears an entity from the world + clear: (self: World, id: Entity) -> (), + --- Removes a component from the given entity + remove: (self: World, id: Entity, component: Id) -> (), + --- Retrieves the value of up to 4 components. These values may be nil. + get: ((self: World, id: Entity, Id) -> A?) + & ((self: World, id: Entity, Id, Id) -> (A?, B?)) + & ((self: World, id: Entity, Id, Id, Id) -> (A?, B?, C?)) + & (self: World, id: Entity, Id, Id, Id, Id) -> (A?, B?, C?, D?), + + --- Returns whether the entity has the ID. + has: ((self: World, entity: Entity, ...Id) -> boolean) + & ((self: World, entity: Entity, Id, Id) -> boolean) + & ((self: World, entity: Entity, Id, Id, Id) -> boolean) + & ((self: World, entity: Entity, Id, Id, Id, Id) -> boolean) + & ((self: World, entity: Entity, Id, Id, Id, Id, Id) -> boolean) + & ((self: World, entity: Entity, Id, Id, Id, Id, Id, Id) -> boolean) + & ((self: World, entity: Entity, Id, Id, Id, Id, Id, Id, Id) -> boolean) + & ((self: World, entity: Entity, Id, Id, Id, Id, Id, Id, Id, ...unknown) -> boolean), + + --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. + parent: (self: World, entity: Entity) -> Entity, + + --- Checks if the world contains the given entity + contains: (self: World, entity: Entity) -> boolean, + + each: (self: World, id: Id) -> () -> Entity, + + children: (self: World, id: Id) -> () -> Entity, + + --- Searches the world for entities that match a given query + query: ((World, Id) -> Query) + & ((World, Id, Id) -> Query) + & ((World, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id, Id, Id, ...Id) -> Query) +} +-- type function ecs_id_t(entity) +-- local ty = entity:components()[2] +-- local __T = ty:readproperty(types.singleton("__T")) +-- 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 }, + + OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>, + OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>, + OnSet = EcsOnSet :: 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 :: (first: P, second: O) -> 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, + + IS_PAIR = ECS_IS_PAIR, + pair_first = ecs_pair_first, + pair_second = ecs_pair_second, + 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, + 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, + + 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 index c465287..f1b1745 100644 --- a/pesde-rbx.toml +++ b/pesde-rbx.toml @@ -1,7 +1,7 @@ authors = ["jecs authors"] description = "A minimal copy of jecs published on the official pesde registry" includes = [ - "init.luau", + "jecs.luau", "pesde.toml", "README.md", "CHANGELOG.md", @@ -17,6 +17,6 @@ version = "0.5.5" default = "https://github.com/pesde-pkg/index" [target] -build_files = ["init.luau"] +build_files = ["jecs.luau"] environment = "roblox" -lib = "init.luau" +lib = "jecs.luau" diff --git a/pesde.toml b/pesde.toml index 67a7bc4..ccfcc7c 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 = [ - "init.luau", + "jecs.luau", "pesde.toml", "README.md", "CHANGELOG.md", @@ -18,4 +18,4 @@ default = "https://github.com/pesde-pkg/index" [target] environment = "luau" -lib = "init.luau" +lib = "jecs.luau" From 32230ab28197110be3027a6775971f72c78c4f8b Mon Sep 17 00:00:00 2001 From: marked Date: Thu, 8 May 2025 02:11:43 +0200 Subject: [PATCH 07/10] Sync to upstream Jecs 0.6.0-rc.1 (#10) Reviewed-on: https://git.devmarked.win/marked/jecs-pesde/pulls/10 --- .luaurc | 5 +- jecs.luau | 1026 ++++++++++++++++++++++++------------------------ pesde-rbx.toml | 2 +- pesde.lock | 2 +- pesde.toml | 2 +- 5 files changed, 520 insertions(+), 517 deletions(-) diff --git a/.luaurc b/.luaurc index 07221f7..d1ae244 100644 --- a/.luaurc +++ b/.luaurc @@ -1,8 +1,9 @@ { "aliases": { "jecs": "jecs", - "testkit": "test/testkit", - "mirror": "mirror" + "testkit": "tools/testkit", + "mirror": "mirror", + "tools": "tools", }, "languageMode": "strict" } diff --git a/jecs.luau b/jecs.luau index e53d366..69a4ad7 100644 --- a/jecs.luau +++ b/jecs.luau @@ -13,40 +13,50 @@ type Column = { any } type Map = { [K]: V } -type GraphEdge = { - from: Archetype, - to: Archetype?, +type ecs_graph_edge_t = { + from: ecs_archetype_t, + to: ecs_archetype_t?, id: number, - prev: GraphEdge?, - next: GraphEdge?, + prev: ecs_graph_edge_t?, + next: ecs_graph_edge_t?, } -type GraphEdges = Map +type ecs_graph_edges_t = Map -type GraphNode = { - add: GraphEdges, - remove: GraphEdges, - refs: GraphEdge, +type ecs_graph_node_t = { + add: ecs_graph_edges_t, + remove: ecs_graph_edges_t, + refs: ecs_graph_edge_t, } +type ecs_archetype_t = { + id: number, + types: Ty, + type: string, + entities: { number }, + columns: { Column }, + records: { [i53]: number }, + counts: { [i53]: number }, +} & ecs_graph_node_t + export type Archetype = { id: number, types: Ty, type: string, entities: { number }, columns: { Column }, - records: { number }, - counts: { number }, -} & GraphNode + records: { [Id]: number }, + counts: { [Id]: number }, +} -export type Record = { - archetype: Archetype, +type ecs_record_t = { + archetype: ecs_archetype_t, row: number, dense: i24, } -type IdRecord = { - columns: { number }, +type ecs_id_record_t = { + cache: { number }, counts: { number }, flags: number, size: number, @@ -57,22 +67,46 @@ type IdRecord = { }, } -type ComponentIndex = Map +type ecs_id_index_t = Map -type Archetypes = { [ArchetypeId]: Archetype } +type ecs_archetypes_map_t = { [string]: ecs_archetype_t } -type ArchetypeDiff = { - added: Ty, - removed: Ty, -} +type ecs_archetypes_t = { ecs_archetype_t } -type EntityIndex = { - dense_array: Map, - sparse_array: Map, +type ecs_entity_index_t = { + 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 = { + 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 @@ -90,60 +124,39 @@ local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12 local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13 local EcsRest = HI_COMPONENT_ID + 14 -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 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 -local function FLAGS_ADD(is_pair: boolean): number - local flags = 0x0 +local ECS_ENTITY_MASK = bit32.lshift(1, 24) +local ECS_GENERATION_MASK = bit32.lshift(1, 16) - 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 +local NULL_ARRAY = table.freeze({}) +local ECS_INTERNAL_ERROR = [[ + This is an internal error, please file a bug report via the following link: - return flags -end - -local function ECS_COMBINE(source: number, target: number): i53 - return (source * 268435456) + (target * ECS_ID_FLAGS_MASK) + https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md +]] + +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 if e > ECS_ENTITY_MASK then (e % ECS_ID_FLAGS_MASK) // ECS_PAIR_FLAG ~= 0 else false + return e > ECS_PAIR_OFFSET end --- HIGH 24 bits LOW 24 bits -local function ECS_GENERATION(e: i53): i24 - return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0 -end - -local function ECS_GENERATION_INC(e: i53) +local function ECS_GENERATION_INC(e: i53): i53 if e > ECS_ENTITY_MASK then - local flags = e // ECS_ID_FLAGS_MASK - local id = flags // ECS_ENTITY_MASK - local generation = flags % ECS_GENERATION_MASK + local id = e % ECS_ENTITY_MASK + local generation = e // ECS_ENTITY_MASK local next_gen = generation + 1 - if next_gen > ECS_GENERATION_MASK then + if next_gen >= ECS_GENERATION_MASK then return id end @@ -152,29 +165,38 @@ local function ECS_GENERATION_INC(e: i53) return ECS_COMBINE(e, 1) end --- FIRST gets the high ID -local function ECS_ENTITY_T_HI(e: i53): i24 - return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_ENTITY_MASK else e -end - --- 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 + return e % ECS_ENTITY_MASK end -local function _STRIP_GENERATION(e: i53): i24 - return ECS_ENTITY_T_LO(e) +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 - return ECS_COMBINE(ECS_ENTITY_T_LO(pred), ECS_ENTITY_T_LO(obj)) + FLAGS_ADD(--[[isPair]] true) :: i53 + pred %= ECS_ENTITY_MASK + obj %= ECS_ENTITY_MASK + + return obj + (pred * ECS_ENTITY_MASK) + ECS_PAIR_OFFSET end -local function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record? +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 then - return nil - end if not r or r.dense == 0 then return nil @@ -183,7 +205,7 @@ local function entity_index_try_get_any(entity_index: EntityIndex, entity: numbe return r end -local function entity_index_try_get(entity_index: EntityIndex, entity: number): Record? +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 @@ -197,7 +219,7 @@ local function entity_index_try_get(entity_index: EntityIndex, entity: number): return r end -local function entity_index_try_get_fast(entity_index: EntityIndex, entity: number): Record? +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 @@ -207,49 +229,74 @@ local function entity_index_try_get_fast(entity_index: EntityIndex, entity: numb return r end -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 0 -end - -local function entity_index_is_alive(entity_index: EntityIndex, entity: number) +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_new_id(entity_index: EntityIndex): i53 +local function entity_index_get_alive(index: ecs_entity_index_t, entity: i53): i53? + local r = entity_index_try_get_any(index, entity) + if r then + return 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 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 - if alive_count ~= #dense_array then + 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 = entity_index.max_id + 1 + local id = 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 } :: Record + entity_index.sparse_array[id] = { dense = alive_count } :: ecs_record_t return id end --- 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)) +local function ecs_pair_first(world: ecs_world_t, e: i53) + local pred = ECS_PAIR_FIRST(e) + return ecs_get_alive(world, pred) end --- 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)) +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, archetype: Archetype) +local function query_match(query: ecs_query_data_t, + archetype: ecs_archetype_t) local records = archetype.records local with = query.filter_with @@ -271,7 +318,8 @@ local function query_match(query, archetype: Archetype) return true end -local function find_observers(world: World, event, component): { Observer }? +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 @@ -279,7 +327,13 @@ local function find_observers(world: World, event, component): { Observer }? return cache[component] :: any end -local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24) +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 @@ -333,21 +387,33 @@ local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: record2.row = src_row end -local function archetype_append(entity: number, archetype: Archetype): number +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: Record, archetype: Archetype): Record +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: EntityIndex, entity: i53, record: Record, to: Archetype) +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) @@ -360,7 +426,8 @@ local function hash(arr: { number }): string return table.concat(arr, "_") end -local function fetch(id, records: { number }, columns: { Column }, row: number): any +local function fetch(id: i53, records: { number }, + columns: { Column }, row: number): any local tr = records[id] if not tr then @@ -370,7 +437,8 @@ local function fetch(id, records: { number }, columns: { Column }, row: number): return columns[tr][row] end -local function world_get(world: World, entity: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any +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 @@ -400,25 +468,7 @@ local function world_get(world: World, entity: i53, a: i53, b: i53?, c: i53?, d: end end -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 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 @@ -434,7 +484,7 @@ local function world_has_one_inline(world: World, entity: number, id: i53): bool return records[id] ~= nil end -local function world_has(world: World, entity: number, ...: i53): boolean +local function world_has(world: ecs_world_t, entity: i53, ...: i53): boolean local record = entity_index_try_get_fast(world.entity_index, entity) if not record then return false @@ -456,7 +506,7 @@ local function world_has(world: World, entity: number, ...: i53): boolean return true end -local function world_target(world: World, entity: i53, relation: i24, index: number?): i24? +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 @@ -468,13 +518,9 @@ local function world_target(world: World, entity: i53, relation: i24, index: num return nil end - local idr = world.component_index[ECS_PAIR(relation, EcsWildcard)] - if not idr then - return nil - end + local r = ECS_PAIR(relation, EcsWildcard) - local archetype_id = archetype.id - local count = idr.counts[archetype.id] + local count = archetype.counts[r] if not count then return nil end @@ -483,15 +529,13 @@ local function world_target(world: World, entity: i53, relation: i24, index: num nth = nth + count + 1 end - local tr = idr.columns[archetype_id] - - nth = archetype.types[nth + tr] - + nth = archetype.types[nth + archetype.records[r]] if not nth then return nil end - return ecs_pair_second(world, nth) + return entity_index_get_alive(world.entity_index, + ECS_PAIR_SECOND(nth)) end local function ECS_ID_IS_WILDCARD(e: i53): boolean @@ -500,16 +544,23 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean return first == EcsWildcard or second == EcsWildcard end -local function id_record_ensure(world: World, id: number): IdRecord +local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t local component_index = world.component_index - local idr: IdRecord = component_index[id] + local entity_index = world.entity_index + local idr: ecs_id_record_t = 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 = ecs_pair_first(world, id) + 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) end local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) @@ -526,7 +577,7 @@ local function id_record_ensure(world: World, id: number): IdRecord 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, ecs_pair_second(world, id), EcsComponent) + is_tag = not world_has_one_inline(world, target, EcsComponent) end flags = bit32.bor( @@ -540,7 +591,7 @@ local function id_record_ensure(world: World, id: number): IdRecord idr = { size = 0, - columns = {}, + cache = {}, counts = {}, flags = flags, hooks = { @@ -557,15 +608,15 @@ local function id_record_ensure(world: World, id: number): IdRecord end local function archetype_append_to_records( - idr: IdRecord, - archetype: Archetype, - id: number, + 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.columns + local idr_columns = idr.cache local idr_counts = idr.counts local tr = idr_columns[archetype_id] if not tr then @@ -581,7 +632,7 @@ local function archetype_append_to_records( end end -local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype +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 @@ -591,7 +642,7 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) local records: { number } = {} local counts: {number} = {} - local archetype: Archetype = { + local archetype: ecs_archetype_t = { columns = columns, entities = {}, id = archetype_id, @@ -602,17 +653,16 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) add = {}, remove = {}, - refs = {} :: GraphEdge, + refs = {} :: ecs_graph_edge_t, } - 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) + 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) @@ -647,15 +697,15 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) return archetype end -local function world_entity(world: World): i53 +local function world_entity(world: ecs_world_t): i53 return entity_index_new_id(world.entity_index) end -local function world_parent(world: World, entity: i53) +local function world_parent(world: ecs_world_t, entity: i53) return world_target(world, entity, EcsChildOf, 0) end -local function archetype_ensure(world: World, id_types): Archetype +local function archetype_ensure(world: ecs_world_t, id_types): ecs_archetype_t if #id_types < 1 then return world.ROOT_ARCHETYPE end @@ -681,7 +731,7 @@ local function find_insert(id_types: { i53 }, toAdd: i53): number return #id_types + 1 end -local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype +local function find_archetype_with(world: ecs_world_t, node: ecs_archetype_t, id: i53): ecs_archetype_t 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 @@ -699,7 +749,11 @@ local function find_archetype_with(world: World, node: Archetype, id: i53): Arch return archetype_ensure(world, dst) end -local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype +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) if at == nil then @@ -712,23 +766,32 @@ local function find_archetype_without(world: World, node: Archetype, id: i53): A return archetype_ensure(world, dst) end -local function archetype_init_edge(archetype: Archetype, edge: GraphEdge, id: i53, to: Archetype) +local function archetype_init_edge( + archetype: ecs_archetype_t, + edge: ecs_graph_edge_t, + id: i53, + to: ecs_archetype_t +) edge.from = archetype edge.to = to edge.id = id end -local function archetype_ensure_edge(world, edges: GraphEdges, id): GraphEdge +local function archetype_ensure_edge( + world: ecs_world_t, + edges: ecs_graph_edges_t, + id: i53 +): ecs_graph_edge_t local edge = edges[id] if not edge then - edge = {} :: GraphEdge + edge = {} :: ecs_graph_edge_t edges[id] = edge end return edge end -local function init_edge_for_add(world, archetype: Archetype, edge: GraphEdge, id, to: Archetype) +local function init_edge_for_add(world, archetype: ecs_archetype_t, edge: ecs_graph_edge_t, id, to: ecs_archetype_t) archetype_init_edge(archetype, edge, id, to) archetype_ensure_edge(world, archetype.add, id) if archetype ~= to then @@ -745,7 +808,13 @@ local function init_edge_for_add(world, archetype: Archetype, edge: GraphEdge, i end end -local function init_edge_for_remove(world: World, archetype: Archetype, edge: GraphEdge, id: number, to: Archetype) +local function init_edge_for_remove( + world: ecs_world_t, + archetype: ecs_archetype_t, + edge: ecs_graph_edge_t, + id: number, + to: ecs_archetype_t +) archetype_init_edge(archetype, edge, id, to) archetype_ensure_edge(world, archetype.remove, id) if archetype ~= to then @@ -762,19 +831,33 @@ local function init_edge_for_remove(world: World, archetype: Archetype, edge: Gr end end -local function create_edge_for_add(world: World, node: Archetype, edge: GraphEdge, id: i53): Archetype +local function create_edge_for_add( + world: ecs_world_t, + node: ecs_archetype_t, + edge: ecs_graph_edge_t, + id: i53 +): ecs_archetype_t 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 function create_edge_for_remove( + world: ecs_world_t, + node: ecs_archetype_t, + edge: ecs_graph_edge_t, + id: i53 +): ecs_archetype_t local to = find_archetype_without(world, node, id) init_edge_for_remove(world, node, edge, id, to) return to end -local function archetype_traverse_add(world: World, id: i53, from: Archetype): Archetype +local function archetype_traverse_add( + world: ecs_world_t, + id: i53, + from: ecs_archetype_t +): ecs_archetype_t from = from or world.ROOT_ARCHETYPE local edge = archetype_ensure_edge(world, from.add, id) @@ -783,10 +866,14 @@ local function archetype_traverse_add(world: World, id: i53, from: Archetype): A to = create_edge_for_add(world, from, edge, id) end - return to :: Archetype + return to :: ecs_archetype_t end -local function archetype_traverse_remove(world: World, id: i53, from: Archetype): Archetype +local function archetype_traverse_remove( + world: ecs_world_t, + id: i53, + from: ecs_archetype_t +): ecs_archetype_t from = from or world.ROOT_ARCHETYPE local edge = archetype_ensure_edge(world, from.remove, id) @@ -796,10 +883,14 @@ local function archetype_traverse_remove(world: World, id: i53, from: Archetype) to = create_edge_for_remove(world, from, edge, id) end - return to :: Archetype + return to :: ecs_archetype_t end -local function world_add(world: World, entity: i53, id: i53): () +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 @@ -827,15 +918,15 @@ local function world_add(world: World, entity: i53, id: i53): () end end -local function world_set(world: World, entity: i53, id: i53, data: unknown): () +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: Archetype = record.archetype - local to: Archetype = archetype_traverse_add(world, id, from) + 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 @@ -863,16 +954,16 @@ local function world_set(world: World, 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) + end + local on_set = idr_hooks.on_set if on_set then on_set(entity, data) @@ -891,7 +982,7 @@ local function world_component(world: World): i53 return id end -local function world_remove(world: World, entity: i53, id: i53) +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 @@ -902,15 +993,16 @@ local function world_remove(world: World, entity: i53, id: i53) if not from then return end - local to = archetype_traverse_remove(world, id, from) - if from ~= to then + 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) end + local to = archetype_traverse_remove(world, id, record.archetype) + entity_move(entity_index, entity, record, to) end end @@ -932,7 +1024,7 @@ local function archetype_fast_delete(columns: { Column }, column_count: number, end end -local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?) +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 @@ -971,7 +1063,7 @@ local function archetype_delete(world: World, archetype: Archetype, row: number, end end -local function world_clear(world: World, entity: i53) +local function world_clear(world: ecs_world_t, entity: i53) --TODO: use sparse_get (stashed) local record = entity_index_try_get(world.entity_index, entity) if not record then @@ -981,6 +1073,22 @@ local function world_clear(world: World, entity: i53) local archetype = record.archetype local row = record.row + local idr = world.component_index[entity] + if idr then + local count = 0 + local queue = {} + for archetype_id in idr.cache do + local idr_archetype = world.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 archetype then -- In the future should have a destruct mode for -- deleting archetypes themselves. Maybe requires recycling @@ -991,7 +1099,7 @@ local function world_clear(world: World, entity: i53) record.row = nil :: any end -local function archetype_disconnect_edge(edge: GraphEdge) +local function archetype_disconnect_edge(edge: ecs_graph_edge_t) local edge_next = edge.next local edge_prev = edge.prev if edge_next then @@ -1002,14 +1110,14 @@ local function archetype_disconnect_edge(edge: GraphEdge) end end -local function archetype_remove_edge(edges: Map, id: i53, edge: GraphEdge) +local function archetype_remove_edge(edges: ecs_graph_edges_t, id: i53, edge: ecs_graph_edge_t) 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 function archetype_clear_edges(archetype: ecs_archetype_t) + local add: ecs_graph_edges_t = archetype.add + local remove: ecs_graph_edges_t = archetype.remove local node_refs = archetype.refs for id, edge in add do archetype_disconnect_edge(edge) @@ -1022,7 +1130,7 @@ local function archetype_clear_edges(archetype: Archetype) local cur = node_refs.next while cur do - local edge = cur :: GraphEdge + local edge = cur :: ecs_graph_edge_t local next_edge = edge.next archetype_remove_edge(edge.from.add, edge.id, edge) cur = next_edge @@ -1030,7 +1138,7 @@ local function archetype_clear_edges(archetype: Archetype) cur = node_refs.prev while cur do - local edge: GraphEdge = cur + local edge: ecs_graph_edge_t = cur local next_edge = edge.prev archetype_remove_edge(edge.from.remove, edge.id, edge) cur = next_edge @@ -1040,7 +1148,7 @@ local function archetype_clear_edges(archetype: Archetype) node_refs.prev = nil end -local function archetype_destroy(world: World, archetype: Archetype) +local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t) if archetype == world.ROOT_ARCHETYPE then return end @@ -1066,7 +1174,7 @@ local function archetype_destroy(world: World, archetype: Archetype) for id in records do local idr = component_index[id] - idr.columns[archetype_id] = nil :: any + idr.cache[archetype_id] = nil :: any idr.counts[archetype_id] = nil idr.size -= 1 records[id] = nil :: any @@ -1076,7 +1184,7 @@ local function archetype_destroy(world: World, archetype: Archetype) end end -local function world_cleanup(world: World) +local function world_cleanup(world: ecs_world_t) local archetypes = world.archetypes for _, archetype in archetypes do @@ -1085,7 +1193,7 @@ local function world_cleanup(world: World) end end - local new_archetypes = table.create(#archetypes) :: { Archetype } + local new_archetypes = table.create(#archetypes) :: { ecs_archetype_t } local new_archetype_map = {} for index, archetype in archetypes do @@ -1097,155 +1205,143 @@ local function world_cleanup(world: World) world.archetype_index = new_archetype_map 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 - - 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, destruct) - end - - 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.columns 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.columns 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 - - local sparse_array = entity_index.sparse_array - local dense_array = entity_index.dense_array - - if idr_t then - for archetype_id in idr_t.columns do - local children = {} - local idr_t_archetype = archetypes[archetype_id] - - local idr_t_types = idr_t_archetype.types - - for _, child in idr_t_archetype.entities do - table.insert(children, child) - end - - local n = #children - - 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 - 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 = n, 1, -1 do - world_delete(world, children[i]) - end - break - else - local on_remove = id_record.hooks.on_remove - local to = archetype_traverse_remove(world, id, idr_t_archetype) - local empty = #to.types == 0 - for i = n, 1, -1 do - local child = children[i] - if on_remove then - on_remove(child) - end - local r = sparse_array[ECS_ENTITY_T_LO(child)] - if not empty then - entity_move(entity_index, child, r, to) - end - end - end - end - end - - archetype_destroy(world, idr_t_archetype) - 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) +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 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 + + 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 + + local dense_array = entity_index.dense_array + + if idr_t then + local children + local ids + 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 = {} + 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 id in ids do + for _, child in children do + world_remove(world, child, id) + end + end + end + + for archetype_id in archetype_ids do + 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) :: 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: World, entity): boolean +local function world_contains(world: ecs_world_t, entity): boolean return entity_index_is_alive(world.entity_index, entity) end local function NOOP() end -local function ARM(query, ...) - return query -end - -local EMPTY_LIST = {} -local EMPTY_QUERY = { - __iter = function() - return NOOP - end, - iter = function() - return NOOP - end, - with = ARM, - without = ARM, - archetypes = function() - return EMPTY_LIST - end, -} - -setmetatable(EMPTY_QUERY, EMPTY_QUERY) - -type QueryInner = { +export type QueryInner = { compatible_archetypes: { Archetype }, ids: { i53 }, filter_with: { i53 }, @@ -1254,7 +1350,9 @@ type QueryInner = { world: World, } -local function query_iter_init(query: QueryInner): () -> (number, ...any) + + +local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) local world_query_iter_next local compatible_archetypes = query.compatible_archetypes @@ -1599,7 +1697,7 @@ local function query_iter(query): () -> (number, ...any) return query_next end -local function query_without(query: QueryInner, ...: i53) +local function query_without(query: ecs_query_data_t, ...: i53) local without = { ... } query.filter_without = without local compatible_archetypes = query.compatible_archetypes @@ -1629,7 +1727,7 @@ local function query_without(query: QueryInner, ...: i53) return query :: any end -local function query_with(query: QueryInner, ...: i53) +local function query_with(query: ecs_query_data_t, ...: i53) local compatible_archetypes = query.compatible_archetypes local with = { ... } query.filter_with = with @@ -1667,7 +1765,7 @@ local function query_archetypes(query) return query.compatible_archetypes end -local function query_cached(query: QueryInner) +local function query_cached(query: ecs_query_data_t) local with = query.filter_with local ids = query.ids if with then @@ -1687,14 +1785,14 @@ local function query_cached(query: QueryInner) local columns: { Column } local entities: { number } local i: number - local archetype: Archetype + local archetype: ecs_archetype_t local records: { number } local archetypes = query.compatible_archetypes - local world = query.world :: { observable: Observable } + 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 :: Observable + local observable = world.observable :: ecs_observable_t local on_create_action = observable[EcsOnArchetypeCreate] if not on_create_action then on_create_action = {} @@ -1722,7 +1820,7 @@ local function query_cached(query: QueryInner) 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 @@ -2088,7 +2186,7 @@ Query.with = query_with Query.archetypes = query_archetypes Query.cached = query_cached -local function world_query(world: World, ...) +local function world_query(world: ecs_world_t, ...) local compatible_archetypes = {} local length = 0 @@ -2096,7 +2194,7 @@ local function world_query(world: World, ...) local archetypes = world.archetypes - local idr: IdRecord? + local idr: ecs_id_record_t? local component_index = world.component_index local q = setmetatable({ @@ -2120,7 +2218,7 @@ local function world_query(world: World, ...) return q end - for archetype_id in idr.columns do + for archetype_id in idr.cache do local compatibleArchetype = archetypes[archetype_id] if #compatibleArchetype.entities == 0 then continue @@ -2148,15 +2246,15 @@ local function world_query(world: World, ...) return q end -local function world_each(world: World, id): () -> () +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_columns = idr.columns + local idr_cache = idr.cache local archetypes = world.archetypes - local archetype_id = next(idr_columns, nil) :: number + local archetype_id = next(idr_cache, nil) :: number local archetype = archetypes[archetype_id] if not archetype then return NOOP @@ -2168,7 +2266,7 @@ local function world_each(world: World, id): () -> () return function(): any local entity = entities[row] while not entity do - archetype_id = next(idr_columns, archetype_id) :: number + archetype_id = next(idr_cache, archetype_id) :: number if not archetype_id then return end @@ -2182,10 +2280,36 @@ local function world_each(world: World, id): () -> () end end -local function world_children(world, parent) +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) -> ())?, + 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 @@ -2206,122 +2330,13 @@ World.cleanup = world_cleanup World.each = world_each World.children = world_children -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 }, +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_index = {} :: { [string]: Archetype }, archetypes = {} :: Archetypes, @@ -2372,23 +2387,14 @@ function World.new() return self end -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...) +World.new = world_new +export type Entity = { __T: T } +export type Id = { __T: T } +export type Pair = Id

, second: Id) -> Pair, -- Inwards facing API for testing ECS_ID = ECS_ENTITY_T_LO, @@ -2524,6 +2524,8 @@ return { ECS_GENERATION = ECS_GENERATION, ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD, + ECS_ID_DELETE = ECS_ID_DELETE, + IS_PAIR = ECS_IS_PAIR, pair_first = ecs_pair_first, pair_second = ecs_pair_second, diff --git a/pesde-rbx.toml b/pesde-rbx.toml index f1b1745..62662ad 100644 --- a/pesde-rbx.toml +++ b/pesde-rbx.toml @@ -11,7 +11,7 @@ includes = [ license = "MIT" name = "marked/jecs" repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.5.5" +version = "0.6.0-rc.1" [indices] default = "https://github.com/pesde-pkg/index" diff --git a/pesde.lock b/pesde.lock index a1e2363..30c36fb 100644 --- a/pesde.lock +++ b/pesde.lock @@ -2,5 +2,5 @@ # It is not intended for manual editing. format = 1 name = "marked/jecs" -version = "0.5.5" +version = "0.6.0-rc.1" target = "luau" diff --git a/pesde.toml b/pesde.toml index ccfcc7c..e533800 100644 --- a/pesde.toml +++ b/pesde.toml @@ -11,7 +11,7 @@ includes = [ license = "MIT" name = "marked/jecs" repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.5.5" +version = "0.6.0-rc.1" [indices] default = "https://github.com/pesde-pkg/index" From 753a73882aed2ee071f2525d611ea7c4a5d0a5f3 Mon Sep 17 00:00:00 2001 From: marked Date: Sun, 11 May 2025 02:11:44 +0200 Subject: [PATCH 08/10] Sync to upstream Jecs 0.6.0 (#11) Reviewed-on: https://git.devmarked.win/marked/jecs-pesde/pulls/11 --- .luaurc | 1 + CHANGELOG.md | 63 ++-- jecs.luau | 868 +++++++++++++++++++++++++++++-------------------- pesde-rbx.toml | 2 +- pesde.lock | 2 +- pesde.toml | 2 +- 6 files changed, 554 insertions(+), 384 deletions(-) diff --git a/.luaurc b/.luaurc index d1ae244..f856eba 100644 --- a/.luaurc +++ b/.luaurc @@ -4,6 +4,7 @@ "testkit": "tools/testkit", "mirror": "mirror", "tools": "tools", + "addons": "addons" }, "languageMode": "strict" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 52f87cc..fa1d870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,30 +10,53 @@ The format is based on [Keep a Changelog][kac], and this project adheres to ## [Unreleased] +## [0.6.0] - 2025-05-10 + - `[world]`: - - 16% faster `world:get` - - `world:has` no longer typechecks components after the 8th one. -- `[typescript]` + - 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. - - Fixed Entity type to default to `undefined | unknown` instead of just `undefined` +## [0.5.0] - 2024-12-26 +- `[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]`: - - 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 + - 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. ## [0.3.2] - 2024-10-01 diff --git a/jecs.luau b/jecs.luau index 69a4ad7..f705f0e 100644 --- a/jecs.luau +++ b/jecs.luau @@ -13,22 +13,6 @@ type Column = { any } type Map = { [K]: V } -type ecs_graph_edge_t = { - from: ecs_archetype_t, - to: ecs_archetype_t?, - id: number, - prev: ecs_graph_edge_t?, - next: ecs_graph_edge_t?, -} - -type ecs_graph_edges_t = Map - -type ecs_graph_node_t = { - add: ecs_graph_edges_t, - remove: ecs_graph_edges_t, - refs: ecs_graph_edge_t, -} - type ecs_archetype_t = { id: number, types: Ty, @@ -37,7 +21,7 @@ type ecs_archetype_t = { columns: { Column }, records: { [i53]: number }, counts: { [i53]: number }, -} & ecs_graph_node_t +} export type Archetype = { id: number, @@ -61,9 +45,9 @@ type ecs_id_record_t = { flags: number, size: number, hooks: { - on_add: ((entity: i53) -> ())?, - on_set: ((entity: i53, data: any) -> ())?, - on_remove: ((entity: i53) -> ())?, + on_add: ((entity: i53, id: i53, data: any?) -> ())?, + on_change: ((entity: i53, id: i53, data: any) -> ())?, + on_remove: ((entity: i53, id: i53) -> ())?, }, } @@ -78,6 +62,8 @@ type ecs_entity_index_t = { sparse_array: Map, alive_count: number, max_id: number, + range_begin: number?, + range_end: number? } type ecs_query_data_t = { @@ -97,6 +83,7 @@ type ecs_observer_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, @@ -111,7 +98,7 @@ 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 EcsOnSet = HI_COMPONENT_ID + 3 +local EcsOnChange = HI_COMPONENT_ID + 3 local EcsWildcard = HI_COMPONENT_ID + 4 local EcsChildOf = HI_COMPONENT_ID + 5 local EcsComponent = HI_COMPONENT_ID + 6 @@ -124,23 +111,60 @@ local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12 local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13 local EcsRest = HI_COMPONENT_ID + 14 -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 +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({}) +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 @@ -169,6 +193,10 @@ 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 @@ -233,10 +261,10 @@ local function entity_index_is_alive(entity_index: ecs_entity_index_t, entity: i 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(entity_index: ecs_entity_index_t, entity: i53): i53? + local r = entity_index_try_get_any(entity_index, entity) if r then - return index.dense_array[r.dense] + return entity_index.dense_array[r.dense] end return nil end @@ -264,11 +292,15 @@ local function ecs_get_alive(world, entity) 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 + + if alive_count < max_id then alive_count += 1 entity_index.alive_count = alive_count local id = dense_array[alive_count] @@ -276,11 +308,14 @@ local function entity_index_new_id(entity_index: ecs_entity_index_t): i53 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 - entity_index.sparse_array[id] = { dense = alive_count } :: ecs_record_t + sparse_array[id] = { dense = alive_count } :: ecs_record_t return id end @@ -484,7 +519,17 @@ 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 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 @@ -497,13 +542,11 @@ local function world_has(world: ecs_world_t, entity: i53, ...: i53): boolean local records = archetype.records - for i = 1, select("#", ...) do - if not records[select(i, ...)] then - return false - end - end - - return true + 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? @@ -547,63 +590,64 @@ 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] + local idr: ecs_id_record_t? = 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) - 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_set, on_remove = world_get(world, relation, EcsOnAdd, EcsOnSet, 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 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 - ) - - idr = { - size = 0, - cache = {}, - counts = {}, - flags = flags, - hooks = { - on_add = on_add, - on_set = on_set, - on_remove = on_remove, - }, - } - - component_index[id] = idr + 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 @@ -640,7 +684,7 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: local columns = (table.create(length) :: any) :: { Column } local records: { number } = {} - local counts: {number} = {} + local counts: { number } = {} local archetype: ecs_archetype_t = { columns = columns, @@ -650,10 +694,6 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: counts = counts, type = ty, types = id_types, - - add = {}, - remove = {}, - refs = {} :: ecs_graph_edge_t, } for i, component_id in id_types do @@ -693,12 +733,98 @@ 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] = {} :: Map return archetype end -local function world_entity(world: ecs_world_t): i53 - return entity_index_new_id(world.entity_index) +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) @@ -722,6 +848,7 @@ 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 @@ -731,24 +858,6 @@ local function find_insert(id_types: { i53 }, toAdd: i53): number return #id_types + 1 end -local function find_archetype_with(world: ecs_world_t, node: ecs_archetype_t, id: i53): ecs_archetype_t - 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: ecs_world_t, node: ecs_archetype_t, @@ -756,9 +865,6 @@ local function find_archetype_without( ): ecs_archetype_t 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) @@ -766,124 +872,65 @@ local function find_archetype_without( return archetype_ensure(world, dst) end -local function archetype_init_edge( - archetype: ecs_archetype_t, - edge: ecs_graph_edge_t, - id: i53, - to: ecs_archetype_t -) - edge.from = archetype - edge.to = to - edge.id = id -end - -local function archetype_ensure_edge( - world: ecs_world_t, - edges: ecs_graph_edges_t, - id: i53 -): ecs_graph_edge_t - local edge = edges[id] - if not edge then - edge = {} :: ecs_graph_edge_t - edges[id] = edge - end - - return edge -end - -local function init_edge_for_add(world, archetype: ecs_archetype_t, edge: ecs_graph_edge_t, id, to: ecs_archetype_t) - 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: ecs_world_t, - archetype: ecs_archetype_t, - edge: ecs_graph_edge_t, - id: number, - to: ecs_archetype_t -) - 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: ecs_world_t, - node: ecs_archetype_t, - edge: ecs_graph_edge_t, - id: i53 -): ecs_archetype_t - 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: ecs_world_t, node: ecs_archetype_t, - edge: ecs_graph_edge_t, + edge: Map, id: i53 ): ecs_archetype_t local to = find_archetype_without(world, node, id) - init_edge_for_remove(world, node, edge, id, to) + 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_add( - world: ecs_world_t, - id: i53, - from: ecs_archetype_t -): ecs_archetype_t - from = from or world.ROOT_ARCHETYPE - local edge = archetype_ensure_edge(world, from.add, id) - - local to = edge.to - if not to then - to = create_edge_for_add(world, from, edge, id) - end - - return to :: ecs_archetype_t -end - local function archetype_traverse_remove( world: ecs_world_t, id: i53, from: ecs_archetype_t ): ecs_archetype_t - from = from or world.ROOT_ARCHETYPE + local edges = world.archetype_edges + local edge = edges[from.id] - 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) + 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 :: ecs_archetype_t + 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( @@ -914,7 +961,7 @@ local function world_add( local on_add = idr.hooks.on_add if on_add then - on_add(entity) + on_add(entity, id) end end @@ -931,14 +978,15 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown 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 tr = (to :: ecs_archetype_t).records[id] local column = from.columns[tr] column[record.row] = data - local on_set = idr_hooks.on_set - if on_set then - on_set(entity, 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 @@ -961,12 +1009,7 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown local on_add = idr_hooks.on_add if on_add then - on_add(entity) - end - - local on_set = idr_hooks.on_set - if on_set then - on_set(entity, data) + on_add(entity, id, data) end end @@ -998,7 +1041,7 @@ local function world_remove(world: ecs_world_t, entity: i53, id: i53) local idr = world.component_index[id] local on_remove = idr.hooks.on_remove if on_remove then - on_remove(entity) + on_remove(entity, id) end local to = archetype_traverse_remove(world, id, record.archetype) @@ -1050,7 +1093,7 @@ local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, local idr = component_index[id] local on_remove = idr.hooks.on_remove if on_remove then - on_remove(delete) + on_remove(delete, id) end end @@ -1064,21 +1107,20 @@ local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, end local function world_clear(world: ecs_world_t, entity: i53) - --TODO: use sparse_get (stashed) - local record = entity_index_try_get(world.entity_index, entity) - if not record then - return - end + 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] - local archetype = record.archetype - local row = record.row - - local idr = world.component_index[entity] if idr then local count = 0 local queue = {} for archetype_id in idr.cache do - local idr_archetype = world.archetypes[archetype_id] + local idr_archetype = archetypes[archetype_id] local entities = idr_archetype.entities local n = #entities count += n @@ -1089,63 +1131,79 @@ local function world_clear(world: ecs_world_t, entity: i53) 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) + 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 - record.archetype = nil :: any - record.row = nil :: any -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 -local function archetype_disconnect_edge(edge: ecs_graph_edge_t) - local edge_next = edge.next - local edge_prev = edge.prev - if edge_next then - edge_next.prev = edge_prev + for _, e in queue do + for id in ids do + world_remove(world, e, id) + end + end end - if edge_prev then - edge_prev.next = edge_next - end -end - -local function archetype_remove_edge(edges: ecs_graph_edges_t, id: i53, edge: ecs_graph_edge_t) - archetype_disconnect_edge(edge) - edges[id] = nil :: any -end - -local function archetype_clear_edges(archetype: ecs_archetype_t) - local add: ecs_graph_edges_t = archetype.add - local remove: ecs_graph_edges_t = 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 :: ecs_graph_edge_t - 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: ecs_graph_edge_t = 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: ecs_world_t, archetype: ecs_archetype_t) @@ -1154,7 +1212,12 @@ local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t) end local component_index = world.component_index - archetype_clear_edges(archetype) + 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 @@ -1225,8 +1288,11 @@ local function world_delete(world: ecs_world_t, entity: i53) 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 @@ -1256,11 +1322,10 @@ local function world_delete(world: ecs_world_t, entity: i53) end end - local dense_array = entity_index.dense_array - if idr_t then - local children - local ids + local children: { i53 } + local ids: Map + local count = 0 local archetype_ids = idr_t.cache for archetype_id in archetype_ids do @@ -1288,8 +1353,8 @@ local function world_delete(world: ecs_world_t, entity: i53) end break else - if not ids then - ids = {} + if not ids then + ids = {} :: { [i53]: boolean } end ids[id] = true removal_queued = true @@ -1300,7 +1365,7 @@ local function world_delete(world: ecs_world_t, entity: i53) continue end if not children then - children = {} + children = {} :: { i53 } end local n = #entities table.move(entities, 1, n, count + 1, children) @@ -1308,8 +1373,8 @@ local function world_delete(world: ecs_world_t, entity: i53) end if ids then - for id in ids do - for _, child in children do + for _, child in children do + for id in ids do world_remove(world, child, id) end end @@ -1320,19 +1385,68 @@ local function world_delete(world: ecs_world_t, entity: i53) 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 + 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 - 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 + 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 = index_of_last_alive_entity + record.dense = i_swap - dense_array[index_of_deleted_entity] = last_alive_entity - dense_array[index_of_last_alive_entity] = ECS_GENERATION_INC(entity) + 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 @@ -1350,8 +1464,6 @@ export type QueryInner = { world: World, } - - local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) local world_query_iter_next @@ -1795,7 +1907,7 @@ local function query_cached(query: ecs_query_data_t) local observable = world.observable :: ecs_observable_t local on_create_action = observable[EcsOnArchetypeCreate] if not on_create_action then - on_create_action = {} + on_create_action = {} :: Map observable[EcsOnArchetypeCreate] = on_create_action end local query_cache_on_create = on_create_action[A] @@ -1806,7 +1918,7 @@ local function query_cached(query: ecs_query_data_t) local on_delete_action = observable[EcsOnArchetypeDelete] if not on_delete_action then - on_delete_action = {} + on_delete_action = {} :: Map observable[EcsOnArchetypeDelete] = on_delete_action end local query_cache_on_delete = on_delete_action[A] @@ -2209,12 +2321,12 @@ local function world_query(world: ecs_world_t, ...) return q end - if idr == nil or map.size < idr.size then + if idr == nil or (map.size :: number) < (idr.size :: number) then idr = map end end - if not idr then + if idr == nil then return q end @@ -2295,9 +2407,9 @@ export type ComponentRecord = { flags: number, size: number, hooks: { - on_add: ((entity: Entity) -> ())?, - on_set: ((entity: Entity, data: any) -> ())?, - on_remove: ((entity: Entity) -> ())?, + 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 @@ -2308,6 +2420,8 @@ export type EntityIndex = { sparse_array: Map, alive_count: number, max_id: number, + range_begin: number?, + range_end: number? } local World = {} @@ -2326,9 +2440,11 @@ 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 = { @@ -2338,6 +2454,8 @@ local function world_new() max_id = 0, } :: ecs_entity_index_t local self = setmetatable({ + archetype_edges = {}, + archetype_index = {} :: { [string]: Archetype }, archetypes = {} :: Archetypes, component_index = {} :: ComponentIndex, @@ -2345,7 +2463,7 @@ local function world_new() ROOT_ARCHETYPE = (nil :: any) :: Archetype, max_archetype_id = 0, - max_component_id = 0, + max_component_id = ecs_max_component_id, observable = {} :: Observable, }, World) :: any @@ -2363,7 +2481,7 @@ local function world_new() end world_add(self, EcsName, EcsComponent) - world_add(self, EcsOnSet, EcsComponent) + world_add(self, EcsOnChange, EcsComponent) world_add(self, EcsOnAdd, EcsComponent) world_add(self, EcsOnRemove, EcsComponent) world_add(self, EcsWildcard, EcsComponent) @@ -2371,7 +2489,7 @@ local function world_new() world_set(self, EcsOnAdd, EcsName, "jecs.OnAdd") world_set(self, EcsOnRemove, EcsName, "jecs.OnRemove") - world_set(self, EcsOnSet, EcsName, "jecs.OnSet") + 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") @@ -2384,27 +2502,44 @@ local function world_new() 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 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, -})) & { - iter: Iter, - with: (self: Query, ...Id) -> Query, - without: (self: Query, ...Id) -> Query, - archetypes: (self: Query) -> { Archetype }, - cached: (self: Query) -> Query, -} +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) -> (), @@ -2431,6 +2566,9 @@ export type World = { 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. @@ -2438,20 +2576,20 @@ export type World = { 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: 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) -> (), + add: (self: World, id: Entity, component: Id) -> (), --- Assigns a value to a component on the given entity set: (self: World, id: Entity, component: Id, data: T) -> (), cleanup: (self: World) -> (), -- Clears an entity from the world - clear: (self: World, id: Entity) -> (), + clear: (self: World, id: Id) -> (), --- 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?)) @@ -2459,7 +2597,10 @@ export type World = { & (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: ((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, @@ -2467,9 +2608,12 @@ export type World = { --- Checks if the world contains the given entity contains:(self: World, entity: Entity) -> boolean, - each: (self: World, id: Id) -> () -> Entity, + --- Checks if the entity exists + exists: (self: World, entity: Entity) -> boolean, - children: (self: World, id: Id) -> () -> Entity, + 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) @@ -2497,14 +2641,19 @@ export type World = { -- return first -- end -- end +-- return { World = World :: { new: () -> World }, - 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) -> ()>, - OnSet = EcsOnSet :: Entity<(entity: Entity, data: any) -> ()>, + OnChange = EcsOnChange :: Entity<(entity: Entity, data: any) -> ()>, ChildOf = EcsChildOf :: Entity, Component = EcsComponent :: Entity, Wildcard = EcsWildcard :: Entity, @@ -2523,12 +2672,14 @@ return { 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, - pair_first = ecs_pair_first, - pair_second = ecs_pair_second, + 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, @@ -2538,11 +2689,6 @@ 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/pesde-rbx.toml b/pesde-rbx.toml index 62662ad..d0520fc 100644 --- a/pesde-rbx.toml +++ b/pesde-rbx.toml @@ -11,7 +11,7 @@ includes = [ license = "MIT" name = "marked/jecs" repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.6.0-rc.1" +version = "0.6.0" [indices] default = "https://github.com/pesde-pkg/index" diff --git a/pesde.lock b/pesde.lock index 30c36fb..91f9895 100644 --- a/pesde.lock +++ b/pesde.lock @@ -2,5 +2,5 @@ # It is not intended for manual editing. format = 1 name = "marked/jecs" -version = "0.6.0-rc.1" +version = "0.6.0" target = "luau" diff --git a/pesde.toml b/pesde.toml index e533800..ecf59dc 100644 --- a/pesde.toml +++ b/pesde.toml @@ -11,7 +11,7 @@ includes = [ license = "MIT" name = "marked/jecs" repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.6.0-rc.1" +version = "0.6.0" [indices] default = "https://github.com/pesde-pkg/index" From b69adecc1de16d5547104ff28736ffbb0261451a Mon Sep 17 00:00:00 2001 From: marked Date: Fri, 13 Jun 2025 02:11:47 +0200 Subject: [PATCH 09/10] Sync to upstream Jecs 0.6.1 (#12) Reviewed-on: https://git.devmarked.win/marked/jecs-pesde/pulls/12 --- .luaurc | 20 +-- CHANGELOG.md | 361 +++++++++++++++++++++---------------------------- README.md | 21 ++- jecs.luau | 129 +++++++++--------- pesde-rbx.toml | 2 +- pesde.lock | 2 +- pesde.toml | 2 +- 7 files changed, 254 insertions(+), 283 deletions(-) diff --git a/.luaurc b/.luaurc index f856eba..1d36832 100644 --- a/.luaurc +++ b/.luaurc @@ -1,10 +1,10 @@ -{ - "aliases": { - "jecs": "jecs", - "testkit": "tools/testkit", - "mirror": "mirror", - "tools": "tools", - "addons": "addons" - }, - "languageMode": "strict" -} +{ + "aliases": { + "jecs": "jecs", + "testkit": "tools/testkit", + "mirror": "mirror", + "tools": "tools", + "addons": "addons" + }, + "languageMode": "strict" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index fa1d870..28a4d81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,228 +1,177 @@ -# Jecs Changelog +# Changelog -All notable changes to this project will be documented in this file. +## Unreleased -The format is based on [Keep a Changelog][kac], and this project adheres to -[Semantic Versioning][semver]. - -[kac]: https://keepachangelog.com/en/1.1.0/ -[semver]: https://semver.org/spec/v2.0.0.html - -## [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. - -## [0.5.0] - 2024-12-26 - -- `[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. - -## [0.3.2] - 2024-10-01 - -- `[world]`: - - Changed `world:cleanup` to traverse a header type for graph edges. (Edit) - - Fixed a regression that occurred when you call `world:set` following a `world:remove` using the same component - - Remove explicit error in JECS_DEBUG for `world:target` when not applying an index parameter -- `[typescript]` : - - Fixed `world.set` with NoInfer - -## [0.3.1] - 2024-10-01 - -- `[world]`: - - Added an index parameter to `world:target` - - Added a way to change the components limit via `_G.JECS_HI_COMPONENT_ID` - - Set it to whatever number you want but try to make it as close to the number of components you will use as possible - - Make sure to set this before importing jecs or else it will not work - - Added debug mode, enable via setting `_G.JECS_DEBUG` to true - - Make sure to set this before importing jecs or else it will not work - - Added `world:cleanup` which is called to cleanup empty archetypes manually - - Changed `world:delete` to delete archetypes that are dependent on the passed entity - - Changed `world:delete` to delete entity's children before the entity to prevent cycles -- `[query]`: - - Fixed the iterator to not drain by default -- `[typescript]` - - Fixed entry point of the package.json file to be `src` rather than `src/init` - - Fixed `query.next` returning a query object whereas it would be expected to return a tuple containing the entity and the corresponding component values - - Exported `query.archetypes` - - Changed `pair` to return a number instead of an entity - - Preventing direct usage of a pair as an entity while still allowing it to be used as a component - - Exported built-in components `ChildOf` and `Name` - - Exported `world.parent` - -## [0.2.10] - 2024-09-07 - -- `[world]`: - - Improved performance for hooks - - Changed `world:set` to be idempotent when setting tags -- `[traits]`: - - Added cleanup condition `jecs.OnDelete` for when the entity or component is deleted - - Added cleanup action `jecs.Remove` which removes instances of the specified (component) id from all entities - - This is the default cleanup action - - Added component trait `jecs.Tag` which allows for zero-cost components used as tags - - Setting data to a component with this trait will do nothing -- `[luau]`: - - Exported `world:contains()` - - Exported `query:drain()` - - Exported `Query` - - Improved types for the hook `OnAdd`, `OnSet`, `OnRemove` - - Changed functions to accept any ID including pairs in type parameters - - Applies to `world:add()`, `world:set()`, `world:remove()`, `world:get()`, `world:has()` and `world:query()` - - New exported type `Id = Entity | Pair` - - Changed `world:contains()` to return a `boolean` instead of an entity which may or may not exist - - Fixed `world:has()` to take the correct parameters - -## [0.2.2] - 2024-07-07 - -### Added - -- Added `query:replace(function(...T) return ...U end)` for replacing components in place - - Method is fast pathed to replace the data to the components for each corresponding entity +## 0.6.1 ### Changed - -- Iterator now goes backwards instead to prevent common cases of iterator invalidation - -## [0.2.1] - 2024-07-06 - -### Added - -- Added `jecs.Component` built-in component which will be added to ids created with `world:component()`. - - Used to find every component id with `query(jecs.Component) - -## [0.2.0] - 2024-07-03 - -### Added - -- Added `world:parent(entity)` and `jecs.ChildOf` respectively as first class citizen for building parent-child relationships. - - Give a parent to an entity with `world:add($source, pair(ChildOf, $target))` - - Use `world:parent(entity)` to find the target of the relationship -- Added user-facing Luau types - -### Changed - -- Improved iteration speeds 20-40% by manually indexing rather than using `next()` :scream: - -## [0.1.1] - 2024-05-19 - -### Added - -- Added `world:clear(entity)` for removing the components to the corresponding entity -- Added Typescript Types - -## [0.1.0] - 2024-05-13 - -### Changed - -- Optimized iterator - -## [0.1.0-rc.6] - 2024-05-13 - -### Added - -- Added a `jecs.Wildcard` term - - it lets you query any partially matched pairs - -## [0.1.0-rc.5] - 2024-05-10 - -### Added - -- Added Entity relationships for creating logical connections between entities -- Added `world:__iter method` which allows for iteration over the whole world to get every entity - - used for reconciling whole worlds such as via replication, saving/loading, etc -- Added `world:add(entity, component)` which adds a component to the entity - - it is an idempotent function, so calling it twice and in any order should be fine +- Entity types now unions with numbers should allow for easier time casting while not causing regressing previous behaviours ### Fixed +- Fixed a critical bug with `(*, R)` pairs not being removed when `R` is deleted -- Fixed component overriding when in disorder - - Previously setting the components in different order results in it overriding component data because it incorrectly mapped the index of the column. So it took the index from the source archetype rather than the destination archetype - -## [0.0.0-prototype.rc.3] - 2024-05-01 +## 0.6.0 ### Added - -- Added observers -- Added an arm to query `query:without()` for chaining invariants. +- `World:range` to restrict entity range to allow for e.g. reserving ids `1000..5000` for clients and everything above that (5000+) for entities from the server. This makes it possible to receive ids from a server that don't have to be mapped to local ids. +- `jecs.component`, `jecs.tag` and `jecs.meta` for preregistering ids and their metadata before a world +- Overload to `World:entity` to create an entity at the desired id. ### Changed +- `World:clear` to remove the `ID` from every entity instead of the previous behaviour of removing all of the components on the entity. You should prefer deleting the entity instead for the previous behaviour. +- Entity ID layouts by putting the index in the lower bits, which should make every world function 1–5 nanoseconds faster. +- Hooks now pass the full component ID which is useful for pairs when you need both the relation and the target. +- Replaced `OnSet` with `OnChange`, which now only runs when the component ID was previously present on the entity. +- `OnAdd` now runs after the value has been set and also passes the component ID and the value. +- `OnRemove` now lazily looks up which archetype the entity will move to. This is meant to support interior structural changes within every hook. +- Optimized `world:has` for both single 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 this function multiple times. -- Separates ranges for components and entity IDs. +### 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. - - IDs created with `world:component()` will promote array lookups rather than map lookups in the `component_index` which is a significant boost +## 0.5.0 -- No longer caches the column pointers directly and instead the column indices which stay persistent even when data is reallocated during swap-removals - - This was an issue with the iterator being invalidated when you move an entity to a different archetype. - -### Fixedhttps://github.com/Ukendio/jecs/releases/tag/v0.0.0-prototype.rc.3 - -- Fixed a bug where changing an existing component would be slow because it was always appending changing the row of the entity record - - The fix dramatically improves times where it is basically down to just the speed of setting a field in a table - -## [0.0.0-prototype.rc.2] - 2024-04-26 +### Added +- `World:each` to find entities with a specific Tag. +- `World:children` to find children of an entity. +- `Query:cached` to add a query cache that updates itself when an archetype matching the query gets created or deleted. ### Changed +- Inference of entities' types using user-defined type functions. +- `Pair` to return `Second` if `First` is a `Tag`; otherwise, returns `First`. -- Optimized the creation of the query - - It will now finds the smallest archetype map to iterate over -- Optimized the query iterator +### Fixed +- `World:target` not giving adjacent pairs. - - It will now populates iterator with columns for faster indexing +## 0.4.0 -- Renamed the insertion method from world:add to world:set to better reflect what it does. +### Added +- Recycling support to `world:entity` so reused entity IDs now increment generation. -## [0.0.0-prototype.rc.2] - 2024-04-23 +### Removed +- `Query:drain` +- `Query:next` +- `Query:replace` -- Initial release +### Changed +- `jecs.Pair` type in Luau now returns the first element's type to avoid manual typecasting. -[unreleased]: https://github.com/ukendio/jecs/compare/v0.0.0.0-prototype.rc.2...HEAD -[0.2.2]: https://github.com/ukendio/jecs/releases/tag/v0.2.2 -[0.2.1]: https://github.com/ukendio/jecs/releases/tag/v0.2.1 -[0.2.0]: https://github.com/ukendio/jecs/releases/tag/v0.2.0 -[0.1.1]: https://github.com/ukendio/jecs/releases/tag/v0.1.1 -[0.1.0]: https://github.com/ukendio/jecs/releases/tag/v0.1.0 -[0.1.0-rc.6]: https://github.com/ukendio/jecs/releases/tag/v0.1.0-rc.6 -[0.1.0-rc.5]: https://github.com/ukendio/jecs/releases/tag/v0.1.0-rc.5 -[0.0.0-prototype-rc.3]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.3 -[0.0.0-prototype.rc.2]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.2 -[0.0.0-prototype-rc.1]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.1 +### Fixed +- `Query:archetypes` now correctly takes `self`. + +## 0.3.2 - 2024-10-01 + +### Changed +- `World:cleanup` to traverse a header type for graph edges. + +### Fixed +- Regression when calling `World:set` after `World:remove` on the same component. +- Removed explicit error in `JECS_DEBUG` for `World:target` missing index. +- `World.set` type inference with `NoInfer` in TypeScript. + +## 0.3.1 - 2024-10-01 + +### Added +- Index parameter to `World:target`. +- Global config `_G.JECS_HI_COMPONENT_ID` to change component ID limit (must be set before importing JECS). +- Debug mode via `_G.JECS_DEBUG` (must be set before importing JECS). +- `world:cleanup` to manually clean up empty archetypes. + +### Changed +- `world:delete` now also deletes dependent archetypes and child entities. + +### Fixed +- `Query` iterator to not drain by default. +- TypeScript package entry to `src` instead of `src/init`. +- `Query.next` now returns expected result tuple in typescript. +- `pair` returns a number instead of entity to prevent misuse. +- Exported built-in components `ChildOf`, `Name`, and `world.parent`. + +## 0.2.10 + +### Added +- Trait `jecs.Tag` for zero-cost tag components. +- Cleanup conditions: `jecs.OnDelete`, `jecs.Remove`. + +### Changed +- `world:set` is now idempotent when setting tags. + +### Fixed +- Improved performance for hooks. +- Exported types and functions: `world:contains()`, `query:drain()`, `Query`. +- Hook types: `OnAdd`, `OnSet`, `OnRemove`. +- ID flexibility for `add`, `set`, `remove`, `get`, `has`, `query`. +- `world:contains()` now returns `boolean`. +- `world:has()` parameters now correct. + +## 0.2.2 + +### Added +- `query:replace(fn)` for in-place replacement of component values. + +### Changed +- Iterator now goes backwards to avoid invalidation during iteration. + +## 0.2.1 + +### Added +- Built-in `jecs.Component` used to find all component IDs. + +## 0.2.0 + +### Added +- `world:parent(entity)` and `jecs.ChildOf` for parent-child relationships. + +### Changed +- Iteration performance improved by 20–40% through manual indexing. + +## 0.1.1 + +### Added +- `world:clear(entity)` for removing all components from an entity. +- TypeScript types. + +## 0.1.0 + +### Changed +- Optimized iterator. + +## 0.1.0-rc.6 + +### Added +- `jecs.Wildcard` term to query partially matched pairs. + +## 0.1.0-rc.5 + +### Added +- Entity relationships. +- `world:__iter()` for full world iteration. +- `world:add(entity, component)` (idempotent). + +### Fixed +- Component overriding when set out of order. + +## 0.0.0-prototype.rc.3 + +### Added +- Observers. +- `query:without()` for invariant queries. + +### Changed +- Separate ID ranges for entities and components. +- Avoid caching pointers; cache stable column indices instead. + +### Fixed +- Slow component updates due to unnecessary row changes. + +## 0.0.0-prototype.rc.2 - 2024-04-26 + +### Changed +- Query now uses smallest archetype map. +- Optimized query iterator. +- Renamed `world:add` to `world:set`. + +## 0.0.0-prototype.rc.1 + +### Added +- Initial release. diff --git a/README.md b/README.md index d8e7df0..e5a0ade 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Install with pesde @ `marked/jecs`

-[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE) [![Wally](https://img.shields.io/github/v/tag/ukendio/jecs?&style=for-the-badge)](https://wally.run/package/ukendio/jecs) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE) [![Wally](https://img.shields.io/github/v/tag/ukendio/jecs?&style=for-the-badge)](https://wally.run/package/ukendio/jecs) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ukendio/jecs/unit-testing.yaml?&style=for-the-badge)](https://github.com/Ukendio/jecs/actions/workflows/unit-testing.yaml) Just a stupidly fast Entity Component System @@ -16,7 +16,22 @@ Just a stupidly fast Entity Component System - Zero-dependency package - Optimized for column-major operations - Cache friendly [archetype/SoA](https://ajmmertens.medium.com/building-an-ecs-2-archetypes-and-vectorization-fe21690805f9) storage -- Rigorously [unit tested](https://github.com/Ukendio/jecs/actions/workflows/ci.yaml) for stability +- Rigorously [unit tested](https://github.com/Ukendio/jecs/actions/workflows/unit-testing.yaml) for stability + +### Installation + +With [Wally](https://wally.run/): +```bash +jecs = "ukendio/jecs@0.6.0" # Inside wally.toml +``` +With [pesde](https://pesde.dev/): +```bash +pesde add wally#ukendio/jecs@0.6.0 +``` +With [npm](https://www.npmjs.com/package/@rbxts/jecs) ([roblox-ts](https://roblox-ts.com/)): +```bash +npm i @rbxts/jecs +``` ### Example @@ -59,6 +74,8 @@ end -- sara is the child of alice ``` +### Benchmarks + 21,000 entities 125 archetypes 4 random components queried. ![Queries](assets/image-3.png) Can be found under /benches/visual/query.luau diff --git a/jecs.luau b/jecs.luau index f705f0e..416bc1f 100644 --- a/jecs.luau +++ b/jecs.luau @@ -1,3 +1,4 @@ + --!optimize 2 --!native --!strict @@ -94,29 +95,31 @@ type ecs_world_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 ECS_PAIR_OFFSET = 2^48 -local ECS_ENTITY_MASK = bit32.lshift(1, 24) -local ECS_GENERATION_MASK = bit32.lshift(1, 16) +local ECS_ID_DELETE = 0b01 +local ECS_ID_IS_TAG = 0b10 +local ECS_ID_MASK = 0b00 + +local HI_COMPONENT_ID = 256 +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 NULL_ARRAY = table.freeze({}) :: Column local NULL = newproxy(false) @@ -168,7 +171,6 @@ 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 @@ -824,7 +826,7 @@ local function world_entity(world: ecs_world_t, entity: i53?): i53 return entity end end - return entity_index_new_id(entity_index, entity) + return entity_index_new_id(entity_index) end local function world_parent(world: ecs_world_t, entity: i53) @@ -1050,7 +1052,7 @@ local function world_remove(world: ecs_world_t, entity: i53, id: i53) end end -local function archetype_fast_delete_last(columns: { Column }, column_count: number, types: { i53 }, entity: i53) +local function archetype_fast_delete_last(columns: { Column }, column_count: number) for i, column in columns do if column ~= NULL_ARRAY then column[column_count] = nil @@ -1058,7 +1060,7 @@ local function archetype_fast_delete_last(columns: { Column }, column_count: num end end -local function archetype_fast_delete(columns: { Column }, column_count: number, row, types, entity) +local function archetype_fast_delete(columns: { Column }, column_count: number, row: number) for i, column in columns do if column ~= NULL_ARRAY then column[row] = column[column_count] @@ -1100,9 +1102,9 @@ local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, entities[last] = nil :: any if row == last then - archetype_fast_delete_last(columns, column_count, id_types, delete) + archetype_fast_delete_last(columns, column_count) else - archetype_fast_delete(columns, column_count, row, id_types, delete) + archetype_fast_delete(columns, column_count, row) end end @@ -1408,14 +1410,14 @@ local function world_delete(world: ecs_world_t, entity: i53) 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 + for i = tr, tr + tr_count - 1 do + ids[types[i]] = 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) @@ -1933,6 +1935,9 @@ local function query_cached(query: ecs_query_data_t) local function on_delete_callback(archetype) local i = table.find(archetypes, archetype) :: number + if i == nil then + return + end local n = #archetypes archetypes[i] = archetypes[n] archetypes[n] = nil @@ -2521,8 +2526,8 @@ end World.new = world_new -export type Entity = { __T: T } -export type Id = { __T: T } +export type Entity = number | { __T: T } +export type Id = number | { __T: T } export type Pair = Id

type ecs_id_t = Id | Pair | Pair<"Tag", T> export type Item = (self: Query) -> (Entity, T...) @@ -2570,46 +2575,46 @@ export type World = { range: (self: World, range_begin: number, range_end: number?) -> (), --- Creates a new entity - entity: (self: World, id: Entity?) -> 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?, + 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) -> (), + 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: a) -> (), cleanup: (self: World) -> (), -- Clears an entity from the world - clear: (self: World, id: Id) -> (), + clear: (self: World, id: Id) -> (), --- 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: & ((World, Entity, Id) -> a?) + & ((World, Entity, Id, Id) -> (a?, b?)) + & ((World, Entity, Id, Id, Id) -> (a?, b?, c?)) + & ((World, 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, + has: ((World, Entity, Id) -> boolean) + & ((World, Entity, Id, Id) -> boolean) + & ((World, Entity, Id, Id, Id) -> boolean) + & (World, Entity, Id, Id, Id, Id) -> boolean, --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. - parent:(self: World, entity: Entity) -> Entity, + 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, --- Checks if the entity exists - exists: (self: World, entity: Entity) -> boolean, + exists: (self: World, entity: Entity) -> boolean, each: (self: World, id: Id) -> () -> Entity, @@ -2651,19 +2656,19 @@ return { 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, + OnAdd = (EcsOnAdd :: any) :: Entity<(entity: Entity, id: Id, data: T) -> ()>, + OnRemove = (EcsOnRemove :: any) :: Entity<(entity: Entity, id: Id) -> ()>, + OnChange = (EcsOnChange :: any) :: Entity<(entity: Entity, id: Id, data: T) -> ()>, + ChildOf = (EcsChildOf :: any) :: Entity, + Component = (EcsComponent :: any) :: Entity, + Wildcard = (EcsWildcard :: any) :: Entity, + w = (EcsWildcard :: any) :: Entity, + OnDelete = (EcsOnDelete :: any) :: Entity, + OnDeleteTarget = (EcsOnDeleteTarget :: any) :: Entity, + Delete = (EcsDelete :: any) :: Entity, + Remove = (EcsRemove :: any) :: Entity, + Name = (EcsName :: any) :: Entity, + Rest = (EcsRest :: any) :: Entity, pair = (ECS_PAIR :: any) :: (first: Id

, second: Id) -> Pair, diff --git a/pesde-rbx.toml b/pesde-rbx.toml index d0520fc..546b39e 100644 --- a/pesde-rbx.toml +++ b/pesde-rbx.toml @@ -11,7 +11,7 @@ includes = [ license = "MIT" name = "marked/jecs" repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.6.0" +version = "0.6.1" [indices] default = "https://github.com/pesde-pkg/index" diff --git a/pesde.lock b/pesde.lock index 91f9895..d67f8e4 100644 --- a/pesde.lock +++ b/pesde.lock @@ -2,5 +2,5 @@ # It is not intended for manual editing. format = 1 name = "marked/jecs" -version = "0.6.0" +version = "0.6.1" target = "luau" diff --git a/pesde.toml b/pesde.toml index ecf59dc..ca8559e 100644 --- a/pesde.toml +++ b/pesde.toml @@ -11,7 +11,7 @@ includes = [ license = "MIT" name = "marked/jecs" repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.6.0" +version = "0.6.1" [indices] default = "https://github.com/pesde-pkg/index" From fa500795afd889a7a97ed0141439f4894bc35533 Mon Sep 17 00:00:00 2001 From: marked Date: Sat, 28 Jun 2025 02:11:47 +0200 Subject: [PATCH 10/10] Sync to upstream Jecs 0.7.2 (#13) Reviewed-on: https://git.devmarked.win/marked/jecs-pesde/pulls/13 --- CHANGELOG.md | 13 + README.md | 4 +- jecs.luau | 2518 ++++++++++++++++++++++++++---------------------- pesde-rbx.toml | 2 +- pesde.lock | 2 +- pesde.toml | 2 +- 6 files changed, 1372 insertions(+), 1169 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28a4d81..b880f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ ## Unreleased +## 0.7.0 + +### Added +- `jecs.component_record` for retrieving the component_record of a component. +- `Column` and `ColumnsMap` types for typescript. +- `bulk_insert` and `bulk_remove` respectively for moving an entity to an archetype without intermediate steps. + +### Changed +- The fields `archetype.records[id]` and `archetype.counts[id` have been removed from the archetype struct and been moved to the component record `component_index[id].records[archetype.id]` and `component_index[id].counts[archetype.id]` respectively. +- Removed the metatable `jecs.World`. Use `jecs.world()` to create your World. +- Archetypes will no longer be garbage collected when invalidated, allowing them to be recycled to save a lot of performance during frequent deletion. +- Removed `jecs.entity_index_try_get_fast`. Use `jecs.entity_index_try_get` instead. + ## 0.6.1 ### Changed diff --git a/README.md b/README.md index e5a0ade..3159dcf 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,8 @@ world:set(sara, Name, "sara") print(getName(parent(sara))) -for e in world:query(pair(ChildOf, alice)) do - print(getName(e), "is the child of alice") +for e, name in world:query(Name, pair(ChildOf, alice)) do + print(name, "is the child of alice") end -- Output diff --git a/jecs.luau b/jecs.luau index 416bc1f..3dacd69 100644 --- a/jecs.luau +++ b/jecs.luau @@ -1,4 +1,3 @@ - --!optimize 2 --!native --!strict @@ -7,92 +6,176 @@ type i53 = number type i24 = number -type Ty = { i53 } +type Ty = { Entity } 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 }, + entities: { Entity }, columns: { Column }, - records: { [Id]: number }, - counts: { [Id]: number }, + columns_map: { [Id]: Column }, + dead: boolean, } -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 }, +export type QueryInner = { + compatible_archetypes: { Archetype }, ids: { i53 }, filter_with: { i53 }, filter_without: { i53 }, next: () -> (number, ...any), - world: ecs_world_t, + world: World, } -type ecs_observer_t = { - callback: (archetype: ecs_archetype_t) -> (), - query: ecs_query_data_t, +export type Entity = number | { __T: T } +export type Id = number | { __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, } -type ecs_observable_t = Map> +export type World = { + archetype_edges: Map>, + archetype_index: { [string]: Archetype }, + archetypes: Archetypes, + component_index: ComponentIndex, + entity_index: EntityIndex, + ROOT_ARCHETYPE: Archetype, -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>, + max_archetype_id: number, + + observable: Map>, + + --- 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: a) -> (), + + 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: & ((World, Entity, Id) -> a?) + & ((World, Entity, Id, Id) -> (a?, b?)) + & ((World, Entity, Id, Id, Id) -> (a?, b?, c?)) + & ((World, Entity, Id, Id, Id, Id) -> (a?, b?, c?, d?)), + + --- Returns whether the entity has the ID. + has: ((World, Entity, Id) -> boolean) + & ((World, Entity, Id, Id) -> boolean) + & ((World, Entity, Id, Id, Id) -> boolean) + & (World, Entity, Id, Id, Id, Id) -> boolean, + + --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. + parent: (self: World, entity: Entity) -> Entity?, + + --- Checks if the world contains the given entity + contains: (self: World, entity: Entity) -> boolean, + + --- 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), +} + +export type Record = { + archetype: Archetype, + row: number, + dense: i24, +} +export type ComponentRecord = { + records: { [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?, } -- stylua: ignore start @@ -223,9 +306,9 @@ local function ECS_PAIR_SECOND(e: i53): i24 end local function entity_index_try_get_any( - entity_index: ecs_entity_index_t, + entity_index: EntityIndex, entity: number -): ecs_record_t? +): Record? local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] if not r or r.dense == 0 then @@ -235,8 +318,8 @@ 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 r = entity_index_try_get_any(entity_index, entity) +local function entity_index_try_get(entity_index: EntityIndex, entity: Entity): Record? + local r = entity_index_try_get_any(entity_index, entity :: number) if r then local r_dense = r.dense if r_dense > entity_index.alive_count then @@ -249,29 +332,19 @@ 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 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) +local function entity_index_is_alive(entity_index: EntityIndex, entity: Entity): boolean 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) +local function entity_index_get_alive(entity_index: EntityIndex, entity: Entity): Entity? + local r = entity_index_try_get_any(entity_index, entity :: number) if r then return entity_index.dense_array[r.dense] end return nil end -local function ecs_get_alive(world, entity) +local function ecs_get_alive(world: World, entity: Entity): Entity if entity == 0 then return 0 end @@ -282,7 +355,7 @@ local function ecs_get_alive(world, entity) return entity end - if entity > ECS_ENTITY_MASK then + if (entity :: number) > ECS_ENTITY_MASK then return 0 end @@ -296,7 +369,7 @@ 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 function entity_index_new_id(entity_index: EntityIndex): Entity local dense_array = entity_index.dense_array local alive_count = entity_index.alive_count local sparse_array = entity_index.sparse_array @@ -317,28 +390,27 @@ local function entity_index_new_id(entity_index: ecs_entity_index_t): i53 alive_count += 1 entity_index.alive_count = alive_count dense_array[alive_count] = id - sparse_array[id] = { dense = alive_count } :: ecs_record_t + sparse_array[id] = { dense = alive_count } :: Record return id end -local function ecs_pair_first(world: ecs_world_t, e: i53) +local function ecs_pair_first(world: World, e: i53) local pred = ECS_PAIR_FIRST(e) return ecs_get_alive(world, pred) end -local function ecs_pair_second(world: ecs_world_t, e: i53) +local function ecs_pair_second(world: World, e: i53) local obj = ECS_PAIR_SECOND(e) return ecs_get_alive(world, obj) end -local function query_match(query: ecs_query_data_t, - archetype: ecs_archetype_t) - local records = archetype.records +local function query_match(query: QueryInner, archetype: Archetype) + local columns_map = archetype.columns_map local with = query.filter_with for _, id in with do - if not records[id] then + if not columns_map[id] then return false end end @@ -346,7 +418,7 @@ local function query_match(query: ecs_query_data_t, local without = query.filter_without if without then for _, id in without do - if records[id] then + if columns_map[id] then return false end end @@ -355,8 +427,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: Id, component: Id): { Observer }? local cache = world.observable[event] if not cache then return nil @@ -365,20 +436,19 @@ local function find_observers(world: ecs_world_t, event: i53, end local function archetype_move( - entity_index: ecs_entity_index_t, - to: ecs_archetype_t, + entity_index: EntityIndex, + to: Archetype, dst_row: i24, - from: ecs_archetype_t, + from: Archetype, 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 + local columns_map = to.columns_map for i, column in src_columns do if column == NULL_ARRAY then @@ -386,11 +456,11 @@ local function archetype_move( 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]] + local dst_column = columns_map[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] + if dst_column then + dst_column[dst_row] = column[src_row] end -- If the entity is the last row in the archetype then swapping it would be meaningless. @@ -418,15 +488,15 @@ local function archetype_move( 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)] + local record1 = sparse_array[ECS_ENTITY_T_LO(e1 :: number)] + local record2 = sparse_array[ECS_ENTITY_T_LO(e2 :: number)] record1.row = dst_row record2.row = src_row end local function archetype_append( - entity: i53, - archetype: ecs_archetype_t + entity: Entity, + archetype: Archetype ): number local entities = archetype.entities local length = #entities + 1 @@ -435,10 +505,10 @@ local function archetype_append( end local function new_entity( - entity: i53, - record: ecs_record_t, - archetype: ecs_archetype_t -): ecs_record_t + entity: Entity, + record: Record, + archetype: Archetype +): Record local row = archetype_append(entity, archetype) record.archetype = archetype record.row = row @@ -446,10 +516,10 @@ local function new_entity( end local function entity_move( - entity_index: ecs_entity_index_t, - entity: i53, - record: ecs_record_t, - to: ecs_archetype_t + entity_index: EntityIndex, + entity: Entity, + record: Record, + to: Archetype ) local sourceRow = record.row local from = record.archetype @@ -459,24 +529,23 @@ local function entity_move( record.row = dst_row end -local function hash(arr: { number }): string +local function hash(arr: { Entity }): string return table.concat(arr, "_") end -local function fetch(id: i53, records: { number }, - columns: { Column }, row: number): any - local tr = records[id] +local function fetch(id: Id, columns_map: { [Entity]: Column }, row: number): any + local column = columns_map[id] - if not tr then + if not column then return nil end - return columns[tr][row] + return column[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) +local function world_get(world: World, entity: Entity, + a: Id, b: Id?, c: Id?, d: Id?, e: Id?): ...any + local record = entity_index_try_get(world.entity_index, entity) if not record then return nil end @@ -486,27 +555,26 @@ local function world_get(world: ecs_world_t, entity: i53, return nil end - local records = archetype.records - local columns = archetype.columns + local columns_map = archetype.columns_map local row = record.row - local va = fetch(a, records, columns, row) + local va = fetch(a, columns_map, row) if not b then return va elseif not c then - return va, fetch(b, records, columns, row) + return va, fetch(b, columns_map, row) elseif not d then - return va, fetch(b, records, columns, row), fetch(c, records, columns, row) + return va, fetch(b, columns_map, row), fetch(c, columns_map, row) elseif not e then - return va, fetch(b, records, columns, row), fetch(c, records, columns, row), fetch(d, records, columns, row) + return va, fetch(b, columns_map, row), fetch(c, columns_map, row), fetch(d, columns_map, 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) +local function world_has_one_inline(world: World, entity: Entity, id: i53): boolean + local record = entity_index_try_get(world.entity_index, entity) if not record then return false end @@ -516,44 +584,12 @@ local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): b return false end - local records = archetype.records - - return records[id] ~= nil + return archetype.columns_map[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) +local function world_target(world: World, entity: Entity, relation: Id, index: number?): Entity? + local entity_index = world.entity_index + local record = entity_index_try_get(entity_index, entity) if not record then return nil end @@ -563,24 +599,33 @@ local function world_target(world: ecs_world_t, entity: i53, relation: i24, inde return nil end - local r = ECS_PAIR(relation, EcsWildcard) + local r = ECS_PAIR(relation :: number, EcsWildcard) + local idr = world.component_index[r] - local count = archetype.counts[r] + if not idr then + return nil + end + + local archetype_id = archetype.id + local count = idr.counts[archetype_id] if not count then return nil end + local nth = index or 0 + if nth >= count then nth = nth + count + 1 end - nth = archetype.types[nth + archetype.records[r]] + nth = archetype.types[nth + idr.records[archetype_id]] + if not nth then return nil end - return entity_index_get_alive(world.entity_index, - ECS_PAIR_SECOND(nth)) + return entity_index_get_alive(entity_index, + ECS_PAIR_SECOND(nth :: number)) end local function ECS_ID_IS_WILDCARD(e: i53): boolean @@ -589,10 +634,21 @@ 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_get(world: World, id: Entity): ComponentRecord? + local component_index = world.component_index + local idr: ComponentRecord = component_index[id] + + if idr then + return idr + end + + return nil +end + +local function id_record_ensure(world: World, id: Entity): ComponentRecord local component_index = world.component_index local entity_index = world.entity_index - local idr: ecs_id_record_t? = component_index[id] + local idr: ComponentRecord? = component_index[id] if idr then return idr @@ -601,12 +657,12 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t local flags = ECS_ID_MASK local relation = id local target = 0 - local is_pair = ECS_IS_PAIR(id) + local is_pair = ECS_IS_PAIR(id :: number) if is_pair then - relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53 + relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id :: number)) :: 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 + target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id :: number)) :: i53 ecs_assert(target and entity_index_is_alive( entity_index, target), ECS_INTERNAL_ERROR) end @@ -638,7 +694,7 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t idr = { size = 0, - cache = {}, + records = {}, counts = {}, flags = flags, hooks = { @@ -646,7 +702,7 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t on_change = on_change, on_remove = on_remove, }, - } :: ecs_id_record_t + } :: ComponentRecord component_index[id] = idr @@ -654,80 +710,82 @@ 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, + idr: ComponentRecord, + archetype_id: number, + columns_map: { [Id]: Column }, id: i53, - index: number + index: number, + column: Column ) - local archetype_id = archetype.id - local archetype_records = archetype.records - local archetype_counts = archetype.counts - local idr_columns = idr.cache + local idr_records = idr.records local idr_counts = idr.counts - local tr = idr_columns[archetype_id] + local tr = idr_records[archetype_id] if not tr then - idr_columns[archetype_id] = index + idr_records[archetype_id] = index idr_counts[archetype_id] = 1 - - archetype_records[id] = index - archetype_counts[id] = 1 + columns_map[id] = column 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 function archetype_register(world: World, archetype: Archetype) + local archetype_id = archetype.id + local columns_map = archetype.columns_map + local columns = archetype.columns + for i, component_id in archetype.types do + local idr = id_record_ensure(world, component_id) + local is_tag = bit32.band(idr.flags, ECS_ID_IS_TAG) ~= 0 + local column = if is_tag then NULL_ARRAY else {} + columns[i] = column + + archetype_append_to_records(idr, archetype_id, columns_map, component_id :: number, i, column) + + if ECS_IS_PAIR(component_id :: number) then + local relation = ECS_PAIR_FIRST(component_id :: number) + local object = ECS_PAIR_SECOND(component_id :: number) + local r = ECS_PAIR(relation, EcsWildcard) + local idr_r = id_record_ensure(world, r) + + archetype_append_to_records(idr_r, archetype_id, columns_map, r, i, column) + + local t = ECS_PAIR(EcsWildcard, object) + local idr_t = id_record_ensure(world, t) + + archetype_append_to_records(idr_t, archetype_id, columns_map, t, i, column) + end + end +end + +local function archetype_create(world: World, id_types: { Id }, ty, prev: i53?): Archetype local archetype_id = (world.max_archetype_id :: number) + 1 world.max_archetype_id = archetype_id local length = #id_types local columns = (table.create(length) :: any) :: { Column } - local records: { number } = {} - local counts: { number } = {} + local columns_map: { [Id]: Column } = {} - local archetype: ecs_archetype_t = { + local archetype: Archetype = { columns = columns, + columns_map = columns_map, entities = {}, id = archetype_id, - records = records, - counts = counts, type = ty, types = id_types, + dead = false, } - 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) + archetype_register(world, archetype) - 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 + for id in columns_map 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 + if query_match(observer.query :: QueryInner, archetype) then observer.callback(archetype) end end @@ -735,12 +793,12 @@ 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] = {} :: Map + world.archetype_edges[archetype.id] = {} :: Map return archetype end -local function world_range(world: ecs_world_t, range_begin: number, range_end: number?) +local function world_range(world: World, range_begin: number, range_end: number?) local entity_index = world.entity_index entity_index.range_begin = range_begin @@ -756,84 +814,14 @@ local function world_range(world: ecs_world_t, range_begin: number, range_end: n dense_array[i] = i sparse_array[i] = { dense = 0 - } :: ecs_record_t + } :: Record 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) -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 +local function archetype_ensure(world: World, id_types: { Id }): Archetype if #id_types < 1 then return world.ROOT_ARCHETYPE end @@ -841,6 +829,10 @@ local function archetype_ensure(world: ecs_world_t, id_types): ecs_archetype_t local ty = hash(id_types) local archetype = world.archetype_index[ty] if archetype then + if archetype.dead then + archetype_register(world, archetype) + archetype.dead = false :: any + end return archetype end @@ -850,7 +842,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 @@ -861,10 +852,10 @@ local function find_insert(id_types: { i53 }, toAdd: i53): number end local function find_archetype_without( - world: ecs_world_t, - node: ecs_archetype_t, - id: i53 -): ecs_archetype_t + world: World, + node: Archetype, + id: Id +): Archetype local id_types = node.types local at = table.find(id_types, id) @@ -876,11 +867,11 @@ end local function create_edge_for_remove( - world: ecs_world_t, - node: ecs_archetype_t, - edge: Map, - id: i53 -): ecs_archetype_t + world: World, + node: Archetype, + edge: Map, + id: Id +): Archetype local to = find_archetype_without(world, node, id) local edges = world.archetype_edges local archetype_id = node.id @@ -890,14 +881,14 @@ local function create_edge_for_remove( end local function archetype_traverse_remove( - world: ecs_world_t, - id: i53, - from: ecs_archetype_t -): ecs_archetype_t + world: World, + id: Id, + from: Archetype +): Archetype local edges = world.archetype_edges local edge = edges[from.id] - local to: ecs_archetype_t = edge[id] + local to: Archetype = edge[id] if to == nil then to = find_archetype_without(world, from, id) edge[id] = to @@ -907,19 +898,19 @@ local function archetype_traverse_remove( return to end -local function find_archetype_with(world, id, from): ecs_archetype_t +local function find_archetype_with(world: World, id: Id, from: Archetype): Archetype local id_types = from.types - local at = find_insert(id_types, id) - local dst = table.clone(id_types) :: { i53 } + local at = find_insert(id_types :: { number } , id :: number) + local dst = table.clone(id_types) 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 +local function archetype_traverse_add(world: World, id: Id, from: Archetype): Archetype from = from or world.ROOT_ARCHETYPE - if from.records[id] then + if from.columns_map[id] then return from end local edges = world.archetype_edges @@ -935,86 +926,6 @@ local function archetype_traverse_add(world, id, from: ecs_archetype_t): ecs_arc 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 @@ -1027,30 +938,7 @@ local function world_component(world: World): i53 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) for i, column in columns do @@ -1069,7 +957,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) local entity_index = world.entity_index local component_index = world.component_index local columns = archetype.columns @@ -1082,7 +970,7 @@ local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, local delete = move if row ~= last then - local record_to_move = entity_index_try_get_any(entity_index, move) + local record_to_move = entity_index_try_get_any(entity_index, move :: number) if record_to_move then record_to_move.row = row end @@ -1108,107 +996,8 @@ 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 - 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) +local function archetype_destroy(world: World, archetype: Archetype) if archetype == world.ROOT_ARCHETYPE then return end @@ -1223,9 +1012,9 @@ local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t) local archetype_id = archetype.id world.archetypes[archetype_id] = nil :: any world.archetype_index[archetype.type] = nil :: any - local records = archetype.records + local columns_map = archetype.columns_map - for id in records do + for id in columns_map do local observer_list = find_observers(world, EcsOnArchetypeDelete, id) if not observer_list then continue @@ -1237,236 +1026,21 @@ local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t) end end - for id in records do + for id in columns_map do local idr = component_index[id] - idr.cache[archetype_id] = nil :: any + idr.records[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 + tr_count - 1 do - ids[types[i]] = 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 function query_iter_init(query: QueryInner): () -> (number, ...any) local world_query_iter_next local compatible_archetypes = query.compatible_archetypes @@ -1475,10 +1049,9 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) 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 columns_map = archetype.columns_map local ids = query.ids local A, B, C, D, E, F, G, H, I = unpack(ids) @@ -1486,49 +1059,49 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) local e: Column, f: Column, g: Column, h: Column if not B then - a = columns[records[A]] + a = columns_map[A] elseif not C then - a = columns[records[A]] - b = columns[records[B]] + a = columns_map[A] + b = columns_map[B] elseif not D then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] elseif not E then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[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]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[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]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[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]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + else + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + h = columns_map[H] end if not B then @@ -1547,9 +1120,8 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] + columns_map = archetype.columns_map + a = columns_map[A] end local row = i @@ -1573,10 +1145,9 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] end local row = i @@ -1600,11 +1171,10 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] end local row = i @@ -1628,12 +1198,11 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) 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]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] end local row = i @@ -1657,13 +1226,12 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) 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]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] end local row = i @@ -1687,14 +1255,13 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) 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]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] end local row = i @@ -1718,15 +1285,14 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) 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]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] end local row = i @@ -1750,16 +1316,15 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) 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]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + h = columns_map[H] end local row = i @@ -1769,6 +1334,7 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) end else local output = {} + local ids_len = #ids function world_query_iter_next(): any local entity = entities[i] while entity == nil do @@ -1784,18 +1350,25 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + h = columns_map[H] end local row = i i -= 1 - for j, id in ids do - output[j] = columns[records[id]][row] + for i = 9, ids_len do + output[i - 8] = columns_map[i][row] end - return entity, unpack(output) + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row], unpack(output) end end @@ -1811,17 +1384,17 @@ 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 for i = #compatible_archetypes, 1, -1 do local archetype = compatible_archetypes[i] - local records = archetype.records + local columns_map = archetype.columns_map local matches = true for _, id in without do - if records[id] then + if columns_map[id] then matches = false break end @@ -1841,18 +1414,18 @@ 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 for i = #compatible_archetypes, 1, -1 do local archetype = compatible_archetypes[i] - local records = archetype.records + local columns_map = archetype.columns_map local matches = true for _, id in with do - if not records[id] then + if not columns_map[id] then matches = false break end @@ -1879,7 +1452,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 @@ -1896,20 +1469,19 @@ local function query_cached(query: ecs_query_data_t) local e: Column, f: Column, g: Column, h: Column local world_query_iter_next - local columns: { Column } - local entities: { number } + local entities: { Entity } local i: number - local archetype: ecs_archetype_t - local records: { number } + local archetype: Archetype + local columns_map: { [Id]: Column } local archetypes = query.compatible_archetypes - local world = query.world :: { observable: ecs_observable_t } + local world = query.world -- 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 local on_create_action = observable[EcsOnArchetypeCreate] if not on_create_action then - on_create_action = {} :: Map + on_create_action = {} :: Map observable[EcsOnArchetypeCreate] = on_create_action end local query_cache_on_create = on_create_action[A] @@ -1920,7 +1492,7 @@ local function query_cached(query: ecs_query_data_t) local on_delete_action = observable[EcsOnArchetypeDelete] if not on_delete_action then - on_delete_action = {} :: Map + on_delete_action = {} :: Map observable[EcsOnArchetypeDelete] = on_delete_action end local query_cache_on_delete = on_delete_action[A] @@ -1957,52 +1529,51 @@ local function query_cached(query: ecs_query_data_t) end entities = archetype.entities i = #entities - records = archetype.records - columns = archetype.columns + columns_map = archetype.columns_map if not B then - a = columns[records[A]] + a = columns_map[A] elseif not C then - a = columns[records[A]] - b = columns[records[B]] + a = columns_map[A] + b = columns_map[B] elseif not D then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] elseif not E then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[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]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[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]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[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]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + else + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + h = columns_map[H] end return world_query_iter_next @@ -2024,9 +1595,8 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] + columns_map = archetype.columns_map + a = columns_map[A] end local row = i @@ -2050,10 +1620,9 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] end local row = i @@ -2077,11 +1646,10 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] end local row = i @@ -2105,12 +1673,11 @@ local function query_cached(query: ecs_query_data_t) 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]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] end local row = i @@ -2134,13 +1701,12 @@ local function query_cached(query: ecs_query_data_t) 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]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] end local row = i @@ -2164,14 +1730,13 @@ local function query_cached(query: ecs_query_data_t) 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]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] end local row = i @@ -2195,15 +1760,14 @@ local function query_cached(query: ecs_query_data_t) 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]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] end local row = i @@ -2227,16 +1791,15 @@ local function query_cached(query: ecs_query_data_t) 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]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + h = columns_map[H] end local row = i @@ -2245,7 +1808,8 @@ local function query_cached(query: ecs_query_data_t) return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] end else - local queryOutput = {} + local output = {} + local ids_len = #ids function world_query_iter_next(): any local entity = entities[i] while entity == nil do @@ -2261,28 +1825,25 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + h = columns_map[H] 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] + for i = 9, ids_len do + output[i - 8] = columns_map[i][row] end - for j, id in ids do - queryOutput[j] = columns[records[id]][row] - end - - return entity, unpack(queryOutput) + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], unpack(output) end end @@ -2303,7 +1864,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 @@ -2311,7 +1872,7 @@ local function world_query(world: ecs_world_t, ...) local archetypes = world.archetypes - local idr: ecs_id_record_t? + local idr: ComponentRecord? local component_index = world.component_index local q = setmetatable({ @@ -2335,18 +1896,18 @@ local function world_query(world: ecs_world_t, ...) return q end - for archetype_id in idr.cache do + for archetype_id in idr.records do local compatibleArchetype = archetypes[archetype_id] if #compatibleArchetype.entities == 0 then continue end - local records = compatibleArchetype.records + local columns_map = compatibleArchetype.columns_map local skip = false for i, id in ids do - local tr = records[id] - if not tr then + local column = columns_map[id] + if not column then skip = true break end @@ -2363,18 +1924,18 @@ 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: Id): () -> Entity local idr = world.component_index[id] if not idr then - return NOOP + return NOOP :: () -> Entity end - local idr_cache = idr.cache + local records = idr.records local archetypes = world.archetypes - local archetype_id = next(idr_cache, nil) :: number + local archetype_id = next(records, nil) :: number local archetype = archetypes[archetype_id] if not archetype then - return NOOP + return NOOP :: () -> Entity end local entities = archetype.entities @@ -2383,7 +1944,7 @@ local function world_each(world: ecs_world_t, id: i53): () -> () return function(): any local entity = entities[row] while not entity do - archetype_id = next(idr_cache, archetype_id) :: number + archetype_id = next(records, archetype_id) :: number if not archetype_id then return end @@ -2397,87 +1958,813 @@ local function world_each(world: ecs_world_t, id: i53): () -> () end end -local function world_children(world: ecs_world_t, parent: i53) - return world_each(world, ECS_PAIR(EcsChildOf, parent)) +local function world_children(world: World, parent: Id) + return world_each(world, ECS_PAIR(EcsChildOf, parent::number)) 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 } +local function ecs_bulk_insert(world: World, entity: Entity, ids: { Entity }, values: { any }) + local entity_index = world.entity_index + local r = entity_index_try_get(entity_index, entity) + if not r then + return + end + local from = r.archetype + local component_index = world.component_index + if not from then + local dst_types = ids + local to = archetype_ensure(world, dst_types) + new_entity(entity, r, to) + local row = r.row + local columns_map = to.columns_map + for i, id in ids do + local value = values[i] + local cdr = component_index[id] -export type EntityIndex = { - dense_array: Map, - sparse_array: Map, - alive_count: number, - max_id: number, - range_begin: number?, - range_end: number? -} + local on_add = cdr.hooks.on_add + if value then + columns_map[id][row] = value + if on_add then + on_add(entity, id, value :: any) + end + else + if on_add then + on_add(entity, id) + end + end + end + return + end -local World = {} -World.__index = World + local dst_types = table.clone(from.types) -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 emplaced: { [number]: boolean } = {} + + for i, id in ids do + local at = find_insert(dst_types :: { number }, id :: number) + if at == -1 then + emplaced[i] = true + continue + end + + emplaced[i] = false + + table.insert(dst_types, at, id) + end + + local to = archetype_ensure(world, dst_types) + local columns_map = to.columns_map + + if from ~= to then + entity_move(entity_index, entity, r, to) + end + local row = r.row + + for i, set in emplaced do + local id = ids[i] + local idr = component_index[id] + + local value = values[i] :: any + + local on_add = idr.hooks.on_add + local on_change = idr.hooks.on_change + + if value then + columns_map[id][row] = value + local hook = if set then on_change else on_add + if hook then + hook(entity, id, value :: any) + end + else + if on_add then + on_add(entity, id, value) + end + end + end +end + +local function ecs_bulk_remove(world: World, entity: Entity, ids: { Entity }) + local entity_index = world.entity_index + local r = entity_index_try_get(entity_index, entity) + if not r then + return + end + local from = r.archetype + local component_index = world.component_index + if not from then + return + end + + local remove: { [Entity]: boolean } = {} + + local columns_map = from.columns_map + + for i, id in ids do + if not columns_map[id] then + continue + end + + remove[id] = true + local idr = component_index[id] + + local on_remove = idr.hooks.on_remove + if on_remove then + on_remove(entity, id) + end + end + + local to = r.archetype + if from ~= to then + from = to + end + + local dst_types = table.clone(from.types) :: { Entity } + + for id in remove do + local at = table.find(dst_types, id) + table.remove(dst_types, at) + end + + to = archetype_ensure(world, dst_types) + if from ~= to then + entity_move(entity_index, entity, r, to) + end +end local function world_new() + local eindex_dense_array = {} :: { Entity } + local eindex_sparse_array = {} :: { Record } + local eindex_alive_count = 0 + local eindex_max_id = 0 + local entity_index = { - dense_array = {}, - sparse_array = {}, - alive_count = 0, - max_id = 0, - } :: ecs_entity_index_t - local self = setmetatable({ - archetype_edges = {}, + dense_array = eindex_dense_array, + sparse_array = eindex_sparse_array, + alive_count = eindex_alive_count, + max_id = eindex_max_id, + } :: EntityIndex - archetype_index = {} :: { [string]: Archetype }, - archetypes = {} :: Archetypes, - component_index = {} :: ComponentIndex, + local component_index = {} :: ComponentIndex + + local archetype_index = {} :: { [string]: Archetype } + local archetypes = {} :: Archetypes + local archetype_edges = {} :: { [number]: { [Id]: Archetype } } + + local observable = {} + + local world = { + archetype_edges = archetype_edges, + + component_index = component_index, entity_index = entity_index, - ROOT_ARCHETYPE = (nil :: any) :: Archetype, + ROOT_ARCHETYPE = nil :: any, + archetypes = archetypes, + archetype_index = archetype_index, max_archetype_id = 0, max_component_id = ecs_max_component_id, - observable = {} :: Observable, - }, World) :: any + observable = observable, + } :: World - self.ROOT_ARCHETYPE = archetype_create(self, {}, "") + + local ROOT_ARCHETYPE = archetype_create(world, {}, "") + world.ROOT_ARCHETYPE = ROOT_ARCHETYPE + + local function inner_entity_index_try_get_any(entity: number): Record? + local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)] + + if not r or r.dense == 0 then + return nil + end + + return r + end + + -- local function entity_index_try_get_safe(entity: number): Record? + -- local r = entity_index_try_get_any_fast(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 inner_entity_index_try_get(entity: number): Record? + local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)] + if r then + if eindex_dense_array[r.dense] ~= entity then + return nil + end + end + return r + end + + + local function inner_world_add( + world: World, + entity: Entity, + id: Id + ): () + local entity_index = world.entity_index + local record = inner_entity_index_try_get(entity :: number) + 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 inner_world_get(world: World, entity: Entity, + a: Id, b: Id?, c: Id?, d: Id?, e: Id?): ...any + local record = inner_entity_index_try_get(entity::number) + if not record then + return nil + end + + local archetype = record.archetype + if not archetype then + return nil + end + + local columns_map = archetype.columns_map + local row = record.row + + local va = fetch(a, columns_map, row) + + if not b then + return va + elseif not c then + return va, fetch(b, columns_map, row) + elseif not d then + return va, fetch(b, columns_map, row), fetch(c, columns_map, row) + elseif not e then + return va, fetch(b, columns_map, row), fetch(c, columns_map, row), fetch(d, columns_map, row) + else + error("args exceeded") + end + end + + local function inner_world_has(world: World, entity: i53, + a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean + + local record = inner_entity_index_try_get(entity) + if not record then + return false + end + + local archetype = record.archetype + if not archetype then + return false + end + + local columns_map = archetype.columns_map + + return columns_map[a] ~= nil and + (b == nil or columns_map[b] ~= nil) and + (c == nil or columns_map[c] ~= nil) and + (d == nil or columns_map[d] ~= nil) and + (e == nil or error("args exceeded")) + end + + local function inner_world_target(world: World, entity: Entity, relation: Id, index: number?): Entity? + local record = inner_entity_index_try_get(entity :: number) + if not record then + return nil + end + + local archetype = record.archetype + if not archetype then + return nil + end + + local r = ECS_PAIR(relation::number, EcsWildcard) + local idr = world.component_index[r] + + if not idr then + return nil + end + + local archetype_id = archetype.id + local count = idr.counts[archetype_id] + if not count then + return nil + end + + local nth = index or 0 + + if nth >= count then + nth = nth + count + 1 + end + + nth = archetype.types[nth + idr.records[archetype_id]] + + if not nth then + return nil + end + + return entity_index_get_alive(world.entity_index, + ECS_PAIR_SECOND(nth :: number)) + end + + local function inner_world_parent(world: World, entity: Entity): Entity? + return inner_world_target(world, entity, EcsChildOf, 0) + end + + local function inner_archetype_traverse_add(id: Id, from: Archetype): Archetype + from = from or ROOT_ARCHETYPE + if from.columns_map[id] then + return from + end + local edges = archetype_edges + local edge = edges[from.id] + + local to = edge[id] :: Archetype + if not to then + to = find_archetype_with(world, id, from) + edge[id] = to + edges[to.id][id] = from + end + + return to + end + + local function inner_world_set(world: World, entity: Entity, id: Id, data: a): () + local record = inner_entity_index_try_get(entity :: number) + if not record then + return + end + + local from: Archetype = record.archetype + local to: Archetype = inner_archetype_traverse_add(id, from) + local idr = component_index[id] + local idr_hooks = idr.hooks + + if from == to then + local column = to.columns_map[id] + column[record.row] = data + + -- If the archetypes are the same it can avoid moving the entity + -- and just set the data directly. + local on_change = idr_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 + new_entity(entity, record, to) + end + local column = to.columns_map[id] + column[record.row] = data + + local on_add = idr_hooks.on_add + if on_add then + on_add(entity, id, data) + end + end + + local function inner_world_entity(world: World, entity: Entity?): Entity + if entity then + local index = ECS_ID(entity :: number) + local alive_count = entity_index.alive_count + local r = eindex_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 = eindex_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 = eindex_dense_array[dense] + local r_swap = inner_entity_index_try_get_any(e_swap :: number) :: Record + alive_count += 1 + entity_index.alive_count = alive_count + r_swap.dense = dense + r.dense = alive_count + eindex_dense_array[dense] = e_swap + eindex_dense_array[alive_count] = entity + + return entity + else + for i = eindex_max_id + 1, index do + eindex_sparse_array[i] = { dense = i } :: Record + eindex_dense_array[i] = i + end + entity_index.max_id = index + + local e_swap = eindex_dense_array[alive_count] + local r_swap = eindex_sparse_array[alive_count] + r_swap.dense = index + + alive_count += 1 + entity_index.alive_count = alive_count + + r = eindex_sparse_array[index] + + r.dense = alive_count + + eindex_sparse_array[index] = r + + eindex_dense_array[index] = e_swap + eindex_dense_array[alive_count] = entity + + return entity + end + end + return entity_index_new_id(entity_index) + end + + local function inner_world_remove(world: World, entity: Entity, id: Id) + local record = inner_entity_index_try_get(entity :: number) + if not record then + return + end + local from = record.archetype + + if not from then + return + end + + if from.columns_map[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 inner_world_clear(world: World, entity: Entity) + local tgt = ECS_PAIR(EcsWildcard, entity::number) + local idr_t = component_index[tgt] + local idr = component_index[entity] + local rel = ECS_PAIR(entity::number, EcsWildcard) + local idr_r = component_index[rel] + + if idr then + local count = 0 + local queue = {} + for archetype_id in idr.records 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 + inner_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.records + 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::number) then + continue + end + local object = entity_index_get_alive( + entity_index, ECS_PAIR_SECOND(id::number)) + 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 + inner_world_remove(world, child, id) + end + end + end + + if idr_r then + local count = 0 + local archetype_ids = idr_r.records + local ids = {} + local queue = {} + local records = idr_r.records + local counts = idr_r.counts + for archetype_id in archetype_ids do + local idr_r_archetype = archetypes[archetype_id] + local entities = idr_r_archetype.entities + local tr = records[archetype_id] + local tr_count = counts[archetype_id] + 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 + inner_world_remove(world, e, id) + end + end + end + end + + local function inner_world_delete(world: World, entity: Entity) + local entity_index = world.entity_index + local record = inner_entity_index_try_get(entity::number) + 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 component_index = world.component_index + local archetypes = world.archetypes + local tgt = ECS_PAIR(EcsWildcard, entity::number) + local rel = ECS_PAIR(entity::number, EcsWildcard) + + local idr_t = component_index[tgt] + local idr = component_index[entity::number] + 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.records do + local idr_archetype = archetypes[archetype_id] + + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + inner_world_delete(world, entities[i]) + end + + archetype_destroy(world, idr_archetype) + end + else + for archetype_id in idr.records do + local idr_archetype = archetypes[archetype_id] + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + inner_world_remove(world, entities[i], entity) + 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.records + 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::number) then + continue + end + local object = entity_index_get_alive( + entity_index, ECS_PAIR_SECOND(id::number)) + if object ~= entity then + continue + end + local id_record = component_index[id] + local flags = id_record.flags + local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE) + if flags_delete_mask ~= 0 then + for i = #entities, 1, -1 do + local child = entities[i] + inner_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 + inner_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.records + 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 + inner_world_delete(world, entities[i]) + end + archetype_destroy(world, idr_r_archetype) + end + else + local children = {} + local count = 0 + local ids = {} + local counts = idr_r.counts + local records = idr_r.records + for archetype_id in archetype_ids do + local idr_r_archetype = archetypes[archetype_id] + local entities = idr_r_archetype.entities + local tr = records[archetype_id] + local tr_count = counts[archetype_id] + 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, children) + count += n + end + for _, child in children do + for id in ids do + inner_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 = inner_entity_index_try_get_any(e_swap :: number) :: Record + + 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 :: number) + end + + local function inner_world_exists(world: World, entity: Entity): boolean + return inner_entity_index_try_get_any(entity :: number) ~= nil + end + + local function inner_world_contains(world: World, entity: Entity): boolean + return entity_index_is_alive(world.entity_index, entity) + end + + local function inner_world_cleanup(world: World) + for _, archetype in archetypes do + if #archetype.entities == 0 then + archetype_destroy(world, archetype) + end + end + + local new_archetypes = {} + local new_archetype_map = {} + + for index, archetype in archetypes do + new_archetypes[index] = archetype + new_archetype_map[archetype.type] = archetype + end + + archetypes = new_archetypes + archetype_index = new_archetype_map + + world.archetypes = new_archetypes + world.archetype_index = new_archetype_map + end + + world.entity = inner_world_entity + world.query = world_query :: any + world.remove = inner_world_remove + world.clear = inner_world_clear + world.delete = inner_world_delete + world.component = world_component + world.add = inner_world_add + world.set = inner_world_set + world.get = inner_world_get :: any + world.has = inner_world_has :: any + world.target = inner_world_target + world.parent = inner_world_parent + world.contains = inner_world_contains + world.exists = inner_world_exists + world.cleanup = inner_world_cleanup + world.each = world_each + world.children = world_children + world.range = world_range for i = 1, HI_COMPONENT_ID do local e = entity_index_new_id(entity_index) - world_add(self, e, EcsComponent) + inner_world_add(world, e, EcsComponent) end for i = HI_COMPONENT_ID + 1, EcsRest do @@ -2485,27 +2772,27 @@ local function world_new() 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) + inner_world_add(world, EcsName, EcsComponent) + inner_world_add(world, EcsOnChange, EcsComponent) + inner_world_add(world, EcsOnAdd, EcsComponent) + inner_world_add(world, EcsOnRemove, EcsComponent) + inner_world_add(world, EcsWildcard, EcsComponent) + inner_world_add(world, 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") + inner_world_set(world, EcsOnAdd, EcsName, "jecs.OnAdd") + inner_world_set(world, EcsOnRemove, EcsName, "jecs.OnRemove") + inner_world_set(world, EcsOnChange, EcsName, "jecs.OnChange") + inner_world_set(world, EcsWildcard, EcsName, "jecs.Wildcard") + inner_world_set(world, EcsChildOf, EcsName, "jecs.ChildOf") + inner_world_set(world, EcsComponent, EcsName, "jecs.Component") + inner_world_set(world, EcsOnDelete, EcsName, "jecs.OnDelete") + inner_world_set(world, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget") + inner_world_set(world, EcsDelete, EcsName, "jecs.Delete") + inner_world_set(world, EcsRemove, EcsName, "jecs.Remove") + inner_world_set(world, EcsName, EcsName, "jecs.Name") + inner_world_set(world, EcsRest, EcsRest, "jecs.Rest") - world_add(self, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) + inner_world_add(world, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) for i = EcsRest + 1, ecs_max_tag_id do entity_index_new_id(entity_index) @@ -2514,122 +2801,16 @@ local function world_new() for i, bundle in ecs_metadata do for ty, value in bundle do if value == NULL then - world_add(self, i, ty) + inner_world_add(world, i, ty) else - world_set(self, i, ty, value) + inner_world_add(world, i, ty, value) end end end - return self + return world end -World.new = world_new - -export type Entity = number | { __T: T } -export type Id = number | { __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: a) -> (), - - 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: & ((World, Entity, Id) -> a?) - & ((World, Entity, Id, Id) -> (a?, b?)) - & ((World, Entity, Id, Id, Id) -> (a?, b?, c?)) - & ((World, Entity, Id, Id, Id, Id) -> (a?, b?, c?, d?)), - - --- Returns whether the entity has the ID. - has: ((World, Entity, Id) -> boolean) - & ((World, Entity, Id, Id) -> boolean) - & ((World, Entity, Id, Id, Id) -> boolean) - & (World, Entity, Id, Id, Id, Id) -> boolean, - - --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. - parent: (self: World, entity: Entity) -> Entity, - - --- Checks if the world contains the given entity - contains: (self: World, entity: Entity) -> boolean, - - --- 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")) @@ -2648,8 +2829,15 @@ export type World = { -- end -- +local function ecs_is_tag(world: World, entity: Entity): 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 + return { - World = World :: { new: () -> World }, world = world_new :: () -> World, component = (ECS_COMPONENT :: any) :: () -> Entity, tag = (ECS_TAG :: any) :: () -> Entity, @@ -2689,6 +2877,7 @@ return { archetype_append_to_records = archetype_append_to_records, id_record_ensure = id_record_ensure, + component_record = id_record_get, archetype_create = archetype_create, archetype_ensure = archetype_ensure, find_insert = find_insert, @@ -2697,12 +2886,13 @@ return { create_edge_for_remove = create_edge_for_remove, archetype_traverse_add = archetype_traverse_add, archetype_traverse_remove = archetype_traverse_remove, + bulk_insert = ecs_bulk_insert, + bulk_remove = ecs_bulk_remove, 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, diff --git a/pesde-rbx.toml b/pesde-rbx.toml index 546b39e..b96e594 100644 --- a/pesde-rbx.toml +++ b/pesde-rbx.toml @@ -11,7 +11,7 @@ includes = [ license = "MIT" name = "marked/jecs" repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.6.1" +version = "0.7.2" [indices] default = "https://github.com/pesde-pkg/index" diff --git a/pesde.lock b/pesde.lock index d67f8e4..19456eb 100644 --- a/pesde.lock +++ b/pesde.lock @@ -2,5 +2,5 @@ # It is not intended for manual editing. format = 1 name = "marked/jecs" -version = "0.6.1" +version = "0.7.2" target = "luau" diff --git a/pesde.toml b/pesde.toml index ca8559e..fcf82a9 100644 --- a/pesde.toml +++ b/pesde.toml @@ -11,7 +11,7 @@ includes = [ license = "MIT" name = "marked/jecs" repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.6.1" +version = "0.7.2" [indices] default = "https://github.com/pesde-pkg/index"

+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, @@ -2405,9 +2411,9 @@ export type Observer = { query: QueryInner, } -type Observable = { - [i53]: { - [i53]: { +export type Observable = { + [Id]: { + [Id]: { { Observer } } } @@ -2426,51 +2432,44 @@ export type World = { observable: any, --- Creates a new entity - entity: (self: World) -> 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: Entity, index: number?) -> 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) -> (), + 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: U) -> (), + set: (self: World, id: Entity, component: Id, data: T) -> (), 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) - & ((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), + has: (self: World, entity: Entity, ...Id) -> boolean, --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. - parent: (self: World, entity: Entity) -> Entity, + 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) @@ -2501,6 +2500,7 @@ export type World = { return { World = World :: { new: () -> World }, + world = World.new :: () -> World, OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>, OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>, @@ -2516,7 +2516,7 @@ return { Name = EcsName :: Entity, Rest = EcsRest :: Entity, - pair = ECS_PAIR :: (first: P, second: O) -> Pair, + pair = (ECS_PAIR :: any) :: (first: Id