From 6e1bbbaedcfa7b01464509eaf464c0254072b7b9 Mon Sep 17 00:00:00 2001 From: marked Date: Wed, 12 Mar 2025 21:03:42 +0100 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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"

+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