diff --git a/.gitattributes b/.gitattributes old mode 100644 new mode 100755 diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.md b/.github/ISSUE_TEMPLATE/BUG-REPORT.md old mode 100644 new mode 100755 diff --git a/.github/ISSUE_TEMPLATE/DOCUMENTATION.md b/.github/ISSUE_TEMPLATE/DOCUMENTATION.md old mode 100644 new mode 100755 diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md old mode 100644 new mode 100755 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md old mode 100644 new mode 100755 diff --git a/.github/workflows/analysis.yaml b/.github/workflows/analysis.yaml old mode 100644 new mode 100755 diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml old mode 100644 new mode 100755 diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml old mode 100644 new mode 100755 diff --git a/.github/workflows/unit-testing.yaml b/.github/workflows/unit-testing.yaml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.luaurc b/.luaurc old mode 100644 new mode 100755 diff --git a/.prettierrc b/.prettierrc old mode 100644 new mode 100755 diff --git a/.stylua.toml b/.stylua.toml old mode 100644 new mode 100755 diff --git a/CHANGELOG.md b/CHANGELOG.md old mode 100644 new mode 100755 index 28a4d81..0c4c7aa --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +### Added +- `jecs.component_record` for retrieving the component_record of a component. +- `Column` and `ColumnsMap` types for typescript. + +### Changed +- The fields `archetype.records[id]` and `archetype.counts[id` have been removed from the archetype struct and been moved to the component record `component_index[id].records[archetype.id]` and `component_index[id].counts[archetype.id]` respectively. +- Removed the metatable `jecs.World`. Use `jecs.world()` to create your World. +- Archetypes will no longer be garbage collected when invalidated, allowing them to be recycled to save a lot of performance during frequent deletion. +- Removed `jecs.entity_index_try_get_fast`. Use `jecs.entity_index_try_get` instead. + ## 0.6.1 ### Changed diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/addons/observers.luau b/addons/observers.luau old mode 100644 new mode 100755 diff --git a/assets/image-1.png b/assets/image-1.png old mode 100644 new mode 100755 diff --git a/assets/image-2.png b/assets/image-2.png old mode 100644 new mode 100755 diff --git a/assets/image-3.png b/assets/image-3.png old mode 100644 new mode 100755 diff --git a/assets/image-4.png b/assets/image-4.png old mode 100644 new mode 100755 diff --git a/assets/image-5.png b/assets/image-5.png old mode 100644 new mode 100755 diff --git a/assets/jecs_darkmode.svg b/assets/jecs_darkmode.svg old mode 100644 new mode 100755 diff --git a/assets/jecs_lightmode.svg b/assets/jecs_lightmode.svg old mode 100644 new mode 100755 diff --git a/assets/logo_old.png b/assets/logo_old.png old mode 100644 new mode 100755 diff --git a/bench.project.json b/bench.project.json old mode 100644 new mode 100755 diff --git a/benches/cached.luau b/benches/cached.luau old mode 100644 new mode 100755 diff --git a/benches/general.luau b/benches/general.luau old mode 100644 new mode 100755 index 3df5538..51ef332 --- a/benches/general.luau +++ b/benches/general.luau @@ -14,7 +14,7 @@ local pair = jecs.pair do TITLE("create") - local world = jecs.World.new() + local world = jecs.world() BENCH("entity", function() for i = 1, START(N) do @@ -35,7 +35,7 @@ end do TITLE("set") - local world = jecs.World.new() + local world = jecs.world() local A = world:component() local entities = table.create(N) @@ -69,7 +69,7 @@ end do TITLE("set relationship") - local world = jecs.World.new() + local world = jecs.world() local A = world:component() local entities = table.create(N) @@ -103,7 +103,7 @@ end do TITLE("get") - local world = jecs.World.new() + local world = jecs.world() local A = world:component() local B = world:component() local C = world:component() @@ -147,7 +147,7 @@ do TITLE("target") BENCH("1st target", function() - local world = jecs.World.new() + local world = jecs.world() local A = world:component() local B = world:component() local C = world:component() @@ -179,7 +179,7 @@ do local function view_bench(n: number) BENCH(`{n} entities per archetype`, function() - local world = jecs.World.new() + local world = jecs.world() local A = world:component() local B = world:component() @@ -205,7 +205,7 @@ do end) BENCH(`inlined query`, function() - local world = jecs.World.new() + local world = jecs.world() local A = world:component() local B = world:component() local C = world:component() @@ -225,12 +225,22 @@ do local archetypes = world:query(A, B, C, D):archetypes() START() + -- for _, archetype in archetypes do + -- local columns, records = archetype.columns, archetype.records + -- local a = columns[records[A]] + -- local b = columns[records[B]] + -- local c = columns[records[C]] + -- local d = columns[records[D]] + -- for row in archetype.entities do + -- local _1, _2, _3, _4 = a[row], b[row], c[row], d[row] + -- end + -- end for _, archetype in archetypes do - local columns, records = archetype.columns, archetype.records - local a = columns[records[A]] - local b = columns[records[B]] - local c = columns[records[C]] - local d = columns[records[D]] + local columns_map = archetype.columns_map + local a = columns_map[A] + local b = columns_map[B] + local c = columns_map[C] + local d = columns_map[D] for row in archetype.entities do local _1, _2, _3, _4 = a[row], b[row], c[row], d[row] end diff --git a/benches/query.luau b/benches/query.luau old mode 100644 new mode 100755 index ecd7fd5..77f0fe0 --- a/benches/query.luau +++ b/benches/query.luau @@ -15,7 +15,7 @@ type i53 = number do TITLE(testkit.color.white_underline("Jecs query")) - local ecs = jecs.World.new() + local ecs = jecs.world() do TITLE("one component in common") diff --git a/benches/visual/despawn.bench.luau b/benches/visual/despawn.bench.luau old mode 100644 new mode 100755 index 5c424d9..a7b4a8d --- a/benches/visual/despawn.bench.luau +++ b/benches/visual/despawn.bench.luau @@ -6,8 +6,7 @@ local Matter = require(ReplicatedStorage.DevPackages.Matter) local ecr = require(ReplicatedStorage.DevPackages.ecr) local jecs = require(ReplicatedStorage.Lib) local pair = jecs.pair -local newWorld = Matter.World.new() -local ecs = jecs.World.new() +local ecs = jecs.world() local mirror = require(ReplicatedStorage.mirror) local mcs = mirror.World.new() @@ -26,8 +25,6 @@ mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete)) local E4 = mcs:entity() mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete)) -local registry2 = ecr.registry() - return { ParameterGenerator = function() local j = ecs:entity() diff --git a/benches/visual/insertion.bench.luau b/benches/visual/insertion.bench.luau old mode 100644 new mode 100755 index 802b406..9b3d25d --- a/benches/visual/insertion.bench.luau +++ b/benches/visual/insertion.bench.luau @@ -2,30 +2,11 @@ --!native local ReplicatedStorage = game:GetService("ReplicatedStorage") -local Matter = require(ReplicatedStorage.DevPackages.Matter) -local ecr = require(ReplicatedStorage.DevPackages.ecr) local jecs = require(ReplicatedStorage.Lib:Clone()) -local ecs = jecs.World.new() +local ecs = jecs.world() local mirror = require(ReplicatedStorage.mirror:Clone()) local mcs = mirror.World.new() -local A1 = Matter.component() -local A2 = Matter.component() -local A3 = Matter.component() -local A4 = Matter.component() -local A5 = Matter.component() -local A6 = Matter.component() -local A7 = Matter.component() -local A8 = Matter.component() - -local B1 = ecr.component() -local B2 = ecr.component() -local B3 = ecr.component() -local B4 = ecr.component() -local B5 = ecr.component() -local B6 = ecr.component() -local B7 = ecr.component() -local B8 = ecr.component() local C1 = ecs:component() local C2 = ecs:component() @@ -44,7 +25,6 @@ local E6 = mcs:component() local E7 = mcs:component() local E8 = mcs:component() -local registry2 = ecr.registry() return { ParameterGenerator = function() return diff --git a/benches/visual/query.bench.luau b/benches/visual/query.bench.luau old mode 100644 new mode 100755 index 16602f1..6516900 --- a/benches/visual/query.bench.luau +++ b/benches/visual/query.bench.luau @@ -2,14 +2,14 @@ --!native local ReplicatedStorage = game:GetService("ReplicatedStorage") -local Matter = require(ReplicatedStorage.DevPackages["_Index"]["matter-ecs_matter@0.8.1"].matter) -local ecr = require(ReplicatedStorage.DevPackages["_Index"]["centau_ecr@0.8.0"].ecr) +local Matter = require(ReplicatedStorage.DevPackages.matter) +local ecr = require(ReplicatedStorage.DevPackages.ecr) local newWorld = Matter.World.new() -local jecs = require(ReplicatedStorage.Lib) -local mirror = require(ReplicatedStorage.mirror) -local mcs = mirror.World.new() -local ecs = jecs.World.new() +local jecs = require(ReplicatedStorage.Lib:Clone()) +local mirror = require(ReplicatedStorage.mirror:Clone()) +local mcs = mirror.world() +local ecs = jecs.world() local A1 = Matter.component() local A2 = Matter.component() @@ -38,14 +38,14 @@ local D6 = ecs:component() local D7 = ecs:component() local D8 = ecs:component() -local E1 = mcs:entity() -local E2 = mcs:entity() -local E3 = mcs:entity() -local E4 = mcs:entity() -local E5 = mcs:entity() -local E6 = mcs:entity() -local E7 = mcs:entity() -local E8 = mcs:entity() +local E1 = mcs:component() +local E2 = mcs:component() +local E3 = mcs:component() +local E4 = mcs:component() +local E5 = mcs:component() +local E6 = mcs:component() +local E7 = mcs:component() +local E8 = mcs:component() local registry2 = ecr.registry() @@ -146,13 +146,18 @@ return { end, Functions = { - Matter = function() - for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do - end - end, + -- Matter = function() + -- for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do + -- end + -- end, - ECR = function() - for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do + -- ECR = function() + -- for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do + -- end + -- end, + -- + Mirror = function() + for entityId, firstComponent in mcs:query(E2, E4, E6, E8) do end end, diff --git a/benches/visual/remove.bench.luau b/benches/visual/remove.bench.luau old mode 100644 new mode 100755 index 5af2a17..40f4a3f --- a/benches/visual/remove.bench.luau +++ b/benches/visual/remove.bench.luau @@ -6,7 +6,7 @@ local Matter = require(ReplicatedStorage.DevPackages.Matter) local ecr = require(ReplicatedStorage.DevPackages.ecr) local jecs = require(ReplicatedStorage.Lib) local pair = jecs.pair -local ecs = jecs.World.new() +local ecs = jecs.world() local mirror = require(ReplicatedStorage.mirror) local mcs = mirror.World.new() diff --git a/benches/visual/spawn.bench.luau b/benches/visual/spawn.bench.luau old mode 100644 new mode 100755 diff --git a/benches/visual/wally.toml b/benches/visual/wally.toml old mode 100644 new mode 100755 diff --git a/default.project.json b/default.project.json old mode 100644 new mode 100755 diff --git a/demo/.gitignore b/demo/.gitignore old mode 100644 new mode 100755 diff --git a/demo/README.md b/demo/README.md old mode 100644 new mode 100755 diff --git a/demo/default.project.json b/demo/default.project.json old mode 100644 new mode 100755 diff --git a/demo/src/ReplicatedStorage/collect.luau b/demo/src/ReplicatedStorage/collect.luau old mode 100644 new mode 100755 diff --git a/demo/src/ReplicatedStorage/components.luau b/demo/src/ReplicatedStorage/components.luau old mode 100644 new mode 100755 diff --git a/demo/src/ReplicatedStorage/main.client.luau b/demo/src/ReplicatedStorage/main.client.luau old mode 100644 new mode 100755 diff --git a/demo/src/ReplicatedStorage/observers_add.luau b/demo/src/ReplicatedStorage/observers_add.luau old mode 100644 new mode 100755 diff --git a/demo/src/ReplicatedStorage/remotes.luau b/demo/src/ReplicatedStorage/remotes.luau old mode 100644 new mode 100755 diff --git a/demo/src/ReplicatedStorage/schedule.luau b/demo/src/ReplicatedStorage/schedule.luau old mode 100644 new mode 100755 diff --git a/demo/src/ReplicatedStorage/systems/receive_replication.luau b/demo/src/ReplicatedStorage/systems/receive_replication.luau old mode 100644 new mode 100755 diff --git a/demo/src/ReplicatedStorage/types.luau b/demo/src/ReplicatedStorage/types.luau old mode 100644 new mode 100755 diff --git a/demo/src/ServerScriptService/main.server.luau b/demo/src/ServerScriptService/main.server.luau old mode 100644 new mode 100755 diff --git a/demo/src/ServerScriptService/systems/life_is_painful.luau b/demo/src/ServerScriptService/systems/life_is_painful.luau old mode 100644 new mode 100755 diff --git a/demo/src/ServerScriptService/systems/players_added.luau b/demo/src/ServerScriptService/systems/players_added.luau old mode 100644 new mode 100755 diff --git a/demo/src/ServerScriptService/systems/poison_hurts.luau b/demo/src/ServerScriptService/systems/poison_hurts.luau old mode 100644 new mode 100755 diff --git a/demo/src/ServerScriptService/systems/replication.luau b/demo/src/ServerScriptService/systems/replication.luau old mode 100644 new mode 100755 diff --git a/demo/wally.toml b/demo/wally.toml old mode 100644 new mode 100755 diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts old mode 100644 new mode 100755 diff --git a/docs/api/jecs.md b/docs/api/jecs.md old mode 100644 new mode 100755 diff --git a/docs/api/query.md b/docs/api/query.md old mode 100644 new mode 100755 diff --git a/docs/api/world.md b/docs/api/world.md old mode 100644 new mode 100755 diff --git a/docs/index.md b/docs/index.md old mode 100644 new mode 100755 diff --git a/docs/learn/contributing/coverage.md b/docs/learn/contributing/coverage.md old mode 100644 new mode 100755 diff --git a/docs/learn/contributing/guidelines.md b/docs/learn/contributing/guidelines.md old mode 100644 new mode 100755 diff --git a/docs/learn/contributing/issues.md b/docs/learn/contributing/issues.md old mode 100644 new mode 100755 diff --git a/docs/learn/contributing/pull-requests.md b/docs/learn/contributing/pull-requests.md old mode 100644 new mode 100755 diff --git a/docs/learn/overview.md b/docs/learn/overview.md old mode 100644 new mode 100755 diff --git a/docs/learn/public/jecs_logo.svg b/docs/learn/public/jecs_logo.svg old mode 100644 new mode 100755 diff --git a/docs/public/coverage/ansi.luau.html b/docs/public/coverage/ansi.luau.html old mode 100644 new mode 100755 diff --git a/docs/public/coverage/entity_visualiser.luau.html b/docs/public/coverage/entity_visualiser.luau.html old mode 100644 new mode 100755 diff --git a/docs/public/coverage/index.html b/docs/public/coverage/index.html old mode 100644 new mode 100755 diff --git a/docs/public/coverage/jecs.luau.html b/docs/public/coverage/jecs.luau.html old mode 100644 new mode 100755 diff --git a/docs/public/coverage/lifetime_tracker.luau.html b/docs/public/coverage/lifetime_tracker.luau.html old mode 100644 new mode 100755 diff --git a/docs/public/coverage/testkit.luau.html b/docs/public/coverage/testkit.luau.html old mode 100644 new mode 100755 diff --git a/docs/public/coverage/tests.luau.html b/docs/public/coverage/tests.luau.html old mode 100644 new mode 100755 diff --git a/docs/public/jecs_logo.svg b/docs/public/jecs_logo.svg old mode 100644 new mode 100755 diff --git a/docs/resources.md b/docs/resources.md old mode 100644 new mode 100755 diff --git a/examples/README.md b/examples/README.md old mode 100644 new mode 100755 diff --git a/examples/luau/entities/basics.luau b/examples/luau/entities/basics.luau old mode 100644 new mode 100755 diff --git a/examples/luau/entities/hierarchy.luau b/examples/luau/entities/hierarchy.luau old mode 100644 new mode 100755 diff --git a/examples/luau/hooks/cleanup.luau b/examples/luau/hooks/cleanup.luau old mode 100644 new mode 100755 diff --git a/examples/luau/queries/basics.luau b/examples/luau/queries/basics.luau old mode 100644 new mode 100755 diff --git a/examples/luau/queries/changetracking.luau b/examples/luau/queries/changetracking.luau old mode 100644 new mode 100755 diff --git a/examples/luau/queries/spatial_grids.luau b/examples/luau/queries/spatial_grids.luau old mode 100644 new mode 100755 diff --git a/examples/luau/queries/wildcards.luau b/examples/luau/queries/wildcards.luau old mode 100644 new mode 100755 diff --git a/jecs.d.ts b/jecs.d.ts old mode 100644 new mode 100755 index a9caeae..cccf6d5 --- a/jecs.d.ts +++ b/jecs.d.ts @@ -41,16 +41,15 @@ type Nullable = { [K in keyof T]: T[K] | undefined }; type InferComponents = { [K in keyof A]: InferComponent }; type ArchetypeId = number; -type Column = unknown[]; +export type Column = T[]; -export type Archetype = { +export type Archetype = { id: number; types: number[]; type: string; entities: number[]; - columns: Column[]; - records: number[]; - counts: number[]; + columns: Column[]; + columns_map: { [K in keyof T]: Column } }; type Iter = IterableFunction>; @@ -65,7 +64,7 @@ export type CachedQuery = { * Returns the matched archetypes of the query * @returns An array of archetypes of the query */ - archetypes(): Archetype[]; + archetypes(): Archetype[]; } & Iter; export type Query = { @@ -99,7 +98,7 @@ export type Query = { * Returns the matched archetypes of the query * @returns An array of archetypes of the query */ - archetypes(): Archetype[]; + archetypes(): Archetype[]; } & Iter; export class World { @@ -310,3 +309,11 @@ export declare const Delete: Tag; export declare const Remove: Tag; export declare const Name: Entity; export declare const Rest: Entity; + +export type ComponentRecord = { + records: Map, + counts: Map, + size: number, +} + +export function component_record(world: World, id: Id): ComponentRecord diff --git a/jecs.luau b/jecs.luau old mode 100644 new mode 100755 index 416bc1f..925308e --- a/jecs.luau +++ b/jecs.luau @@ -1,4 +1,3 @@ - --!optimize 2 --!native --!strict @@ -7,92 +6,156 @@ type i53 = number type i24 = number -type Ty = { i53 } +type Ty = { Entity } type ArchetypeId = number type Column = { any } type Map = { [K]: V } -type ecs_archetype_t = { - id: number, - types: Ty, - type: string, - entities: { number }, - columns: { Column }, - records: { [i53]: number }, - counts: { [i53]: number }, -} - export type Archetype = { id: number, types: Ty, type: string, - entities: { number }, + entities: { Entity }, columns: { Column }, - records: { [Id]: number }, - counts: { [Id]: number }, + columns_map: { [Id]: Column }, + dead: boolean, } -type ecs_record_t = { - archetype: ecs_archetype_t, - row: number, - dense: i24, -} - -type ecs_id_record_t = { - cache: { number }, - counts: { number }, - flags: number, - size: number, - hooks: { - on_add: ((entity: i53, id: i53, data: any?) -> ())?, - on_change: ((entity: i53, id: i53, data: any) -> ())?, - on_remove: ((entity: i53, id: i53) -> ())?, - }, -} - -type ecs_id_index_t = Map - -type ecs_archetypes_map_t = { [string]: ecs_archetype_t } - -type ecs_archetypes_t = { ecs_archetype_t } - -type ecs_entity_index_t = { - dense_array: Map, - sparse_array: Map, - alive_count: number, - max_id: number, - range_begin: number?, - range_end: number? -} - -type ecs_query_data_t = { - compatible_archetypes: { ecs_archetype_t }, +export type QueryInner = { + compatible_archetypes: { Archetype }, ids: { i53 }, filter_with: { i53 }, filter_without: { i53 }, next: () -> (number, ...any), - world: ecs_world_t, + world: World, } -type ecs_observer_t = { - callback: (archetype: ecs_archetype_t) -> (), - query: ecs_query_data_t, +export type Entity = number | { __T: T } +export type Id = number | { __T: T } +export type Pair = Id

+type ecs_id_t = Id | Pair | Pair<"Tag", T> +export type Item = (self: Query) -> (Entity, T...) +export type Iter = (query: Query) -> () -> (Entity, T...) + +export type Query = typeof(setmetatable( + {} :: { + iter: Iter, + with: (self: Query, ...Id) -> Query, + without: (self: Query, ...Id) -> Query, + archetypes: (self: Query) -> { Archetype }, + cached: (self: Query) -> Query, + }, + {} :: { + __iter: Iter + } +)) + +export type Observer = { + callback: (archetype: Archetype) -> (), + query: QueryInner, } -type ecs_observable_t = Map> +export type World = { + archetype_edges: Map>, + archetype_index: { [string]: Archetype }, + archetypes: Archetypes, + component_index: ComponentIndex, + entity_index: EntityIndex, + ROOT_ARCHETYPE: Archetype, -type ecs_world_t = { - archetype_edges: Map>, - entity_index: ecs_entity_index_t, - component_index: ecs_id_index_t, - archetypes: ecs_archetypes_t, - archetype_index: ecs_archetypes_map_t, - max_archetype_id: number, max_component_id: number, - ROOT_ARCHETYPE: ecs_archetype_t, - observable: Map>, + max_archetype_id: number, + + observable: Map>, + + --- Enforce a check on entities to be created within desired range + range: (self: World, range_begin: number, range_end: number?) -> (), + + --- Creates a new entity + entity: (self: World, id: Entity?) -> Entity, + --- Creates a new entity located in the first 256 ids. + --- These should be used for static components for fast access. + component: (self: World) -> Entity, + --- Gets the target of an relationship. For example, when a user calls + --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. + target: (self: World, id: Entity, relation: Id, index: number?) -> Entity?, + --- Deletes an entity and all it's related components and relationships. + delete: (self: World, id: Entity) -> (), + + --- Adds a component to the entity with no value + add: (self: World, id: Entity, component: Id) -> (), + --- Assigns a value to a component on the given entity + set: (self: World, id: Entity, component: Id, data: a) -> (), + + cleanup: (self: World) -> (), + -- Clears an entity from the world + clear: (self: World, id: Id) -> (), + --- Removes a component from the given entity + remove: (self: World, id: Entity, component: Id) -> (), + --- Retrieves the value of up to 4 components. These values may be nil. + get: & ((World, Entity, Id) -> a?) + & ((World, Entity, Id, Id) -> (a?, b?)) + & ((World, Entity, Id, Id, Id) -> (a?, b?, c?)) + & ((World, Entity, Id, Id, Id, Id) -> (a?, b?, c?, d?)), + + --- Returns whether the entity has the ID. + has: ((World, Entity, Id) -> boolean) + & ((World, Entity, Id, Id) -> boolean) + & ((World, Entity, Id, Id, Id) -> boolean) + & (World, Entity, Id, Id, Id, Id) -> boolean, + + --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. + parent: (self: World, entity: Entity) -> Entity?, + + --- Checks if the world contains the given entity + contains: (self: World, entity: Entity) -> boolean, + + --- Checks if the entity exists + exists: (self: World, entity: Entity) -> boolean, + + each: (self: World, id: Id) -> () -> Entity, + + children: (self: World, id: Id) -> () -> Entity, + + --- Searches the world for entities that match a given query + query: ((World, Id) -> Query) + & ((World, Id, Id) -> Query) + & ((World, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id, Id, Id, ...Id) -> Query) +} + +export type Record = { + archetype: Archetype, + row: number, + dense: i24, +} +export type ComponentRecord = { + records: { [Id]: number }, + counts: { [Id]: number }, + flags: number, + size: number, + hooks: { + on_add: ((entity: Entity, id: Entity, value: T?) -> ())?, + on_change: ((entity: Entity, id: Entity, value: T) -> ())?, + on_remove: ((entity: Entity, id: Entity) -> ())?, + }, +} +export type ComponentIndex = Map +export type Archetypes = { [Id]: Archetype } + +export type EntityIndex = { + dense_array: Map, + sparse_array: Map, + alive_count: number, + max_id: number, + range_begin: number?, + range_end: number? } -- stylua: ignore start @@ -223,9 +286,9 @@ local function ECS_PAIR_SECOND(e: i53): i24 end local function entity_index_try_get_any( - entity_index: ecs_entity_index_t, + entity_index: EntityIndex, entity: number -): ecs_record_t? +): Record? local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] if not r or r.dense == 0 then @@ -235,8 +298,8 @@ local function entity_index_try_get_any( return r end -local function entity_index_try_get(entity_index: ecs_entity_index_t, entity: number): ecs_record_t? - local r = entity_index_try_get_any(entity_index, entity) +local function entity_index_try_get(entity_index: EntityIndex, entity: Entity): Record? + local r = entity_index_try_get_any(entity_index, entity :: number) if r then local r_dense = r.dense if r_dense > entity_index.alive_count then @@ -249,29 +312,19 @@ local function entity_index_try_get(entity_index: ecs_entity_index_t, entity: nu return r end -local function entity_index_try_get_fast(entity_index: ecs_entity_index_t, entity: number): ecs_record_t? - local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] - if r then - if entity_index.dense_array[r.dense] ~= entity then - return nil - end - end - return r -end - -local function entity_index_is_alive(entity_index: ecs_entity_index_t, entity: i53) +local function entity_index_is_alive(entity_index: EntityIndex, entity: Entity): boolean return entity_index_try_get(entity_index, entity) ~= nil end -local function entity_index_get_alive(entity_index: ecs_entity_index_t, entity: i53): i53? - local r = entity_index_try_get_any(entity_index, entity) +local function entity_index_get_alive(entity_index: EntityIndex, entity: Entity): Entity? + local r = entity_index_try_get_any(entity_index, entity :: number) if r then return entity_index.dense_array[r.dense] end return nil end -local function ecs_get_alive(world, entity) +local function ecs_get_alive(world: World, entity: Entity): Entity if entity == 0 then return 0 end @@ -282,7 +335,7 @@ local function ecs_get_alive(world, entity) return entity end - if entity > ECS_ENTITY_MASK then + if (entity :: number) > ECS_ENTITY_MASK then return 0 end @@ -296,7 +349,7 @@ end local ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY = "Entity is outside range" -local function entity_index_new_id(entity_index: ecs_entity_index_t): i53 +local function entity_index_new_id(entity_index: EntityIndex): Entity local dense_array = entity_index.dense_array local alive_count = entity_index.alive_count local sparse_array = entity_index.sparse_array @@ -317,28 +370,27 @@ local function entity_index_new_id(entity_index: ecs_entity_index_t): i53 alive_count += 1 entity_index.alive_count = alive_count dense_array[alive_count] = id - sparse_array[id] = { dense = alive_count } :: ecs_record_t + sparse_array[id] = { dense = alive_count } :: Record return id end -local function ecs_pair_first(world: ecs_world_t, e: i53) +local function ecs_pair_first(world: World, e: i53) local pred = ECS_PAIR_FIRST(e) return ecs_get_alive(world, pred) end -local function ecs_pair_second(world: ecs_world_t, e: i53) +local function ecs_pair_second(world: World, e: i53) local obj = ECS_PAIR_SECOND(e) return ecs_get_alive(world, obj) end -local function query_match(query: ecs_query_data_t, - archetype: ecs_archetype_t) - local records = archetype.records +local function query_match(query: QueryInner, archetype: Archetype) + local columns_map = archetype.columns_map local with = query.filter_with for _, id in with do - if not records[id] then + if not columns_map[id] then return false end end @@ -346,7 +398,7 @@ local function query_match(query: ecs_query_data_t, local without = query.filter_without if without then for _, id in without do - if records[id] then + if columns_map[id] then return false end end @@ -355,8 +407,7 @@ local function query_match(query: ecs_query_data_t, return true end -local function find_observers(world: ecs_world_t, event: i53, - component: i53): { ecs_observer_t }? +local function find_observers(world: World, event: Id, component: Id): { Observer }? local cache = world.observable[event] if not cache then return nil @@ -365,20 +416,19 @@ local function find_observers(world: ecs_world_t, event: i53, end local function archetype_move( - entity_index: ecs_entity_index_t, - to: ecs_archetype_t, + entity_index: EntityIndex, + to: Archetype, dst_row: i24, - from: ecs_archetype_t, + from: Archetype, src_row: i24 ) local src_columns = from.columns - local dst_columns = to.columns local dst_entities = to.entities local src_entities = from.entities local last = #src_entities local id_types = from.types - local records = to.records + local columns_map = to.columns_map for i, column in src_columns do if column == NULL_ARRAY then @@ -386,11 +436,11 @@ local function archetype_move( end -- Retrieves the new column index from the source archetype's record from each component -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. - local tr = records[id_types[i]] + local dst_column = columns_map[id_types[i]] -- Sometimes target column may not exist, e.g. when you remove a component. - if tr then - dst_columns[tr][dst_row] = column[src_row] + if dst_column then + dst_column[dst_row] = column[src_row] end -- If the entity is the last row in the archetype then swapping it would be meaningless. @@ -418,15 +468,15 @@ local function archetype_move( local sparse_array = entity_index.sparse_array - local record1 = sparse_array[ECS_ENTITY_T_LO(e1)] - local record2 = sparse_array[ECS_ENTITY_T_LO(e2)] + local record1 = sparse_array[ECS_ENTITY_T_LO(e1 :: number)] + local record2 = sparse_array[ECS_ENTITY_T_LO(e2 :: number)] record1.row = dst_row record2.row = src_row end local function archetype_append( - entity: i53, - archetype: ecs_archetype_t + entity: Entity, + archetype: Archetype ): number local entities = archetype.entities local length = #entities + 1 @@ -435,10 +485,10 @@ local function archetype_append( end local function new_entity( - entity: i53, - record: ecs_record_t, - archetype: ecs_archetype_t -): ecs_record_t + entity: Entity, + record: Record, + archetype: Archetype +): Record local row = archetype_append(entity, archetype) record.archetype = archetype record.row = row @@ -446,10 +496,10 @@ local function new_entity( end local function entity_move( - entity_index: ecs_entity_index_t, - entity: i53, - record: ecs_record_t, - to: ecs_archetype_t + entity_index: EntityIndex, + entity: Entity, + record: Record, + to: Archetype ) local sourceRow = record.row local from = record.archetype @@ -459,24 +509,23 @@ local function entity_move( record.row = dst_row end -local function hash(arr: { number }): string +local function hash(arr: { Entity }): string return table.concat(arr, "_") end -local function fetch(id: i53, records: { number }, - columns: { Column }, row: number): any - local tr = records[id] +local function fetch(id: Id, columns_map: { [Entity]: Column }, row: number): any + local column = columns_map[id] - if not tr then + if not column then return nil end - return columns[tr][row] + return column[row] end -local function world_get(world: ecs_world_t, entity: i53, - a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any - local record = entity_index_try_get_fast(world.entity_index, entity) +local function world_get(world: World, entity: Entity, + a: Id, b: Id?, c: Id?, d: Id?, e: Id?): ...any + local record = entity_index_try_get(world.entity_index, entity) if not record then return nil end @@ -486,27 +535,26 @@ local function world_get(world: ecs_world_t, entity: i53, return nil end - local records = archetype.records - local columns = archetype.columns + local columns_map = archetype.columns_map local row = record.row - local va = fetch(a, records, columns, row) + local va = fetch(a, columns_map, row) if not b then return va elseif not c then - return va, fetch(b, records, columns, row) + return va, fetch(a, columns_map, row) elseif not d then - return va, fetch(b, records, columns, row), fetch(c, records, columns, row) + return va, fetch(b, columns_map, row), fetch(c, columns_map, row) elseif not e then - return va, fetch(b, records, columns, row), fetch(c, records, columns, row), fetch(d, records, columns, row) + return va, fetch(b, columns_map, row), fetch(c, columns_map, row), fetch(d, columns_map, row) else error("args exceeded") end end -local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): boolean - local record = entity_index_try_get_fast(world.entity_index, entity) +local function world_has_one_inline(world: World, entity: Entity, id: i53): boolean + local record = entity_index_try_get(world.entity_index, entity) if not record then return false end @@ -516,44 +564,12 @@ local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): b return false end - local records = archetype.records - - return records[id] ~= nil + return archetype.columns_map[id] ~= nil end -local function ecs_is_tag(world: ecs_world_t, entity: i53): boolean - local idr = world.component_index[entity] - if idr then - return bit32.band(idr.flags, ECS_ID_IS_TAG) ~= 0 - end - return not world_has_one_inline(world, entity, EcsComponent) -end - -local function world_has(world: ecs_world_t, entity: i53, - a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean - - local record = entity_index_try_get_fast(world.entity_index, entity) - if not record then - return false - end - - local archetype = record.archetype - if not archetype then - return false - end - - local records = archetype.records - - return records[a] ~= nil and - (b == nil or records[b] ~= nil) and - (c == nil or records[c] ~= nil) and - (d == nil or records[d] ~= nil) and - (e == nil or error("args exceeded")) -end - -local function world_target(world: ecs_world_t, entity: i53, relation: i24, index: number?): i24? - local nth = index or 0 - local record = entity_index_try_get_fast(world.entity_index, entity) +local function world_target(world: World, entity: Entity, relation: Id, index: number?): Entity? + local entity_index = world.entity_index + local record = entity_index_try_get(entity_index, entity) if not record then return nil end @@ -563,24 +579,33 @@ local function world_target(world: ecs_world_t, entity: i53, relation: i24, inde return nil end - local r = ECS_PAIR(relation, EcsWildcard) + local r = ECS_PAIR(relation :: number, EcsWildcard) + local idr = world.component_index[r] - local count = archetype.counts[r] + if not idr then + return nil + end + + local archetype_id = archetype.id + local count = idr.counts[archetype_id] if not count then return nil end + local nth = index or 0 + if nth >= count then nth = nth + count + 1 end - nth = archetype.types[nth + archetype.records[r]] + nth = archetype.types[nth + idr.records[archetype_id]] + if not nth then return nil end - return entity_index_get_alive(world.entity_index, - ECS_PAIR_SECOND(nth)) + return entity_index_get_alive(entity_index, + ECS_PAIR_SECOND(nth :: number)) end local function ECS_ID_IS_WILDCARD(e: i53): boolean @@ -589,10 +614,21 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean return first == EcsWildcard or second == EcsWildcard end -local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t +local function id_record_get(world: World, id: Entity): ComponentRecord? + local component_index = world.component_index + local idr: ComponentRecord = component_index[id] + + if idr then + return idr + end + + return nil +end + +local function id_record_ensure(world: World, id: Entity): ComponentRecord local component_index = world.component_index local entity_index = world.entity_index - local idr: ecs_id_record_t? = component_index[id] + local idr: ComponentRecord? = component_index[id] if idr then return idr @@ -601,12 +637,12 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t local flags = ECS_ID_MASK local relation = id local target = 0 - local is_pair = ECS_IS_PAIR(id) + local is_pair = ECS_IS_PAIR(id :: number) if is_pair then - relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53 + relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id :: number)) :: i53 ecs_assert(relation and entity_index_is_alive( entity_index, relation), ECS_INTERNAL_ERROR) - target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53 + target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id :: number)) :: i53 ecs_assert(target and entity_index_is_alive( entity_index, target), ECS_INTERNAL_ERROR) end @@ -638,7 +674,7 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t idr = { size = 0, - cache = {}, + records = {}, counts = {}, flags = flags, hooks = { @@ -646,7 +682,7 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t on_change = on_change, on_remove = on_remove, }, - } :: ecs_id_record_t + } :: ComponentRecord component_index[id] = idr @@ -654,80 +690,82 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t end local function archetype_append_to_records( - idr: ecs_id_record_t, - archetype: ecs_archetype_t, + idr: ComponentRecord, + archetype_id: number, + columns_map: { [Id]: Column }, id: i53, - index: number + index: number, + column: Column ) - local archetype_id = archetype.id - local archetype_records = archetype.records - local archetype_counts = archetype.counts - local idr_columns = idr.cache + local idr_records = idr.records local idr_counts = idr.counts - local tr = idr_columns[archetype_id] + local tr = idr_records[archetype_id] if not tr then - idr_columns[archetype_id] = index + idr_records[archetype_id] = index idr_counts[archetype_id] = 1 - - archetype_records[id] = index - archetype_counts[id] = 1 + columns_map[id] = column else local max_count = idr_counts[archetype_id] + 1 idr_counts[archetype_id] = max_count - archetype_counts[id] = max_count end end -local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: i53?): ecs_archetype_t +local function archetype_register(world: World, archetype: Archetype) + local archetype_id = archetype.id + local columns_map = archetype.columns_map + local columns = archetype.columns + for i, component_id in archetype.types do + local idr = id_record_ensure(world, component_id) + local is_tag = bit32.band(idr.flags, ECS_ID_IS_TAG) ~= 0 + local column = if is_tag then NULL_ARRAY else {} + columns[i] = column + + archetype_append_to_records(idr, archetype_id, columns_map, component_id :: number, i, column) + + if ECS_IS_PAIR(component_id :: number) then + local relation = ECS_PAIR_FIRST(component_id :: number) + local object = ECS_PAIR_SECOND(component_id :: number) + local r = ECS_PAIR(relation, EcsWildcard) + local idr_r = id_record_ensure(world, r) + + archetype_append_to_records(idr_r, archetype_id, columns_map, r, i, column) + + local t = ECS_PAIR(EcsWildcard, object) + local idr_t = id_record_ensure(world, t) + + archetype_append_to_records(idr_t, archetype_id, columns_map, t, i, column) + end + end +end + +local function archetype_create(world: World, id_types: { Id }, ty, prev: i53?): Archetype local archetype_id = (world.max_archetype_id :: number) + 1 world.max_archetype_id = archetype_id local length = #id_types local columns = (table.create(length) :: any) :: { Column } - local records: { number } = {} - local counts: { number } = {} + local columns_map: { [Id]: Column } = {} - local archetype: ecs_archetype_t = { + local archetype: Archetype = { columns = columns, + columns_map = columns_map, entities = {}, id = archetype_id, - records = records, - counts = counts, type = ty, types = id_types, + dead = false, } - for i, component_id in id_types do - local idr = id_record_ensure(world, component_id) - archetype_append_to_records(idr, archetype, component_id, i) + archetype_register(world, archetype) - if ECS_IS_PAIR(component_id) then - local relation = ECS_PAIR_FIRST(component_id) - local object = ECS_PAIR_SECOND(component_id) - local r = ECS_PAIR(relation, EcsWildcard) - local idr_r = id_record_ensure(world, r) - archetype_append_to_records(idr_r, archetype, r, i) - - local t = ECS_PAIR(EcsWildcard, object) - local idr_t = id_record_ensure(world, t) - archetype_append_to_records(idr_t, archetype, t, i) - end - - if bit32.band(idr.flags, ECS_ID_IS_TAG) == 0 then - columns[i] = {} - else - columns[i] = NULL_ARRAY - end - end - - for id in records do + for id in columns_map do local observer_list = find_observers(world, EcsOnArchetypeCreate, id) if not observer_list then continue end for _, observer in observer_list do - if query_match(observer.query, archetype) then + if query_match(observer.query :: QueryInner, archetype) then observer.callback(archetype) end end @@ -735,12 +773,12 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: world.archetype_index[ty] = archetype world.archetypes[archetype_id] = archetype - world.archetype_edges[archetype.id] = {} :: Map + world.archetype_edges[archetype.id] = {} :: Map return archetype end -local function world_range(world: ecs_world_t, range_begin: number, range_end: number?) +local function world_range(world: World, range_begin: number, range_end: number?) local entity_index = world.entity_index entity_index.range_begin = range_begin @@ -756,84 +794,14 @@ local function world_range(world: ecs_world_t, range_begin: number, range_end: n dense_array[i] = i sparse_array[i] = { dense = 0 - } :: ecs_record_t + } :: Record end entity_index.max_id = range_begin - 1 entity_index.alive_count = range_begin - 1 end end -local function world_entity(world: ecs_world_t, entity: i53?): i53 - local entity_index = world.entity_index - if entity then - local index = ECS_ID(entity) - local max_id = entity_index.max_id - local sparse_array = entity_index.sparse_array - local dense_array = entity_index.dense_array - local alive_count = entity_index.alive_count - local r = sparse_array[index] - if r then - local dense = r.dense - - if not dense or r.dense == 0 then - r.dense = index - dense = index - end - - local any = dense_array[dense] - if dense <= alive_count then - if any ~= entity then - error("Entity ID is already in use with a different generation") - else - return entity - end - end - - local e_swap = dense_array[dense] - local r_swap = entity_index_try_get_any(entity_index, e_swap) :: ecs_record_t - alive_count += 1 - entity_index.alive_count = alive_count - r_swap.dense = dense - r.dense = alive_count - dense_array[dense] = e_swap - dense_array[alive_count] = entity - - return entity - else - for i = max_id + 1, index do - sparse_array[i] = { dense = i } :: ecs_record_t - dense_array[i] = i - end - entity_index.max_id = index - - local e_swap = dense_array[alive_count] - local r_swap = sparse_array[alive_count] - r_swap.dense = index - - alive_count += 1 - entity_index.alive_count = alive_count - - r = sparse_array[index] - - r.dense = alive_count - - sparse_array[index] = r - - dense_array[index] = e_swap - dense_array[alive_count] = entity - - - return entity - end - end - return entity_index_new_id(entity_index) -end - -local function world_parent(world: ecs_world_t, entity: i53) - return world_target(world, entity, EcsChildOf, 0) -end - -local function archetype_ensure(world: ecs_world_t, id_types): ecs_archetype_t +local function archetype_ensure(world: World, id_types: { Id }): Archetype if #id_types < 1 then return world.ROOT_ARCHETYPE end @@ -841,6 +809,10 @@ local function archetype_ensure(world: ecs_world_t, id_types): ecs_archetype_t local ty = hash(id_types) local archetype = world.archetype_index[ty] if archetype then + if archetype.dead then + archetype_register(world, archetype) + archetype.dead = false :: any + end return archetype end @@ -861,10 +833,10 @@ local function find_insert(id_types: { i53 }, toAdd: i53): number end local function find_archetype_without( - world: ecs_world_t, - node: ecs_archetype_t, - id: i53 -): ecs_archetype_t + world: World, + node: Archetype, + id: Id +): Archetype local id_types = node.types local at = table.find(id_types, id) @@ -876,11 +848,11 @@ end local function create_edge_for_remove( - world: ecs_world_t, - node: ecs_archetype_t, - edge: Map, - id: i53 -): ecs_archetype_t + world: World, + node: Archetype, + edge: Map, + id: Id +): Archetype local to = find_archetype_without(world, node, id) local edges = world.archetype_edges local archetype_id = node.id @@ -890,14 +862,14 @@ local function create_edge_for_remove( end local function archetype_traverse_remove( - world: ecs_world_t, - id: i53, - from: ecs_archetype_t -): ecs_archetype_t + world: World, + id: Id, + from: Archetype +): Archetype local edges = world.archetype_edges local edge = edges[from.id] - local to: ecs_archetype_t = edge[id] + local to: Archetype = edge[id] if to == nil then to = find_archetype_without(world, from, id) edge[id] = to @@ -907,19 +879,19 @@ local function archetype_traverse_remove( return to end -local function find_archetype_with(world, id, from): ecs_archetype_t +local function find_archetype_with(world: World, id: Id, from: Archetype): Archetype local id_types = from.types - local at = find_insert(id_types, id) - local dst = table.clone(id_types) :: { i53 } + local at = find_insert(id_types :: { number } , id :: number) + local dst = table.clone(id_types) table.insert(dst, at, id) return archetype_ensure(world, dst) end -local function archetype_traverse_add(world, id, from: ecs_archetype_t): ecs_archetype_t +local function archetype_traverse_add(world: World, id: Id, from: Archetype): Archetype from = from or world.ROOT_ARCHETYPE - if from.records[id] then + if from.columns_map[id] then return from end local edges = world.archetype_edges @@ -935,86 +907,6 @@ local function archetype_traverse_add(world, id, from: ecs_archetype_t): ecs_arc return to end -local function world_add( - world: ecs_world_t, - entity: i53, - id: i53 -): () - local entity_index = world.entity_index - local record = entity_index_try_get_fast(entity_index, entity) - if not record then - return - end - - local from = record.archetype - local to = archetype_traverse_add(world, id, from) - if from == to then - return - end - if from then - entity_move(entity_index, entity, record, to) - else - if #to.types > 0 then - new_entity(entity, record, to) - end - end - - local idr = world.component_index[id] - local on_add = idr.hooks.on_add - - if on_add then - on_add(entity, id) - end -end - -local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown): () - local entity_index = world.entity_index - local record = entity_index_try_get_fast(entity_index, entity) - if not record then - return - end - - local from: ecs_archetype_t = record.archetype - local to: ecs_archetype_t = archetype_traverse_add(world, id, from) - local idr = world.component_index[id] - local idr_hooks = idr.hooks - - if from == to then - local tr = (to :: ecs_archetype_t).records[id] - local column = from.columns[tr] - column[record.row] = data - - -- If the archetypes are the same it can avoid moving the entity - -- and just set the data directly. - local on_change = idr_hooks.on_change - if on_change then - on_change(entity, id, data) - end - - return - end - - if from then - -- If there was a previous archetype, then the entity needs to move the archetype - entity_move(entity_index, entity, record, to) - else - if #to.types > 0 then - -- When there is no previous archetype it should create the archetype - new_entity(entity, record, to) - end - end - - local tr = to.records[id] - local column = to.columns[tr] - - column[record.row] = data - - local on_add = idr_hooks.on_add - if on_add then - on_add(entity, id, data) - end -end - local function world_component(world: World): i53 local id = (world.max_component_id :: number) + 1 if id > HI_COMPONENT_ID then @@ -1027,30 +919,7 @@ local function world_component(world: World): i53 return id end -local function world_remove(world: ecs_world_t, entity: i53, id: i53) - local entity_index = world.entity_index - local record = entity_index_try_get_fast(entity_index, entity) - if not record then - return - end - local from = record.archetype - if not from then - return - end - - if from.records[id] then - local idr = world.component_index[id] - local on_remove = idr.hooks.on_remove - if on_remove then - on_remove(entity, id) - end - - local to = archetype_traverse_remove(world, id, record.archetype) - - entity_move(entity_index, entity, record, to) - end -end local function archetype_fast_delete_last(columns: { Column }, column_count: number) for i, column in columns do @@ -1069,7 +938,7 @@ local function archetype_fast_delete(columns: { Column }, column_count: number, end end -local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, row: number) +local function archetype_delete(world: World, archetype: Archetype, row: number) local entity_index = world.entity_index local component_index = world.component_index local columns = archetype.columns @@ -1082,7 +951,7 @@ local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, local delete = move if row ~= last then - local record_to_move = entity_index_try_get_any(entity_index, move) + local record_to_move = entity_index_try_get_any(entity_index, move :: number) if record_to_move then record_to_move.row = row end @@ -1108,107 +977,8 @@ local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, end end -local function world_clear(world: ecs_world_t, entity: i53) - local entity_index = world.entity_index - local component_index = world.component_index - local archetypes = world.archetypes - local tgt = ECS_PAIR(EcsWildcard, entity) - local idr_t = component_index[tgt] - local idr = component_index[entity] - local rel = ECS_PAIR(entity, EcsWildcard) - local idr_r = component_index[rel] - if idr then - local count = 0 - local queue = {} - for archetype_id in idr.cache do - local idr_archetype = archetypes[archetype_id] - local entities = idr_archetype.entities - local n = #entities - count += n - table.move(entities, 1, n, #queue + 1, queue) - end - for _, e in queue do - world_remove(world, e, entity) - end - end - - if idr_t then - local queue: { i53 } - local ids: Map - - local count = 0 - local archetype_ids = idr_t.cache - for archetype_id in archetype_ids do - local idr_t_archetype = archetypes[archetype_id] - local idr_t_types = idr_t_archetype.types - local entities = idr_t_archetype.entities - local removal_queued = false - - for _, id in idr_t_types do - if not ECS_IS_PAIR(id) then - continue - end - local object = entity_index_get_alive( - entity_index, ECS_PAIR_SECOND(id)) - if object ~= entity then - continue - end - if not ids then - ids = {} :: { [i53]: boolean } - end - ids[id] = true - removal_queued = true - end - - if not removal_queued then - continue - end - - if not queue then - queue = {} :: { i53 } - end - - local n = #entities - table.move(entities, 1, n, count + 1, queue) - count += n - end - - for id in ids do - for _, child in queue do - world_remove(world, child, id) - end - end - end - - if idr_r then - local count = 0 - local archetype_ids = idr_r.cache - local ids = {} - local queue = {} - for archetype_id in archetype_ids do - local idr_r_archetype = archetypes[archetype_id] - local entities = idr_r_archetype.entities - local tr = idr_r_archetype.records[rel] - local tr_count = idr_r_archetype.counts[rel] - local types = idr_r_archetype.types - for i = tr, tr + tr_count - 1 do - ids[types[i]] = true - end - local n = #entities - table.move(entities, 1, n, count + 1, queue) - count += n - end - - for _, e in queue do - for id in ids do - world_remove(world, e, id) - end - end - end -end - -local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t) +local function archetype_destroy(world: World, archetype: Archetype) if archetype == world.ROOT_ARCHETYPE then return end @@ -1223,9 +993,9 @@ local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t) local archetype_id = archetype.id world.archetypes[archetype_id] = nil :: any world.archetype_index[archetype.type] = nil :: any - local records = archetype.records + local columns_map = archetype.columns_map - for id in records do + for id in columns_map do local observer_list = find_observers(world, EcsOnArchetypeDelete, id) if not observer_list then continue @@ -1237,236 +1007,21 @@ local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t) end end - for id in records do + for id in columns_map do local idr = component_index[id] - idr.cache[archetype_id] = nil :: any + idr.records[archetype_id] = nil :: any idr.counts[archetype_id] = nil idr.size -= 1 - records[id] = nil :: any if idr.size == 0 then component_index[id] = nil :: any end end end -local function world_cleanup(world: ecs_world_t) - local archetypes = world.archetypes - - for _, archetype in archetypes do - if #archetype.entities == 0 then - archetype_destroy(world, archetype) - end - end - - local new_archetypes = table.create(#archetypes) :: { ecs_archetype_t } - local new_archetype_map = {} - - for index, archetype in archetypes do - new_archetypes[index] = archetype - new_archetype_map[archetype.type] = archetype - end - - world.archetypes = new_archetypes - world.archetype_index = new_archetype_map -end - -local function world_delete(world: ecs_world_t, entity: i53) - local entity_index = world.entity_index - local record = entity_index_try_get(entity_index, entity) - if not record then - return - end - - local archetype = record.archetype - local row = record.row - - if archetype then - -- In the future should have a destruct mode for - -- deleting archetypes themselves. Maybe requires recycling - archetype_delete(world, archetype, row) - end - - local delete = entity - local component_index = world.component_index - local archetypes = world.archetypes - local tgt = ECS_PAIR(EcsWildcard, delete) - local rel = ECS_PAIR(delete, EcsWildcard) - - local idr_t = component_index[tgt] - local idr = component_index[delete] - local idr_r = component_index[rel] - - if idr then - local flags = idr.flags - if bit32.band(flags, ECS_ID_DELETE) ~= 0 then - for archetype_id in idr.cache do - local idr_archetype = archetypes[archetype_id] - - local entities = idr_archetype.entities - local n = #entities - for i = n, 1, -1 do - world_delete(world, entities[i]) - end - - archetype_destroy(world, idr_archetype) - end - else - for archetype_id in idr.cache do - local idr_archetype = archetypes[archetype_id] - local entities = idr_archetype.entities - local n = #entities - for i = n, 1, -1 do - world_remove(world, entities[i], delete) - end - - archetype_destroy(world, idr_archetype) - end - end - end - - if idr_t then - local children: { i53 } - local ids: Map - - local count = 0 - local archetype_ids = idr_t.cache - for archetype_id in archetype_ids do - local idr_t_archetype = archetypes[archetype_id] - local idr_t_types = idr_t_archetype.types - local entities = idr_t_archetype.entities - local removal_queued = false - - for _, id in idr_t_types do - if not ECS_IS_PAIR(id) then - continue - end - local object = entity_index_get_alive( - entity_index, ECS_PAIR_SECOND(id)) - if object ~= delete then - continue - end - local id_record = component_index[id] - local flags = id_record.flags - local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE) - if flags_delete_mask ~= 0 then - for i = #entities, 1, -1 do - local child = entities[i] - world_delete(world, child) - end - break - else - if not ids then - ids = {} :: { [i53]: boolean } - end - ids[id] = true - removal_queued = true - end - end - - if not removal_queued then - continue - end - if not children then - children = {} :: { i53 } - end - local n = #entities - table.move(entities, 1, n, count + 1, children) - count += n - end - - if ids then - for _, child in children do - for id in ids do - world_remove(world, child, id) - end - end - end - - for archetype_id in archetype_ids do - archetype_destroy(world, archetypes[archetype_id]) - end - end - - if idr_r then - local archetype_ids = idr_r.cache - local flags = idr_r.flags - if (bit32.band(flags, ECS_ID_DELETE) :: number) ~= 0 then - for archetype_id in archetype_ids do - local idr_r_archetype = archetypes[archetype_id] - local entities = idr_r_archetype.entities - local n = #entities - for i = n, 1, -1 do - world_delete(world, entities[i]) - end - archetype_destroy(world, idr_r_archetype) - end - else - local children = {} - local count = 0 - local ids = {} - for archetype_id in archetype_ids do - local idr_r_archetype = archetypes[archetype_id] - local entities = idr_r_archetype.entities - local tr = idr_r_archetype.records[rel] - local tr_count = idr_r_archetype.counts[rel] - local types = idr_r_archetype.types - for i = tr, tr + tr_count - 1 do - ids[types[i]] = true - end - - local n = #entities - table.move(entities, 1, n, count + 1, children) - count += n - end - for _, child in children do - for id in ids do - world_remove(world, child, id) - end - end - - for archetype_id in archetype_ids do - archetype_destroy(world, archetypes[archetype_id]) - end - end - end - - local dense_array = entity_index.dense_array - local dense = record.dense - local i_swap = entity_index.alive_count - entity_index.alive_count = i_swap - 1 - - local e_swap = dense_array[i_swap] - local r_swap = entity_index_try_get_any(entity_index, e_swap) :: ecs_record_t - - r_swap.dense = dense - record.archetype = nil :: any - record.row = nil :: any - record.dense = i_swap - - dense_array[dense] = e_swap - dense_array[i_swap] = ECS_GENERATION_INC(entity) -end - -local function world_exists(world: ecs_world_t, entity): boolean - return entity_index_try_get_any(world.entity_index, entity) ~= nil -end - -local function world_contains(world: ecs_world_t, entity): boolean - return entity_index_is_alive(world.entity_index, entity) -end - local function NOOP() end -export type QueryInner = { - compatible_archetypes: { Archetype }, - ids: { i53 }, - filter_with: { i53 }, - filter_without: { i53 }, - next: () -> (number, ...any), - world: World, -} -local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) +local function query_iter_init(query: QueryInner): () -> (number, ...any) local world_query_iter_next local compatible_archetypes = query.compatible_archetypes @@ -1475,10 +1030,9 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) if not archetype then return NOOP :: () -> (number, ...any) end - local columns = archetype.columns local entities = archetype.entities local i = #entities - local records = archetype.records + local columns_map = archetype.columns_map local ids = query.ids local A, B, C, D, E, F, G, H, I = unpack(ids) @@ -1486,49 +1040,49 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) local e: Column, f: Column, g: Column, h: Column if not B then - a = columns[records[A]] + a = columns_map[A] elseif not C then - a = columns[records[A]] - b = columns[records[B]] + a = columns_map[A] + b = columns_map[B] elseif not D then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] elseif not E then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] elseif not F then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] elseif not G then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] elseif not H then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - elseif not I then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - h = columns[records[H]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + else + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + h = columns_map[H] end if not B then @@ -1547,9 +1101,8 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] + columns_map = archetype.columns_map + a = columns_map[A] end local row = i @@ -1573,10 +1126,9 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] end local row = i @@ -1600,11 +1152,10 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] end local row = i @@ -1628,12 +1179,11 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] end local row = i @@ -1657,13 +1207,12 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] end local row = i @@ -1687,14 +1236,13 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] end local row = i @@ -1718,15 +1266,14 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] end local row = i @@ -1750,16 +1297,15 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - h = columns[records[H]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + h = columns_map[H] end local row = i @@ -1769,6 +1315,7 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) end else local output = {} + local ids_len = #ids function world_query_iter_next(): any local entity = entities[i] while entity == nil do @@ -1784,18 +1331,25 @@ local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + h = columns_map[H] end local row = i i -= 1 - for j, id in ids do - output[j] = columns[records[id]][row] + for i = 9, ids_len do + output[i - 8] = columns_map[i][row] end - return entity, unpack(output) + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row], unpack(output) end end @@ -1811,17 +1365,17 @@ local function query_iter(query): () -> (number, ...any) return query_next end -local function query_without(query: ecs_query_data_t, ...: i53) +local function query_without(query: QueryInner, ...: i53) local without = { ... } query.filter_without = without local compatible_archetypes = query.compatible_archetypes for i = #compatible_archetypes, 1, -1 do local archetype = compatible_archetypes[i] - local records = archetype.records + local columns_map = archetype.columns_map local matches = true for _, id in without do - if records[id] then + if columns_map[id] then matches = false break end @@ -1841,18 +1395,18 @@ local function query_without(query: ecs_query_data_t, ...: i53) return query :: any end -local function query_with(query: ecs_query_data_t, ...: i53) +local function query_with(query: QueryInner, ...: i53) local compatible_archetypes = query.compatible_archetypes local with = { ... } query.filter_with = with for i = #compatible_archetypes, 1, -1 do local archetype = compatible_archetypes[i] - local records = archetype.records + local columns_map = archetype.columns_map local matches = true for _, id in with do - if not records[id] then + if not columns_map[id] then matches = false break end @@ -1879,7 +1433,7 @@ local function query_archetypes(query) return query.compatible_archetypes end -local function query_cached(query: ecs_query_data_t) +local function query_cached(query: QueryInner) local with = query.filter_with local ids = query.ids if with then @@ -1896,20 +1450,19 @@ local function query_cached(query: ecs_query_data_t) local e: Column, f: Column, g: Column, h: Column local world_query_iter_next - local columns: { Column } - local entities: { number } + local entities: { Entity } local i: number - local archetype: ecs_archetype_t - local records: { number } + local archetype: Archetype + local columns_map: { [Id]: Column } local archetypes = query.compatible_archetypes - local world = query.world :: { observable: ecs_observable_t } + local world = query.world -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively -- because the event will be emitted for all components of that Archetype. - local observable = world.observable :: ecs_observable_t + local observable = world.observable local on_create_action = observable[EcsOnArchetypeCreate] if not on_create_action then - on_create_action = {} :: Map + on_create_action = {} :: Map observable[EcsOnArchetypeCreate] = on_create_action end local query_cache_on_create = on_create_action[A] @@ -1920,7 +1473,7 @@ local function query_cached(query: ecs_query_data_t) local on_delete_action = observable[EcsOnArchetypeDelete] if not on_delete_action then - on_delete_action = {} :: Map + on_delete_action = {} :: Map observable[EcsOnArchetypeDelete] = on_delete_action end local query_cache_on_delete = on_delete_action[A] @@ -1957,52 +1510,51 @@ local function query_cached(query: ecs_query_data_t) end entities = archetype.entities i = #entities - records = archetype.records - columns = archetype.columns + columns_map = archetype.columns_map if not B then - a = columns[records[A]] + a = columns_map[A] elseif not C then - a = columns[records[A]] - b = columns[records[B]] + a = columns_map[A] + b = columns_map[B] elseif not D then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] elseif not E then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] elseif not F then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] elseif not G then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] elseif not H then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - elseif not I then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - h = columns[records[H]] + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + else + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + h = columns_map[H] end return world_query_iter_next @@ -2024,9 +1576,8 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] + columns_map = archetype.columns_map + a = columns_map[A] end local row = i @@ -2050,10 +1601,9 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] end local row = i @@ -2077,11 +1627,10 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] end local row = i @@ -2105,12 +1654,11 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] end local row = i @@ -2134,13 +1682,12 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] end local row = i @@ -2164,14 +1711,13 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] end local row = i @@ -2195,15 +1741,14 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] end local row = i @@ -2227,16 +1772,15 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - h = columns[records[H]] + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + h = columns_map[H] end local row = i @@ -2245,7 +1789,8 @@ local function query_cached(query: ecs_query_data_t) return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] end else - local queryOutput = {} + local output = {} + local ids_len = #ids function world_query_iter_next(): any local entity = entities[i] while entity == nil do @@ -2261,28 +1806,25 @@ local function query_cached(query: ecs_query_data_t) continue end entity = entities[i] - columns = archetype.columns - records = archetype.records + columns_map = archetype.columns_map + a = columns_map[A] + b = columns_map[B] + c = columns_map[C] + d = columns_map[D] + e = columns_map[E] + f = columns_map[F] + g = columns_map[G] + h = columns_map[H] end local row = i i -= 1 - if not F then - return entity, a[row], b[row], c[row], d[row], e[row] - elseif not G then - return entity, a[row], b[row], c[row], d[row], e[row], f[row] - elseif not H then - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] - elseif not I then - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + for i = 9, ids_len do + output[i - 8] = columns_map[i][row] end - for j, id in ids do - queryOutput[j] = columns[records[id]][row] - end - - return entity, unpack(queryOutput) + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], unpack(output) end end @@ -2303,7 +1845,7 @@ Query.with = query_with Query.archetypes = query_archetypes Query.cached = query_cached -local function world_query(world: ecs_world_t, ...) +local function world_query(world: World, ...) local compatible_archetypes = {} local length = 0 @@ -2311,7 +1853,7 @@ local function world_query(world: ecs_world_t, ...) local archetypes = world.archetypes - local idr: ecs_id_record_t? + local idr: ComponentRecord? local component_index = world.component_index local q = setmetatable({ @@ -2335,18 +1877,18 @@ local function world_query(world: ecs_world_t, ...) return q end - for archetype_id in idr.cache do + for archetype_id in idr.records do local compatibleArchetype = archetypes[archetype_id] if #compatibleArchetype.entities == 0 then continue end - local records = compatibleArchetype.records + local columns_map = compatibleArchetype.columns_map local skip = false for i, id in ids do - local tr = records[id] - if not tr then + local column = columns_map[id] + if not column then skip = true break end @@ -2363,18 +1905,18 @@ local function world_query(world: ecs_world_t, ...) return q end -local function world_each(world: ecs_world_t, id: i53): () -> () +local function world_each(world: World, id: Id): () -> Entity local idr = world.component_index[id] if not idr then - return NOOP + return NOOP :: () -> Entity end - local idr_cache = idr.cache + local records = idr.records local archetypes = world.archetypes - local archetype_id = next(idr_cache, nil) :: number + local archetype_id = next(records, nil) :: number local archetype = archetypes[archetype_id] if not archetype then - return NOOP + return NOOP :: () -> Entity end local entities = archetype.entities @@ -2383,7 +1925,7 @@ local function world_each(world: ecs_world_t, id: i53): () -> () return function(): any local entity = entities[row] while not entity do - archetype_id = next(idr_cache, archetype_id) :: number + archetype_id = next(records, archetype_id) :: number if not archetype_id then return end @@ -2397,87 +1939,688 @@ local function world_each(world: ecs_world_t, id: i53): () -> () end end -local function world_children(world: ecs_world_t, parent: i53) - return world_each(world, ECS_PAIR(EcsChildOf, parent)) +local function world_children(world: World, parent: Id) + return world_each(world, ECS_PAIR(EcsChildOf, parent::number)) end -export type Record = { - archetype: Archetype, - row: number, - dense: i24, -} -export type ComponentRecord = { - cache: { [Id]: number }, - counts: { [Id]: number }, - flags: number, - size: number, - hooks: { - on_add: ((entity: Entity, id: Entity, value: T) -> ())?, - on_change: ((entity: Entity, id: Entity, value: T) -> ())?, - on_remove: ((entity: Entity, id: Entity) -> ())?, - }, -} -export type ComponentIndex = Map -export type Archetypes = { [Id]: Archetype } - -export type EntityIndex = { - dense_array: Map, - sparse_array: Map, - alive_count: number, - max_id: number, - range_begin: number?, - range_end: number? -} - -local World = {} -World.__index = World - -World.entity = world_entity -World.query = world_query -World.remove = world_remove -World.clear = world_clear -World.delete = world_delete -World.component = world_component -World.add = world_add -World.set = world_set -World.get = world_get -World.has = world_has -World.target = world_target -World.parent = world_parent -World.contains = world_contains -World.exists = world_exists -World.cleanup = world_cleanup -World.each = world_each -World.children = world_children -World.range = world_range - local function world_new() + local eindex_dense_array = {} :: { Entity } + local eindex_sparse_array = {} :: { Record } + local eindex_alive_count = 0 + local eindex_max_id = 0 + local entity_index = { - dense_array = {}, - sparse_array = {}, - alive_count = 0, - max_id = 0, - } :: ecs_entity_index_t - local self = setmetatable({ - archetype_edges = {}, + dense_array = eindex_dense_array, + sparse_array = eindex_sparse_array, + alive_count = eindex_alive_count, + max_id = eindex_max_id, + } :: EntityIndex - archetype_index = {} :: { [string]: Archetype }, - archetypes = {} :: Archetypes, - component_index = {} :: ComponentIndex, + local component_index = {} :: ComponentIndex + + local archetype_index = {} :: { [string]: Archetype } + local archetypes = {} :: Archetypes + local archetype_edges = {} :: { [number]: { [Id]: Archetype } } + + local observable = {} + + local world = { + archetype_edges = archetype_edges, + + component_index = component_index, entity_index = entity_index, - ROOT_ARCHETYPE = (nil :: any) :: Archetype, + ROOT_ARCHETYPE = nil :: any, + archetypes = archetypes, + archetype_index = archetype_index, max_archetype_id = 0, max_component_id = ecs_max_component_id, - observable = {} :: Observable, - }, World) :: any + observable = observable, + } :: World - self.ROOT_ARCHETYPE = archetype_create(self, {}, "") + + local ROOT_ARCHETYPE = archetype_create(world, {}, "") + world.ROOT_ARCHETYPE = ROOT_ARCHETYPE + + local function inner_entity_index_try_get_any(entity: number): Record? + local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)] + + if not r or r.dense == 0 then + return nil + end + + return r + end + + -- local function entity_index_try_get_safe(entity: number): Record? + -- local r = entity_index_try_get_any_fast(entity_index, entity) + -- if r then + -- local r_dense = r.dense + -- if r_dense > entity_index.alive_count then + -- return nil + -- end + -- if entity_index.dense_array[r_dense] ~= entity then + -- return nil + -- end + -- end + -- return r + -- end + + local function inner_entity_index_try_get(entity: number): Record? + local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)] + if r then + if eindex_dense_array[r.dense] ~= entity then + return nil + end + end + return r + end + + + local function inner_world_add( + world: World, + entity: Entity, + id: Id + ): () + local entity_index = world.entity_index + local record = inner_entity_index_try_get(entity :: number) + if not record then + return + end + + local from = record.archetype + local to = archetype_traverse_add(world, id, from) + if from == to then + return + end + if from then + entity_move(entity_index, entity, record, to) + else + if #to.types > 0 then + new_entity(entity, record, to) + end + end + + local idr = world.component_index[id] + local on_add = idr.hooks.on_add + + if on_add then + on_add(entity, id) + end + end + + local function inner_world_get(world: World, entity: Entity, + a: Id, b: Id?, c: Id?, d: Id?, e: Id?): ...any + local record = inner_entity_index_try_get(entity::number) + if not record then + return nil + end + + local archetype = record.archetype + if not archetype then + return nil + end + + local columns_map = archetype.columns_map + local row = record.row + + local va = fetch(a, columns_map, row) + + if not b then + return va + elseif not c then + return va, fetch(a, columns_map, row) + elseif not d then + return va, fetch(b, columns_map, row), fetch(c, columns_map, row) + elseif not e then + return va, fetch(b, columns_map, row), fetch(c, columns_map, row), fetch(d, columns_map, row) + else + error("args exceeded") + end + end + + local function inner_world_has(world: World, entity: i53, + a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean + + local record = inner_entity_index_try_get(entity) + if not record then + return false + end + + local archetype = record.archetype + if not archetype then + return false + end + + local columns_map = archetype.columns_map + + return columns_map[a] ~= nil and + (b == nil or columns_map[b] ~= nil) and + (c == nil or columns_map[c] ~= nil) and + (d == nil or columns_map[d] ~= nil) and + (e == nil or error("args exceeded")) + end + + local function inner_world_target(world: World, entity: Entity, relation: Id, index: number?): Entity? + local record = inner_entity_index_try_get(entity :: number) + if not record then + return nil + end + + local archetype = record.archetype + if not archetype then + return nil + end + + local r = ECS_PAIR(relation::number, EcsWildcard) + local idr = world.component_index[r] + + if not idr then + return nil + end + + local archetype_id = archetype.id + local count = idr.counts[archetype_id] + if not count then + return nil + end + + local nth = index or 0 + + if nth >= count then + nth = nth + count + 1 + end + + nth = archetype.types[nth + idr.records[archetype_id]] + + if not nth then + return nil + end + + return entity_index_get_alive(world.entity_index, + ECS_PAIR_SECOND(nth :: number)) + end + + local function inner_world_parent(world: World, entity: Entity): Entity? + return inner_world_target(world, entity, EcsChildOf, 0) + end + + local function inner_archetype_traverse_add(id: Id, from: Archetype): Archetype + from = from or ROOT_ARCHETYPE + if from.columns_map[id] then + return from + end + local edges = archetype_edges + local edge = edges[from.id] + + local to = edge[id] :: Archetype + if not to then + to = find_archetype_with(world, id, from) + edge[id] = to + edges[to.id][id] = from + end + + return to + end + + local function inner_world_set(world: World, entity: Entity, id: Id, data: a): () + local record = inner_entity_index_try_get(entity :: number) + if not record then + return + end + + local from: Archetype = record.archetype + local to: Archetype = inner_archetype_traverse_add(id, from) + local idr = component_index[id] + local idr_hooks = idr.hooks + + if from == to then + local column = to.columns_map[id] + column[record.row] = data + + -- If the archetypes are the same it can avoid moving the entity + -- and just set the data directly. + local on_change = idr_hooks.on_change + if on_change then + on_change(entity, id, data) + end + + return + end + + if from then + -- If there was a previous archetype, then the entity needs to move the archetype + entity_move(entity_index, entity, record, to) + else + if #to.types > 0 then + -- When there is no previous archetype it should create the archetype + new_entity(entity, record, to) + end + end + local column = to.columns_map[id] + column[record.row] = data + + local on_add = idr_hooks.on_add + if on_add then + on_add(entity, id, data) + end + end + + local function inner_world_entity(world: World, entity: Entity?): Entity + if entity then + local index = ECS_ID(entity :: number) + local alive_count = entity_index.alive_count + local r = eindex_sparse_array[index] + if r then + local dense = r.dense + + if not dense or r.dense == 0 then + r.dense = index + dense = index + end + + local any = eindex_dense_array[dense] + if dense <= alive_count then + if any ~= entity then + error("Entity ID is already in use with a different generation") + else + return entity + end + end + + local e_swap = eindex_dense_array[dense] + local r_swap = inner_entity_index_try_get_any(e_swap :: number) :: Record + alive_count += 1 + entity_index.alive_count = alive_count + r_swap.dense = dense + r.dense = alive_count + eindex_dense_array[dense] = e_swap + eindex_dense_array[alive_count] = entity + + return entity + else + for i = eindex_max_id + 1, index do + eindex_sparse_array[i] = { dense = i } :: Record + eindex_dense_array[i] = i + end + entity_index.max_id = index + + local e_swap = eindex_dense_array[alive_count] + local r_swap = eindex_sparse_array[alive_count] + r_swap.dense = index + + alive_count += 1 + entity_index.alive_count = alive_count + + r = eindex_sparse_array[index] + + r.dense = alive_count + + eindex_sparse_array[index] = r + + eindex_dense_array[index] = e_swap + eindex_dense_array[alive_count] = entity + + return entity + end + end + return entity_index_new_id(entity_index) + end + + local function inner_world_remove(world: World, entity: Entity, id: Id) + local record = inner_entity_index_try_get(entity :: number) + if not record then + return + end + local from = record.archetype + + if not from then + return + end + + if from.columns_map[id] then + local idr = world.component_index[id] + local on_remove = idr.hooks.on_remove + if on_remove then + on_remove(entity, id) + end + + local to = archetype_traverse_remove(world, id, record.archetype) + + entity_move(entity_index, entity, record, to) + end + end + + local function inner_world_clear(world: World, entity: Entity) + local tgt = ECS_PAIR(EcsWildcard, entity::number) + local idr_t = component_index[tgt] + local idr = component_index[entity] + local rel = ECS_PAIR(entity::number, EcsWildcard) + local idr_r = component_index[rel] + + if idr then + local count = 0 + local queue = {} + for archetype_id in idr.records do + local idr_archetype = archetypes[archetype_id] + local entities = idr_archetype.entities + local n = #entities + count += n + table.move(entities, 1, n, #queue + 1, queue) + end + for _, e in queue do + inner_world_remove(world, e, entity) + end + end + + if idr_t then + local queue: { i53 } + local ids: Map + + local count = 0 + local archetype_ids = idr_t.records + for archetype_id in archetype_ids do + local idr_t_archetype = archetypes[archetype_id] + local idr_t_types = idr_t_archetype.types + local entities = idr_t_archetype.entities + local removal_queued = false + + for _, id in idr_t_types do + if not ECS_IS_PAIR(id::number) then + continue + end + local object = entity_index_get_alive( + entity_index, ECS_PAIR_SECOND(id::number)) + if object ~= entity then + continue + end + if not ids then + ids = {} :: { [i53]: boolean } + end + ids[id] = true + removal_queued = true + end + + if not removal_queued then + continue + end + + if not queue then + queue = {} :: { i53 } + end + + local n = #entities + table.move(entities, 1, n, count + 1, queue) + count += n + end + + for id in ids do + for _, child in queue do + inner_world_remove(world, child, id) + end + end + end + + if idr_r then + local count = 0 + local archetype_ids = idr_r.records + local ids = {} + local queue = {} + local records = idr_r.records + local counts = idr_r.counts + for archetype_id in archetype_ids do + local idr_r_archetype = archetypes[archetype_id] + local entities = idr_r_archetype.entities + local tr = records[archetype_id] + local tr_count = counts[archetype_id] + local types = idr_r_archetype.types + for i = tr, tr + tr_count - 1 do + ids[types[i]] = true + end + local n = #entities + table.move(entities, 1, n, count + 1, queue) + count += n + end + + for _, e in queue do + for id in ids do + inner_world_remove(world, e, id) + end + end + end + end + + local function inner_world_delete(world: World, entity: Entity) + local entity_index = world.entity_index + local record = inner_entity_index_try_get(entity::number) + if not record then + return + end + + local archetype = record.archetype + local row = record.row + + if archetype then + -- In the future should have a destruct mode for + -- deleting archetypes themselves. Maybe requires recycling + archetype_delete(world, archetype, row) + end + + local component_index = world.component_index + local archetypes = world.archetypes + local tgt = ECS_PAIR(EcsWildcard, entity::number) + local rel = ECS_PAIR(entity::number, EcsWildcard) + + local idr_t = component_index[tgt] + local idr = component_index[entity::number] + local idr_r = component_index[rel] + + if idr then + local flags = idr.flags + if bit32.band(flags, ECS_ID_DELETE) ~= 0 then + for archetype_id in idr.records do + local idr_archetype = archetypes[archetype_id] + + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + inner_world_delete(world, entities[i]) + end + + archetype_destroy(world, idr_archetype) + end + else + for archetype_id in idr.records do + local idr_archetype = archetypes[archetype_id] + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + inner_world_remove(world, entities[i], entity) + end + + archetype_destroy(world, idr_archetype) + end + end + end + + if idr_t then + local children: { i53 } + local ids: Map + + local count = 0 + local archetype_ids = idr_t.records + for archetype_id in archetype_ids do + local idr_t_archetype = archetypes[archetype_id] + local idr_t_types = idr_t_archetype.types + local entities = idr_t_archetype.entities + local removal_queued = false + + for _, id in idr_t_types do + if not ECS_IS_PAIR(id::number) then + continue + end + local object = entity_index_get_alive( + entity_index, ECS_PAIR_SECOND(id::number)) + if object ~= entity then + continue + end + local id_record = component_index[id] + local flags = id_record.flags + local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE) + if flags_delete_mask ~= 0 then + for i = #entities, 1, -1 do + local child = entities[i] + inner_world_delete(world, child) + end + break + else + if not ids then + ids = {} :: { [i53]: boolean } + end + ids[id] = true + removal_queued = true + end + end + + if not removal_queued then + continue + end + if not children then + children = {} :: { i53 } + end + local n = #entities + table.move(entities, 1, n, count + 1, children) + count += n + end + + if ids then + for _, child in children do + for id in ids do + inner_world_remove(world, child, id) + end + end + end + + for archetype_id in archetype_ids do + archetype_destroy(world, archetypes[archetype_id]) + end + end + + if idr_r then + local archetype_ids = idr_r.records + local flags = idr_r.flags + if (bit32.band(flags, ECS_ID_DELETE) :: number) ~= 0 then + for archetype_id in archetype_ids do + local idr_r_archetype = archetypes[archetype_id] + local entities = idr_r_archetype.entities + local n = #entities + for i = n, 1, -1 do + inner_world_delete(world, entities[i]) + end + archetype_destroy(world, idr_r_archetype) + end + else + local children = {} + local count = 0 + local ids = {} + local counts = idr_r.counts + local records = idr_r.records + for archetype_id in archetype_ids do + local idr_r_archetype = archetypes[archetype_id] + local entities = idr_r_archetype.entities + local tr = records[archetype_id] + local tr_count = counts[archetype_id] + local types = idr_r_archetype.types + for i = tr, tr + tr_count - 1 do + ids[types[i]] = true + end + + local n = #entities + table.move(entities, 1, n, count + 1, children) + count += n + end + for _, child in children do + for id in ids do + inner_world_remove(world, child, id) + end + end + + for archetype_id in archetype_ids do + archetype_destroy(world, archetypes[archetype_id]) + end + end + end + + local dense_array = entity_index.dense_array + local dense = record.dense + local i_swap = entity_index.alive_count + entity_index.alive_count = i_swap - 1 + + local e_swap = dense_array[i_swap] + local r_swap = inner_entity_index_try_get_any(e_swap :: number) :: Record + + r_swap.dense = dense + record.archetype = nil :: any + record.row = nil :: any + record.dense = i_swap + + dense_array[dense] = e_swap + dense_array[i_swap] = ECS_GENERATION_INC(entity :: number) + end + + local function inner_world_exists(world: World, entity: Entity): boolean + return inner_entity_index_try_get_any(entity :: number) ~= nil + end + + local function inner_world_contains(world: World, entity: Entity): boolean + return entity_index_is_alive(world.entity_index, entity) + end + + local function inner_world_cleanup(world: World) + for _, archetype in archetypes do + if #archetype.entities == 0 then + archetype_destroy(world, archetype) + end + end + + local new_archetypes = {} + local new_archetype_map = {} + + for index, archetype in archetypes do + new_archetypes[index] = archetype + new_archetype_map[archetype.type] = archetype + end + + archetypes = new_archetypes + archetype_index = new_archetype_map + + world.archetypes = new_archetypes + world.archetype_index = new_archetype_map + end + + world.entity = inner_world_entity + world.query = world_query :: any + world.remove = inner_world_remove + world.clear = inner_world_clear + world.delete = inner_world_delete + world.component = world_component + world.add = inner_world_add + world.set = inner_world_set + world.get = inner_world_get :: any + world.has = inner_world_has :: any + world.target = inner_world_target + world.parent = inner_world_parent + world.contains = inner_world_contains + world.exists = inner_world_exists + world.cleanup = inner_world_cleanup + world.each = world_each + world.children = world_children + world.range = world_range for i = 1, HI_COMPONENT_ID do local e = entity_index_new_id(entity_index) - world_add(self, e, EcsComponent) + inner_world_add(world, e, EcsComponent) end for i = HI_COMPONENT_ID + 1, EcsRest do @@ -2485,27 +2628,27 @@ local function world_new() entity_index_new_id(entity_index) end - world_add(self, EcsName, EcsComponent) - world_add(self, EcsOnChange, EcsComponent) - world_add(self, EcsOnAdd, EcsComponent) - world_add(self, EcsOnRemove, EcsComponent) - world_add(self, EcsWildcard, EcsComponent) - world_add(self, EcsRest, EcsComponent) + inner_world_add(world, EcsName, EcsComponent) + inner_world_add(world, EcsOnChange, EcsComponent) + inner_world_add(world, EcsOnAdd, EcsComponent) + inner_world_add(world, EcsOnRemove, EcsComponent) + inner_world_add(world, EcsWildcard, EcsComponent) + inner_world_add(world, EcsRest, EcsComponent) - world_set(self, EcsOnAdd, EcsName, "jecs.OnAdd") - world_set(self, EcsOnRemove, EcsName, "jecs.OnRemove") - world_set(self, EcsOnChange, EcsName, "jecs.OnChange") - world_set(self, EcsWildcard, EcsName, "jecs.Wildcard") - world_set(self, EcsChildOf, EcsName, "jecs.ChildOf") - world_set(self, EcsComponent, EcsName, "jecs.Component") - world_set(self, EcsOnDelete, EcsName, "jecs.OnDelete") - world_set(self, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget") - world_set(self, EcsDelete, EcsName, "jecs.Delete") - world_set(self, EcsRemove, EcsName, "jecs.Remove") - world_set(self, EcsName, EcsName, "jecs.Name") - world_set(self, EcsRest, EcsRest, "jecs.Rest") + inner_world_set(world, EcsOnAdd, EcsName, "jecs.OnAdd") + inner_world_set(world, EcsOnRemove, EcsName, "jecs.OnRemove") + inner_world_set(world, EcsOnChange, EcsName, "jecs.OnChange") + inner_world_set(world, EcsWildcard, EcsName, "jecs.Wildcard") + inner_world_set(world, EcsChildOf, EcsName, "jecs.ChildOf") + inner_world_set(world, EcsComponent, EcsName, "jecs.Component") + inner_world_set(world, EcsOnDelete, EcsName, "jecs.OnDelete") + inner_world_set(world, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget") + inner_world_set(world, EcsDelete, EcsName, "jecs.Delete") + inner_world_set(world, EcsRemove, EcsName, "jecs.Remove") + inner_world_set(world, EcsName, EcsName, "jecs.Name") + inner_world_set(world, EcsRest, EcsRest, "jecs.Rest") - world_add(self, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) + inner_world_add(world, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) for i = EcsRest + 1, ecs_max_tag_id do entity_index_new_id(entity_index) @@ -2514,122 +2657,16 @@ local function world_new() for i, bundle in ecs_metadata do for ty, value in bundle do if value == NULL then - world_add(self, i, ty) + inner_world_add(world, i, ty) else - world_set(self, i, ty, value) + inner_world_add(world, i, ty, value) end end end - return self + return world end -World.new = world_new - -export type Entity = number | { __T: T } -export type Id = number | { __T: T } -export type Pair = Id

-type ecs_id_t = Id | Pair | Pair<"Tag", T> -export type Item = (self: Query) -> (Entity, T...) -export type Iter = (query: Query) -> () -> (Entity, T...) - -export type Query = typeof(setmetatable( - {} :: { - iter: Iter, - with: (self: Query, ...Id) -> Query, - without: (self: Query, ...Id) -> Query, - archetypes: (self: Query) -> { Archetype }, - cached: (self: Query) -> Query, - }, - {} :: { - __iter: Iter - } -)) - -export type Observer = { - callback: (archetype: Archetype) -> (), - query: QueryInner, -} - -export type Observable = { - [Id]: { - [Id]: { - { Observer } - } - } -} - -export type World = { - archetype_index: { [string]: Archetype }, - archetypes: Archetypes, - component_index: ComponentIndex, - entity_index: EntityIndex, - ROOT_ARCHETYPE: Archetype, - - max_component_id: number, - max_archetype_id: number, - - observable: any, - - --- Enforce a check on entities to be created within desired range - range: (self: World, range_begin: number, range_end: number?) -> (), - - --- Creates a new entity - entity: (self: World, id: Entity?) -> Entity, - --- Creates a new entity located in the first 256 ids. - --- These should be used for static components for fast access. - component: (self: World) -> Entity, - --- Gets the target of an relationship. For example, when a user calls - --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. - target: (self: World, id: Entity, relation: Id, index: number?) -> Entity?, - --- Deletes an entity and all it's related components and relationships. - delete: (self: World, id: Entity) -> (), - - --- Adds a component to the entity with no value - add: (self: World, id: Entity, component: Id) -> (), - --- Assigns a value to a component on the given entity - set: (self: World, id: Entity, component: Id, data: a) -> (), - - cleanup: (self: World) -> (), - -- Clears an entity from the world - clear: (self: World, id: Id) -> (), - --- Removes a component from the given entity - remove: (self: World, id: Entity, component: Id) -> (), - --- Retrieves the value of up to 4 components. These values may be nil. - get: & ((World, Entity, Id) -> a?) - & ((World, Entity, Id, Id) -> (a?, b?)) - & ((World, Entity, Id, Id, Id) -> (a?, b?, c?)) - & ((World, Entity, Id, Id, Id, Id) -> (a?, b?, c?, d?)), - - --- Returns whether the entity has the ID. - has: ((World, Entity, Id) -> boolean) - & ((World, Entity, Id, Id) -> boolean) - & ((World, Entity, Id, Id, Id) -> boolean) - & (World, Entity, Id, Id, Id, Id) -> boolean, - - --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. - parent: (self: World, entity: Entity) -> Entity, - - --- Checks if the world contains the given entity - contains: (self: World, entity: Entity) -> boolean, - - --- Checks if the entity exists - exists: (self: World, entity: Entity) -> boolean, - - each: (self: World, id: Id) -> () -> Entity, - - children: (self: World, id: Id) -> () -> Entity, - - --- Searches the world for entities that match a given query - query: ((World, Id) -> Query) - & ((World, Id, Id) -> Query) - & ((World, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id, Id, Id, Id, ...Id) -> Query) -} -- type function ecs_id_t(entity) -- local ty = entity:components()[2] -- local __T = ty:readproperty(types.singleton("__T")) @@ -2648,8 +2685,15 @@ export type World = { -- end -- +local function ecs_is_tag(world: World, entity: Entity): boolean + local idr = world.component_index[entity] + if idr then + return bit32.band(idr.flags, ECS_ID_IS_TAG) ~= 0 + end + return not world_has_one_inline(world, entity, EcsComponent) +end + return { - World = World :: { new: () -> World }, world = world_new :: () -> World, component = (ECS_COMPONENT :: any) :: () -> Entity, tag = (ECS_TAG :: any) :: () -> Entity, @@ -2689,6 +2733,7 @@ return { archetype_append_to_records = archetype_append_to_records, id_record_ensure = id_record_ensure, + component_record = id_record_get, archetype_create = archetype_create, archetype_ensure = archetype_ensure, find_insert = find_insert, @@ -2702,7 +2747,6 @@ return { entity_index_try_get = entity_index_try_get, entity_index_try_get_any = entity_index_try_get_any, - entity_index_try_get_fast = entity_index_try_get_fast, entity_index_is_alive = entity_index_is_alive, entity_index_new_id = entity_index_new_id, diff --git a/mirror.luau b/mirror.luau old mode 100644 new mode 100755 index c2ceac6..7aa4a8b --- a/mirror.luau +++ b/mirror.luau @@ -1,3 +1,5 @@ + + --!optimize 2 --!native --!strict @@ -11,444 +13,2344 @@ type ArchetypeId = number type Column = { any } -type Archetype = { +type Map = { [K]: V } + +type ecs_archetype_t = { id: number, - edges: { - [i24]: { - add: Archetype, - remove: Archetype, - }, - }, types: Ty, - type: string | number, + type: string, entities: { number }, columns: { Column }, - records: {}, + records: { [i53]: number }, + counts: { [i53]: number }, } -type Record = { - archetype: Archetype, +export type Archetype = { + id: number, + types: Ty, + type: string, + entities: { number }, + columns: { Column }, + records: { [Id]: number }, + counts: { [Id]: number }, +} + +type ecs_record_t = { + archetype: ecs_archetype_t, row: number, + dense: i24, } -type EntityIndex = { [i24]: Record } -type ComponentIndex = { [i24]: ArchetypeMap } - -type ArchetypeRecord = number -type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord }, size: number } -type Archetypes = { [ArchetypeId]: Archetype } - -type ArchetypeDiff = { - added: Ty, - removed: Ty, +type ecs_id_record_t = { + cache: { number }, + counts: { number }, + flags: number, + size: number, + hooks: { + on_add: ((entity: i53, id: i53, data: any?) -> ())?, + on_change: ((entity: i53, id: i53, data: any) -> ())?, + on_remove: ((entity: i53, id: i53) -> ())?, + }, } -local HI_COMPONENT_ID = 256 -local ON_ADD = HI_COMPONENT_ID + 1 -local ON_REMOVE = HI_COMPONENT_ID + 2 -local ON_SET = HI_COMPONENT_ID + 3 -local REST = HI_COMPONENT_ID + 4 +type ecs_id_index_t = Map -local function transitionArchetype( - entityIndex: EntityIndex, - to: Archetype, - destinationRow: i24, - from: Archetype, - sourceRow: i24 +type ecs_archetypes_map_t = { [string]: ecs_archetype_t } + +type ecs_archetypes_t = { ecs_archetype_t } + +type ecs_entity_index_t = { + dense_array: Map, + sparse_array: Map, + alive_count: number, + max_id: number, + range_begin: number?, + range_end: number? +} + +type ecs_query_data_t = { + compatible_archetypes: { ecs_archetype_t }, + ids: { i53 }, + filter_with: { i53 }, + filter_without: { i53 }, + next: () -> (number, ...any), + world: ecs_world_t, +} + +type ecs_observer_t = { + callback: (archetype: ecs_archetype_t) -> (), + query: ecs_query_data_t, +} + +type ecs_observable_t = Map> + +type ecs_world_t = { + archetype_edges: Map>, + entity_index: ecs_entity_index_t, + component_index: ecs_id_index_t, + archetypes: ecs_archetypes_t, + archetype_index: ecs_archetypes_map_t, + max_archetype_id: number, + max_component_id: number, + ROOT_ARCHETYPE: ecs_archetype_t, + observable: Map>, +} + +-- stylua: ignore start + +local ECS_ENTITY_MASK = bit32.lshift(1, 24) +local ECS_GENERATION_MASK = bit32.lshift(1, 16) +local ECS_PAIR_OFFSET = 2^48 + +local ECS_ID_DELETE = 0b01 +local ECS_ID_IS_TAG = 0b10 +local ECS_ID_MASK = 0b00 + +local HI_COMPONENT_ID = 256 +local EcsOnAdd = HI_COMPONENT_ID + 1 +local EcsOnRemove = HI_COMPONENT_ID + 2 +local EcsOnChange = HI_COMPONENT_ID + 3 +local EcsWildcard = HI_COMPONENT_ID + 4 +local EcsChildOf = HI_COMPONENT_ID + 5 +local EcsComponent = HI_COMPONENT_ID + 6 +local EcsOnDelete = HI_COMPONENT_ID + 7 +local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 +local EcsDelete = HI_COMPONENT_ID + 9 +local EcsRemove = HI_COMPONENT_ID + 10 +local EcsName = HI_COMPONENT_ID + 11 +local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12 +local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13 +local EcsRest = HI_COMPONENT_ID + 14 + +local NULL_ARRAY = table.freeze({}) :: Column +local NULL = newproxy(false) + +local ECS_INTERNAL_ERROR = [[ + This is an internal error, please file a bug report via the following link: + + https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md +]] + +local function ecs_assert(condition, msg: string?) + if not condition then + error(msg) + end +end + +local ecs_metadata: Map> = {} +local ecs_max_component_id = 0 +local ecs_max_tag_id = EcsRest + +local function ECS_COMPONENT() + ecs_max_component_id += 1 + if ecs_max_component_id > HI_COMPONENT_ID then + error("Too many components") + end + return ecs_max_component_id +end + +local function ECS_TAG() + ecs_max_tag_id += 1 + return ecs_max_tag_id +end + +local function ECS_META(id: i53, ty: i53, value: any?) + local bundle = ecs_metadata[id] + if bundle == nil then + bundle = {} + ecs_metadata[id] = bundle + end + bundle[ty] = if value == nil then NULL else value +end + +local function ECS_META_RESET() + ecs_metadata = {} + ecs_max_component_id = 0 + ecs_max_tag_id = EcsRest +end + +local function ECS_COMBINE(id: number, generation: number): i53 + return id + (generation * ECS_ENTITY_MASK) +end + +local function ECS_IS_PAIR(e: number): boolean + return e > ECS_PAIR_OFFSET +end + +local function ECS_GENERATION_INC(e: i53): i53 + if e > ECS_ENTITY_MASK then + local id = e % ECS_ENTITY_MASK + local generation = e // ECS_ENTITY_MASK + + local next_gen = generation + 1 + if next_gen >= ECS_GENERATION_MASK then + return id + end + + return ECS_COMBINE(id, next_gen) + end + return ECS_COMBINE(e, 1) +end + +local function ECS_ENTITY_T_LO(e: i53): i24 + return e % ECS_ENTITY_MASK +end + +local function ECS_ID(e: i53) + return e % ECS_ENTITY_MASK +end + +local function ECS_GENERATION(e: i53) + return e // ECS_ENTITY_MASK +end + +local function ECS_ENTITY_T_HI(e: i53): i24 + return e // ECS_ENTITY_MASK +end + +local function ECS_PAIR(pred: i53, obj: i53): i53 + pred %= ECS_ENTITY_MASK + obj %= ECS_ENTITY_MASK + + return obj + (pred * ECS_ENTITY_MASK) + ECS_PAIR_OFFSET +end + +local function ECS_PAIR_FIRST(e: i53): i24 + return (e - ECS_PAIR_OFFSET) // ECS_ENTITY_MASK +end + +local function ECS_PAIR_SECOND(e: i53): i24 + return (e - ECS_PAIR_OFFSET) % ECS_ENTITY_MASK +end + +local function entity_index_try_get_any( + entity_index: ecs_entity_index_t, + entity: number +): ecs_record_t? + local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] + + if not r or r.dense == 0 then + return nil + end + + return r +end + +local function entity_index_try_get(entity_index: ecs_entity_index_t, entity: number): ecs_record_t? + local r = entity_index_try_get_any(entity_index, entity) + if r then + local r_dense = r.dense + if r_dense > entity_index.alive_count then + return nil + end + if entity_index.dense_array[r_dense] ~= entity then + return nil + end + end + return r +end + +local function entity_index_try_get_fast(entity_index: ecs_entity_index_t, entity: number): ecs_record_t? + local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] + if r then + if entity_index.dense_array[r.dense] ~= entity then + return nil + end + end + return r +end + +local function entity_index_is_alive(entity_index: ecs_entity_index_t, entity: i53) + return entity_index_try_get(entity_index, entity) ~= nil +end + +local function entity_index_get_alive(entity_index: ecs_entity_index_t, entity: i53): i53? + local r = entity_index_try_get_any(entity_index, entity) + if r then + return entity_index.dense_array[r.dense] + end + return nil +end + +local function ecs_get_alive(world, entity) + if entity == 0 then + return 0 + end + + local eindex = world.entity_index + + if entity_index_is_alive(eindex, entity) then + return entity + end + + if entity > ECS_ENTITY_MASK then + return 0 + end + + local current = entity_index_get_alive(eindex, entity) + if not current or not entity_index_is_alive(eindex, current) then + return 0 + end + + return current +end + +local ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY = "Entity is outside range" + +local function entity_index_new_id(entity_index: ecs_entity_index_t): i53 + local dense_array = entity_index.dense_array + local alive_count = entity_index.alive_count + local sparse_array = entity_index.sparse_array + local max_id = entity_index.max_id + + if alive_count < max_id then + alive_count += 1 + entity_index.alive_count = alive_count + local id = dense_array[alive_count] + return id + end + + local id = max_id + 1 + local range_end = entity_index.range_end + ecs_assert(range_end == nil or id < range_end, ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY) + + entity_index.max_id = id + alive_count += 1 + entity_index.alive_count = alive_count + dense_array[alive_count] = id + sparse_array[id] = { dense = alive_count } :: ecs_record_t + + return id +end + +local function ecs_pair_first(world: ecs_world_t, e: i53) + local pred = ECS_PAIR_FIRST(e) + return ecs_get_alive(world, pred) +end + +local function ecs_pair_second(world: ecs_world_t, e: i53) + local obj = ECS_PAIR_SECOND(e) + return ecs_get_alive(world, obj) +end + +local function query_match(query: ecs_query_data_t, + archetype: ecs_archetype_t) + local records = archetype.records + local with = query.filter_with + + for _, id in with do + if not records[id] then + return false + end + end + + local without = query.filter_without + if without then + for _, id in without do + if records[id] then + return false + end + end + end + + return true +end + +local function find_observers(world: ecs_world_t, event: i53, + component: i53): { ecs_observer_t }? + local cache = world.observable[event] + if not cache then + return nil + end + return cache[component] :: any +end + +local function archetype_move( + entity_index: ecs_entity_index_t, + to: ecs_archetype_t, + dst_row: i24, + from: ecs_archetype_t, + src_row: i24 ) - local columns = from.columns - local sourceEntities = from.entities - local destinationEntities = to.entities - local destinationColumns = to.columns - local tr = to.records - local types = from.types + local src_columns = from.columns + local dst_columns = to.columns + local dst_entities = to.entities + local src_entities = from.entities - for i, column in columns do + 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 targetColumn = destinationColumns[tr[types[i]]] + local tr = records[id_types[i]] -- Sometimes target column may not exist, e.g. when you remove a component. - if targetColumn then - targetColumn[destinationRow] = column[sourceRow] + 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. - local last = #column - if sourceRow ~= last then + if src_row ~= last then -- Swap rempves columns to ensure there are no holes in the archetype. - column[sourceRow] = column[last] + column[src_row] = column[last] end column[last] = nil end - -- Move the entity from the source to the destination archetype. - local atSourceRow = sourceEntities[sourceRow] - destinationEntities[destinationRow] = atSourceRow - entityIndex[atSourceRow].row = destinationRow + 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 movedAway = #sourceEntities - if sourceRow ~= movedAway then - local atMovedAway = sourceEntities[movedAway] - sourceEntities[sourceRow] = atMovedAway - entityIndex[atMovedAway].row = sourceRow + local e1 = src_entities[src_row] + local e2 = src_entities[moved] + + if src_row ~= moved then + src_entities[src_row] = e2 end - sourceEntities[movedAway] = nil + 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 archetypeAppend(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 newEntity(entityId: i53, record: Record, archetype: Archetype) - local row = archetypeAppend(entityId, archetype) +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 moveEntity(entityIndex, entityId: 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 destinationRow = archetypeAppend(entityId, to) - transitionArchetype(entityIndex, to, destinationRow, from, sourceRow) + local dst_row = archetype_append(entity, to) + archetype_move(entity_index, to, dst_row, from, sourceRow) record.archetype = to - record.row = destinationRow + record.row = dst_row end -local function hash(arr): string | number +local function hash(arr: { number }): string return table.concat(arr, "_") end -local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?) - local destinationIds = to.types - local records = to.records - local id = to.id +local function fetch(id: i53, records: { number }, + columns: { Column }, row: number): any + local tr = records[id] - for i, destinationId in destinationIds do - local archetypesMap = componentIndex[destinationId] + if not tr then + return nil + end - if not archetypesMap then - archetypesMap = { size = 0, sparse = {} } - componentIndex[destinationId] = archetypesMap - end + return columns[tr][row] +end - archetypesMap.sparse[id] = i - records[destinationId] = i +local function world_get(world: ecs_world_t, entity: i53, + a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any + local record = entity_index_try_get_fast(world.entity_index, entity) + if not record then + return nil + end + + local archetype = record.archetype + if not archetype then + return nil + end + + local records = archetype.records + local columns = archetype.columns + local row = record.row + + local va = fetch(a, records, columns, row) + + if not b then + return va + elseif not c then + return va, fetch(b, records, columns, row) + elseif not d then + return va, fetch(b, records, columns, row), fetch(c, records, columns, row) + elseif not e then + return va, fetch(b, records, columns, row), fetch(c, records, columns, row), fetch(d, records, columns, row) + else + error("args exceeded") end end -local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype - local ty = hash(types) - - local id = world.nextArchetypeId + 1 - world.nextArchetypeId = id - - local length = #types - local columns = table.create(length) :: { any } - - for index in types do - columns[index] = {} +local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): boolean + local record = entity_index_try_get_fast(world.entity_index, entity) + if not record then + return false end - local archetype = { + local archetype = record.archetype + if not archetype then + return false + end + + local records = archetype.records + + return records[id] ~= nil +end + +local function ecs_is_tag(world: ecs_world_t, entity: i53): boolean + local idr = world.component_index[entity] + if idr then + return bit32.band(idr.flags, ECS_ID_IS_TAG) ~= 0 + end + return not world_has_one_inline(world, entity, EcsComponent) +end + +local function world_has(world: ecs_world_t, entity: i53, + a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean + + local record = entity_index_try_get_fast(world.entity_index, entity) + if not record then + return false + end + + local archetype = record.archetype + if not archetype then + return false + end + + local records = archetype.records + + return records[a] ~= nil and + (b == nil or records[b] ~= nil) and + (c == nil or records[c] ~= nil) and + (d == nil or records[d] ~= nil) and + (e == nil or error("args exceeded")) +end + +local function world_target(world: ecs_world_t, entity: i53, relation: i24, index: number?): i24? + local nth = index or 0 + local record = entity_index_try_get_fast(world.entity_index, entity) + if not record then + return nil + end + + local archetype = record.archetype + if not archetype then + return nil + end + + local r = ECS_PAIR(relation, EcsWildcard) + + local count = archetype.counts[r] + if not count then + return nil + end + + if nth >= count then + nth = nth + count + 1 + end + + nth = archetype.types[nth + archetype.records[r]] + if not nth then + return nil + end + + return entity_index_get_alive(world.entity_index, + ECS_PAIR_SECOND(nth)) +end + +local function ECS_ID_IS_WILDCARD(e: i53): boolean + local first = ECS_ENTITY_T_HI(e) + local second = ECS_ENTITY_T_LO(e) + return first == EcsWildcard or second == EcsWildcard +end + +local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t + local component_index = world.component_index + local entity_index = world.entity_index + local idr: ecs_id_record_t? = component_index[id] + + if idr then + return idr + end + + local flags = ECS_ID_MASK + local relation = id + local target = 0 + local is_pair = ECS_IS_PAIR(id) + if is_pair then + relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53 + ecs_assert(relation and entity_index_is_alive( + entity_index, relation), ECS_INTERNAL_ERROR) + target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53 + ecs_assert(target and entity_index_is_alive( + entity_index, target), ECS_INTERNAL_ERROR) + end + + local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) + local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0) + + local has_delete = false + + if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then + has_delete = true + end + + local on_add, on_change, on_remove = world_get(world, + relation, EcsOnAdd, EcsOnChange, EcsOnRemove) + + local is_tag = not world_has_one_inline(world, + relation, EcsComponent) + + if is_tag and is_pair then + is_tag = not world_has_one_inline(world, target, EcsComponent) + end + + flags = bit32.bor( + flags, + if has_delete then ECS_ID_DELETE else 0, + if is_tag then ECS_ID_IS_TAG else 0 + ) + + idr = { + size = 0, + cache = {}, + counts = {}, + flags = flags, + hooks = { + on_add = on_add, + on_change = on_change, + on_remove = on_remove, + }, + } :: ecs_id_record_t + + component_index[id] = idr + + return idr +end + +local function archetype_append_to_records( + idr: ecs_id_record_t, + archetype: ecs_archetype_t, + id: i53, + index: number +) + local archetype_id = archetype.id + local archetype_records = archetype.records + local archetype_counts = archetype.counts + local idr_columns = idr.cache + local idr_counts = idr.counts + local tr = idr_columns[archetype_id] + if not tr then + idr_columns[archetype_id] = index + idr_counts[archetype_id] = 1 + + archetype_records[id] = index + archetype_counts[id] = 1 + else + local max_count = idr_counts[archetype_id] + 1 + idr_counts[archetype_id] = max_count + archetype_counts[id] = max_count + end +end + +local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: i53?): ecs_archetype_t + local archetype_id = (world.max_archetype_id :: number) + 1 + world.max_archetype_id = archetype_id + + local length = #id_types + local columns = (table.create(length) :: any) :: { Column } + + local records: { number } = {} + local counts: { number } = {} + + local archetype: ecs_archetype_t = { columns = columns, - edges = {}, entities = {}, - id = id, - records = {}, + id = archetype_id, + records = records, + counts = counts, type = ty, - types = types, + types = id_types, } - world.archetypeIndex[ty] = archetype - world.archetypes[id] = archetype - if length > 0 then - createArchetypeRecords(world.componentIndex, archetype, prev) + + for i, component_id in id_types do + local idr = id_record_ensure(world, component_id) + archetype_append_to_records(idr, archetype, component_id, i) + + if ECS_IS_PAIR(component_id) then + local relation = ECS_PAIR_FIRST(component_id) + local object = ECS_PAIR_SECOND(component_id) + local r = ECS_PAIR(relation, EcsWildcard) + local idr_r = id_record_ensure(world, r) + archetype_append_to_records(idr_r, archetype, r, i) + + local t = ECS_PAIR(EcsWildcard, object) + local idr_t = id_record_ensure(world, t) + archetype_append_to_records(idr_t, archetype, t, i) + end + + if bit32.band(idr.flags, ECS_ID_IS_TAG) == 0 then + columns[i] = {} + else + columns[i] = NULL_ARRAY + end end + for id in records do + local observer_list = find_observers(world, EcsOnArchetypeCreate, id) + if not observer_list then + continue + end + for _, observer in observer_list do + if query_match(observer.query, archetype) then + observer.callback(archetype) + end + end + end + + world.archetype_index[ty] = archetype + world.archetypes[archetype_id] = archetype + world.archetype_edges[archetype.id] = {} :: Map + return archetype end -local World = {} -World.__index = World -function World.new() - local self = setmetatable({ - archetypeIndex = {}, - archetypes = {}, - componentIndex = {}, - entityIndex = {}, - hooks = { - [ON_ADD] = {}, - }, - nextArchetypeId = 0, - nextComponentId = 0, - nextEntityId = 0, - ROOT_ARCHETYPE = (nil :: any) :: Archetype, - }, World) - return self -end +local function world_range(world: ecs_world_t, range_begin: number, range_end: number?) + local entity_index = world.entity_index -local function emit(world, eventDescription) - local event = eventDescription.event + entity_index.range_begin = range_begin + entity_index.range_end = range_end - table.insert(world.hooks[event], { - archetype = eventDescription.archetype, - ids = eventDescription.ids, - offset = eventDescription.offset, - otherArchetype = eventDescription.otherArchetype, - }) -end + local max_id = entity_index.max_id -local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) - if #added > 0 then - emit(world, { - archetype = archetype, - event = ON_ADD, - ids = added, - offset = row, - otherArchetype = otherArchetype, - }) + 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 -export type World = typeof(World.new()) +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 -local function ensureArchetype(world: World, types, prev) - if #types < 1 then + if not dense or r.dense == 0 then + r.dense = index + dense = index + end + + local any = dense_array[dense] + if dense <= alive_count then + if any ~= entity then + error("Entity ID is already in use with a different generation") + else + return entity + end + end + + local e_swap = dense_array[dense] + local r_swap = entity_index_try_get_any(entity_index, e_swap) :: ecs_record_t + alive_count += 1 + entity_index.alive_count = alive_count + r_swap.dense = dense + r.dense = alive_count + dense_array[dense] = e_swap + dense_array[alive_count] = entity + + return entity + else + for i = max_id + 1, index do + sparse_array[i] = { dense = i } :: ecs_record_t + dense_array[i] = i + end + entity_index.max_id = index + + local e_swap = dense_array[alive_count] + local r_swap = sparse_array[alive_count] + r_swap.dense = index + + alive_count += 1 + entity_index.alive_count = alive_count + + r = sparse_array[index] + + r.dense = alive_count + + sparse_array[index] = r + + dense_array[index] = e_swap + dense_array[alive_count] = entity + + + return entity + end + end + return entity_index_new_id(entity_index) +end + +local function world_parent(world: ecs_world_t, entity: i53) + return world_target(world, entity, EcsChildOf, 0) +end + +local function archetype_ensure(world: ecs_world_t, id_types): ecs_archetype_t + if #id_types < 1 then return world.ROOT_ARCHETYPE end - local ty = hash(types) - local archetype = world.archetypeIndex[ty] + local ty = hash(id_types) + local archetype = world.archetype_index[ty] if archetype then return archetype end - return archetypeOf(world, types, prev) + return archetype_create(world, id_types, ty) end -local function findInsert(types: { i53 }, toAdd: i53) - for i, id in types do +local function find_insert(id_types: { i53 }, toAdd: i53): number + for i, id in id_types do if id == toAdd then + error("Duplicate component id") return -1 end if id > toAdd then return i end end - return #types + 1 + return #id_types + 1 end -local function findArchetypeWith(world: World, node: Archetype, componentId: i53) - local 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 at = findInsert(types, componentId) - 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 +local function find_archetype_without( + world: ecs_world_t, + node: ecs_archetype_t, + id: i53 +): ecs_archetype_t + local id_types = node.types + local at = table.find(id_types, id) - local destinationType = table.clone(node.types) - table.insert(destinationType, at, componentId) - return ensureArchetype(world, destinationType, node) + local dst = table.clone(id_types) + table.remove(dst, at) + + return archetype_ensure(world, dst) end -local function ensureEdge(archetype: Archetype, componentId: i53) - local edges = archetype.edges - local edge = edges[componentId] - if not edge then - edge = {} :: any - edges[componentId] = edge - end - return edge + +local function create_edge_for_remove( + world: ecs_world_t, + node: ecs_archetype_t, + edge: Map, + id: i53 +): ecs_archetype_t + local to = find_archetype_without(world, node, id) + local edges = world.archetype_edges + local archetype_id = node.id + edges[archetype_id][id] = to + edges[to.id][id] = node + return to end -local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype - if not from then - -- If there was no source archetype then it should return the ROOT_ARCHETYPE - local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE - if not ROOT_ARCHETYPE then - ROOT_ARCHETYPE = archetypeOf(world, {}, nil) - world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never - end - from = ROOT_ARCHETYPE +local function archetype_traverse_remove( + world: ecs_world_t, + id: i53, + from: ecs_archetype_t +): ecs_archetype_t + local edges = world.archetype_edges + local edge = edges[from.id] + + local to: ecs_archetype_t = edge[id] + if to == nil then + to = find_archetype_without(world, from, id) + edge[id] = to + edges[to.id][id] = from end - local edge = ensureEdge(from, componentId) - local add = edge.add - if not add then - -- Save an edge using the component ID to the archetype to allow - -- faster traversals to adjacent archetypes. - add = findArchetypeWith(world, from, componentId) - edge.add = add :: never - end - - return add + return to end -local function ensureRecord(entityIndex, entityId: i53): Record - local record = entityIndex[entityId] +local function find_archetype_with(world, id, from): ecs_archetype_t + local id_types = from.types + local at = find_insert(id_types, id) + local dst = table.clone(id_types) :: { i53 } + table.insert(dst, at, id) + + return archetype_ensure(world, dst) +end + +local function archetype_traverse_add(world, id, from: ecs_archetype_t): ecs_archetype_t + from = from or world.ROOT_ARCHETYPE + if from.records[id] then + return from + end + local edges = world.archetype_edges + local edge = edges[from.id] + + local to = edge[id] + if not to then + to = find_archetype_with(world, id, from) + edge[id] = to + edges[to.id][id] = from + end + + return to +end + +local function world_add( + world: ecs_world_t, + entity: i53, + id: i53 +): () + local entity_index = world.entity_index + local record = entity_index_try_get_fast(entity_index, entity) if not record then - record = {} - entityIndex[entityId] = record + return end - return record :: Record + local from = record.archetype + local to = archetype_traverse_add(world, id, from) + if from == to then + return + end + if from then + entity_move(entity_index, entity, record, to) + else + if #to.types > 0 then + new_entity(entity, record, to) + end + end + + local idr = world.component_index[id] + local on_add = idr.hooks.on_add + + if on_add then + on_add(entity, id) + end end -function World.set(world: World, entityId: i53, componentId: i53, data: unknown) - local record = ensureRecord(world.entityIndex, entityId) - local from = record.archetype - local to = archetypeTraverseAdd(world, componentId, from) +local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown): () + local entity_index = world.entity_index + local record = entity_index_try_get_fast(entity_index, entity) + if not record then + return + end + + local from: ecs_archetype_t = record.archetype + local to: ecs_archetype_t = archetype_traverse_add(world, id, from) + local idr = world.component_index[id] + local idr_hooks = idr.hooks if from == to then + local tr = (to :: ecs_archetype_t).records[id] + local column = from.columns[tr] + column[record.row] = data + -- If the archetypes are the same it can avoid moving the entity -- and just set the data directly. - local archetypeRecord = to.records[componentId] - from.columns[archetypeRecord][record.row] = data - -- Should fire an OnSet event here. + local on_change = idr_hooks.on_change + if on_change then + on_change(entity, id, data) + end + return end if from then -- If there was a previous archetype, then the entity needs to move the archetype - moveEntity(world.entityIndex, entityId, record, to) + entity_move(entity_index, entity, record, to) else if #to.types > 0 then -- When there is no previous archetype it should create the archetype - newEntity(entityId, record, to) - onNotifyAdd(world, to, from, record.row, { componentId }) + new_entity(entity, record, to) end end - local archetypeRecord = to.records[componentId] - to.columns[archetypeRecord][record.row] = data -end + local tr = to.records[id] + local column = to.columns[tr] -local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype - local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype - local edge = ensureEdge(from, componentId) + column[record.row] = data - local remove = edge.remove - if not remove then - local to = table.clone(from.types) - table.remove(to, table.find(to, componentId)) - remove = ensureArchetype(world, to, from) - edge.remove = remove :: never - end - - return remove -end - -function World.remove(world: World, entityId: i53, componentId: i53) - local entityIndex = world.entityIndex - local record = ensureRecord(entityIndex, entityId) - local sourceArchetype = record.archetype - local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) - - if sourceArchetype and not (sourceArchetype == destinationArchetype) then - moveEntity(entityIndex, entityId, record, destinationArchetype) + local on_add = idr_hooks.on_add + if on_add then + on_add(entity, id, data) end end --- Keeping the function as small as possible to enable inlining -local function get(record: Record, componentId: i24) - local archetype = record.archetype - local archetypeRecord = archetype.records[componentId] - - if not archetypeRecord then - return nil +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 archetype.columns[archetypeRecord][record.row] + return id end -function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) - local id = entityId - local record = world.entityIndex[id] +local function world_remove(world: ecs_world_t, entity: i53, id: i53) + local entity_index = world.entity_index + local record = entity_index_try_get_fast(entity_index, entity) if not record then - return nil + return + end + local from = record.archetype + + if not from then + return end - local va = get(record, a) + if from.records[id] then + local idr = world.component_index[id] + local on_remove = idr.hooks.on_remove + if on_remove then + on_remove(entity, id) + end - if b == nil then - return va - elseif c == nil then - return va, get(record, b) - elseif d == nil then - return va, get(record, b), get(record, c) - elseif e == nil then - return va, get(record, b), get(record, c), get(record, d) + local to = archetype_traverse_remove(world, id, record.archetype) + + entity_move(entity_index, entity, record, to) + end +end + +local function archetype_fast_delete_last(columns: { Column }, column_count: number) + for i, column in columns do + if column ~= NULL_ARRAY then + column[column_count] = nil + end + end +end + +local function archetype_fast_delete(columns: { Column }, column_count: number, row: number) + for i, column in columns do + if column ~= NULL_ARRAY then + column[row] = column[column_count] + column[column_count] = nil + end + end +end + +local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, row: number) + local entity_index = world.entity_index + local component_index = world.component_index + local columns = archetype.columns + local id_types = archetype.types + local entities = archetype.entities + local column_count = #entities + local last = #entities + local move = entities[last] + -- We assume first that the entity is the last in the archetype + local delete = move + + if row ~= last then + local record_to_move = entity_index_try_get_any(entity_index, move) + if record_to_move then + record_to_move.row = row + end + + delete = entities[row] + entities[row] = move + end + + for _, id in id_types do + local idr = component_index[id] + local on_remove = idr.hooks.on_remove + if on_remove then + on_remove(delete, id) + end + end + + entities[last] = nil :: any + + if row == last then + archetype_fast_delete_last(columns, column_count) else - error("args exceeded") + archetype_fast_delete(columns, column_count, row) end end --- the less creation the better -local function actualNoOperation() end -local function noop(_self: Query, ...: i53): () -> (number, ...any) - return actualNoOperation :: any +local function world_clear(world: ecs_world_t, entity: i53) + local entity_index = world.entity_index + local component_index = world.component_index + local archetypes = world.archetypes + local tgt = ECS_PAIR(EcsWildcard, entity) + local idr_t = component_index[tgt] + local idr = component_index[entity] + local rel = ECS_PAIR(entity, EcsWildcard) + local idr_r = component_index[rel] + + if idr then + local count = 0 + local queue = {} + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + local entities = idr_archetype.entities + local n = #entities + count += n + table.move(entities, 1, n, #queue + 1, queue) + end + for _, e in queue do + world_remove(world, e, entity) + end + end + + if idr_t then + local queue: { i53 } + local ids: Map + + local count = 0 + local archetype_ids = idr_t.cache + for archetype_id in archetype_ids do + local idr_t_archetype = archetypes[archetype_id] + local idr_t_types = idr_t_archetype.types + local entities = idr_t_archetype.entities + local removal_queued = false + + for _, id in idr_t_types do + if not ECS_IS_PAIR(id) then + continue + end + local object = entity_index_get_alive( + entity_index, ECS_PAIR_SECOND(id)) + if object ~= entity then + continue + end + if not ids then + ids = {} :: { [i53]: boolean } + end + ids[id] = true + removal_queued = true + end + + if not removal_queued then + continue + end + + if not queue then + queue = {} :: { i53 } + end + + local n = #entities + table.move(entities, 1, n, count + 1, queue) + count += n + end + + for id in ids do + for _, child in queue do + world_remove(world, child, id) + end + end + end + + if idr_r then + local count = 0 + local archetype_ids = idr_r.cache + local ids = {} + local queue = {} + for archetype_id in archetype_ids do + local idr_r_archetype = archetypes[archetype_id] + local entities = idr_r_archetype.entities + local tr = idr_r_archetype.records[rel] + local tr_count = idr_r_archetype.counts[rel] + local types = idr_r_archetype.types + for i = tr, tr + tr_count - 1 do + ids[types[i]] = true + end + local n = #entities + table.move(entities, 1, n, count + 1, queue) + count += n + end + + for _, e in queue do + for id in ids do + world_remove(world, e, id) + end + end + end end -local EmptyQuery = { - __iter = noop, - without = noop, +local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t) + if archetype == world.ROOT_ARCHETYPE then + return + end + + local component_index = world.component_index + local archetype_edges = world.archetype_edges + + for id, edge in archetype_edges[archetype.id] do + archetype_edges[edge.id][id] = nil + end + + local archetype_id = archetype.id + world.archetypes[archetype_id] = nil :: any + world.archetype_index[archetype.type] = nil :: any + local records = archetype.records + + for id in records do + local observer_list = find_observers(world, EcsOnArchetypeDelete, id) + if not observer_list then + continue + end + for _, observer in observer_list do + if query_match(observer.query, archetype) then + observer.callback(archetype) + end + end + end + + for id in records do + local idr = component_index[id] + idr.cache[archetype_id] = nil :: any + idr.counts[archetype_id] = nil + idr.size -= 1 + records[id] = nil :: any + if idr.size == 0 then + component_index[id] = nil :: any + end + end +end + +local function world_cleanup(world: ecs_world_t) + local archetypes = world.archetypes + + for _, archetype in archetypes do + if #archetype.entities == 0 then + archetype_destroy(world, archetype) + end + end + + local new_archetypes = table.create(#archetypes) :: { ecs_archetype_t } + local new_archetype_map = {} + + for index, archetype in archetypes do + new_archetypes[index] = archetype + new_archetype_map[archetype.type] = archetype + end + + world.archetypes = new_archetypes + world.archetype_index = new_archetype_map +end + +local function world_delete(world: ecs_world_t, entity: i53) + local entity_index = world.entity_index + local record = entity_index_try_get(entity_index, entity) + if not record then + return + end + + local archetype = record.archetype + local row = record.row + + if archetype then + -- In the future should have a destruct mode for + -- deleting archetypes themselves. Maybe requires recycling + archetype_delete(world, archetype, row) + end + + local delete = entity + local component_index = world.component_index + local archetypes = world.archetypes + local tgt = ECS_PAIR(EcsWildcard, delete) + local rel = ECS_PAIR(delete, EcsWildcard) + + local idr_t = component_index[tgt] + local idr = component_index[delete] + local idr_r = component_index[rel] + + if idr then + local flags = idr.flags + if bit32.band(flags, ECS_ID_DELETE) ~= 0 then + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + world_delete(world, entities[i]) + end + + archetype_destroy(world, idr_archetype) + end + else + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + world_remove(world, entities[i], delete) + end + + archetype_destroy(world, idr_archetype) + end + end + end + + if idr_t then + local children: { i53 } + local ids: Map + + local count = 0 + local archetype_ids = idr_t.cache + for archetype_id in archetype_ids do + local idr_t_archetype = archetypes[archetype_id] + local idr_t_types = idr_t_archetype.types + local entities = idr_t_archetype.entities + local removal_queued = false + + for _, id in idr_t_types do + if not ECS_IS_PAIR(id) then + continue + end + local object = entity_index_get_alive( + entity_index, ECS_PAIR_SECOND(id)) + if object ~= delete then + continue + end + local id_record = component_index[id] + local flags = id_record.flags + local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE) + if flags_delete_mask ~= 0 then + for i = #entities, 1, -1 do + local child = entities[i] + world_delete(world, child) + end + break + else + if not ids then + ids = {} :: { [i53]: boolean } + end + ids[id] = true + removal_queued = true + end + end + + if not removal_queued then + continue + end + if not children then + children = {} :: { i53 } + end + local n = #entities + table.move(entities, 1, n, count + 1, children) + count += n + end + + if ids then + for _, child in children do + for id in ids do + world_remove(world, child, id) + end + end + end + + for archetype_id in archetype_ids do + archetype_destroy(world, archetypes[archetype_id]) + end + end + + if idr_r then + local archetype_ids = idr_r.cache + local flags = idr_r.flags + if (bit32.band(flags, ECS_ID_DELETE) :: number) ~= 0 then + for archetype_id in archetype_ids do + local idr_r_archetype = archetypes[archetype_id] + local entities = idr_r_archetype.entities + local n = #entities + for i = n, 1, -1 do + world_delete(world, entities[i]) + end + archetype_destroy(world, idr_r_archetype) + end + else + local children = {} + local count = 0 + local ids = {} + for archetype_id in archetype_ids do + local idr_r_archetype = archetypes[archetype_id] + local entities = idr_r_archetype.entities + local tr = idr_r_archetype.records[rel] + local tr_count = idr_r_archetype.counts[rel] + local types = idr_r_archetype.types + for i = tr, tr + tr_count - 1 do + ids[types[i]] = true + end + + local n = #entities + table.move(entities, 1, n, count + 1, children) + count += n + end + for _, child in children do + for id in ids do + world_remove(world, child, id) + end + end + + for archetype_id in archetype_ids do + archetype_destroy(world, archetypes[archetype_id]) + end + end + end + + local dense_array = entity_index.dense_array + local dense = record.dense + local i_swap = entity_index.alive_count + entity_index.alive_count = i_swap - 1 + + local e_swap = dense_array[i_swap] + local r_swap = entity_index_try_get_any(entity_index, e_swap) :: ecs_record_t + + r_swap.dense = dense + record.archetype = nil :: any + record.row = nil :: any + record.dense = i_swap + + dense_array[dense] = e_swap + dense_array[i_swap] = ECS_GENERATION_INC(entity) +end + +local function world_exists(world: ecs_world_t, entity): boolean + return entity_index_try_get_any(world.entity_index, entity) ~= nil +end + +local function world_contains(world: ecs_world_t, entity): boolean + return entity_index_is_alive(world.entity_index, entity) +end + +local function NOOP() end + +export type QueryInner = { + compatible_archetypes: { Archetype }, + ids: { i53 }, + filter_with: { i53 }, + filter_without: { i53 }, + next: () -> (number, ...any), + world: World, } -EmptyQuery.__index = EmptyQuery -setmetatable(EmptyQuery, EmptyQuery) -export type Query = typeof(EmptyQuery) +local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) + local world_query_iter_next -function World.query(world: World, ...: i53): Query - -- breaking? - if (...) == nil then - error("Missing components") + 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 - local compatibleArchetypes = {} + if not B then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + end + + local row = i + i -= 1 + + return entity, a[row] + end + elseif not C then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row] + end + elseif not D then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row] + end + elseif not E then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row] + end + elseif not F then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row] + end + elseif not G then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row], f[row] + end + elseif not H then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + end + elseif not I then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + h = columns[records[H]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + end + else + local output = {} + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + end + + local row = i + i -= 1 + + for j, id in ids do + output[j] = columns[records[id]][row] + end + + return entity, unpack(output) + end + end + + query.next = world_query_iter_next + return world_query_iter_next +end + +local function query_iter(query): () -> (number, ...any) + local query_next = query.next + if not query_next then + query_next = query_iter_init(query) + end + return query_next +end + +local function query_without(query: ecs_query_data_t, ...: i53) + local without = { ... } + query.filter_without = without + local compatible_archetypes = query.compatible_archetypes + for i = #compatible_archetypes, 1, -1 do + local archetype = compatible_archetypes[i] + local records = archetype.records + local matches = true + + for _, id in without do + if records[id] then + matches = false + break + end + end + + if matches then + continue + end + + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil :: any + end + + return query :: any +end + +local function query_with(query: ecs_query_data_t, ...: i53) + local compatible_archetypes = query.compatible_archetypes + local with = { ... } + query.filter_with = with + + for i = #compatible_archetypes, 1, -1 do + local archetype = compatible_archetypes[i] + local records = archetype.records + local matches = true + + for _, id in with do + if not records[id] then + matches = false + break + end + end + + if matches then + continue + end + + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil :: any + end + + return query :: any +end + +-- Meant for directly iterating over archetypes to minimize +-- function call overhead. Should not be used unless iterating over +-- hundreds of thousands of entities in bulk. +local function query_archetypes(query) + return query.compatible_archetypes +end + +local function query_cached(query: ecs_query_data_t) + local with = query.filter_with + local ids = query.ids + if with then + table.move(ids, 1, #ids, #with + 1, with) + else + query.filter_with = ids + end + + local compatible_archetypes = query.compatible_archetypes + local lastArchetype = 1 + + local A, B, C, D, E, F, G, H, I = unpack(ids) + local a: Column, b: Column, c: Column, d: Column + local e: Column, f: Column, g: Column, h: Column + + local world_query_iter_next + local columns: { Column } + local entities: { number } + local i: number + local archetype: ecs_archetype_t + local records: { number } + local archetypes = query.compatible_archetypes + + local world = query.world :: { observable: ecs_observable_t } + -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively + -- because the event will be emitted for all components of that Archetype. + local observable = world.observable :: ecs_observable_t + local on_create_action = observable[EcsOnArchetypeCreate] + if not on_create_action then + on_create_action = {} :: Map + observable[EcsOnArchetypeCreate] = on_create_action + end + local query_cache_on_create = on_create_action[A] + if not query_cache_on_create then + query_cache_on_create = {} + on_create_action[A] = query_cache_on_create + end + + local on_delete_action = observable[EcsOnArchetypeDelete] + if not on_delete_action then + on_delete_action = {} :: Map + observable[EcsOnArchetypeDelete] = on_delete_action + end + local query_cache_on_delete = on_delete_action[A] + if not query_cache_on_delete then + query_cache_on_delete = {} + on_delete_action[A] = query_cache_on_delete + end + + local function on_create_callback(archetype) + table.insert(archetypes, archetype) + end + + local function on_delete_callback(archetype) + local i = table.find(archetypes, archetype) :: number + if i == nil then + return + end + local n = #archetypes + archetypes[i] = archetypes[n] + archetypes[n] = nil + end + + local observer_for_create = { query = query, callback = on_create_callback } + local observer_for_delete = { query = query, callback = on_delete_callback } + + table.insert(query_cache_on_create, observer_for_create) + table.insert(query_cache_on_delete, observer_for_delete) + + local function cached_query_iter() + lastArchetype = 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return NOOP + end + entities = archetype.entities + i = #entities + records = archetype.records + columns = archetype.columns + if not B then + a = columns[records[A]] + elseif not C then + a = columns[records[A]] + b = columns[records[B]] + elseif not D then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + elseif not E then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + elseif not F then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + elseif not G then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + elseif not H then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + elseif not I then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + h = columns[records[H]] + end + + return world_query_iter_next + end + + if not B then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + end + + local row = i + i -= 1 + + return entity, a[row] + end + elseif not C then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row] + end + elseif not D then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row] + end + elseif not E then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row] + end + elseif not F then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row] + end + elseif not G then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row], f[row] + end + elseif not H then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + end + elseif not I then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + h = columns[records[H]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + end + else + local queryOutput = {} + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + end + + local row = i + i -= 1 + + if not F then + return entity, a[row], b[row], c[row], d[row], e[row] + elseif not G then + return entity, a[row], b[row], c[row], d[row], e[row], f[row] + elseif not H then + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + elseif not I then + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + end + + for j, id in ids do + queryOutput[j] = columns[records[id]][row] + end + + return entity, unpack(queryOutput) + end + end + + local cached_query = query :: any + cached_query.archetypes = query_archetypes + cached_query.__iter = cached_query_iter + cached_query.iter = cached_query_iter + setmetatable(cached_query, cached_query) + return cached_query +end + +local Query = {} +Query.__index = Query +Query.__iter = query_iter +Query.iter = query_iter_init +Query.without = query_without +Query.with = query_with +Query.archetypes = query_archetypes +Query.cached = query_cached + +local function world_query(world: ecs_world_t, ...) + local compatible_archetypes = {} local length = 0 - local components = { ... } + local ids = { ... } + local archetypes = world.archetypes - local queryLength = #components - local firstArchetypeMap - local componentIndex = world.componentIndex + local idr: ecs_id_record_t? + local component_index = world.component_index - for _, componentId in components do - local map = componentIndex[componentId] + 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 EmptyQuery + return q end - if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then - firstArchetypeMap = map + if idr == nil or (map.size :: number) < (idr.size :: number) then + idr = map end end - for id in firstArchetypeMap.sparse do - local archetype = archetypes[id] - local archetypeRecords = archetype.records - local indices = {} + if idr == nil then + return q + end + + for archetype_id in idr.cache do + local compatibleArchetype = archetypes[archetype_id] + if #compatibleArchetype.entities == 0 then + continue + end + local records = compatibleArchetype.records + local skip = false - for i, componentId in components do - local index = archetypeRecords[componentId] - if not index then + for i, id in ids do + local tr = records[id] + if not tr then skip = true break end - indices[i] = index end if skip then @@ -456,204 +2358,361 @@ function World.query(world: World, ...: i53): Query end length += 1 - compatibleArchetypes[length] = { archetype, indices } + compatible_archetypes[length] = compatibleArchetype end - local lastArchetype, compatibleArchetype = next(compatibleArchetypes) - if not lastArchetype then - return EmptyQuery + return q +end + +local function world_each(world: ecs_world_t, id: i53): () -> () + local idr = world.component_index[id] + if not idr then + return NOOP end - local preparedQuery = {} - preparedQuery.__index = preparedQuery + local idr_cache = idr.cache + local archetypes = world.archetypes + local archetype_id = next(idr_cache, nil) :: number + local archetype = archetypes[archetype_id] + if not archetype then + return NOOP + end - function preparedQuery:without(...) - local withoutComponents = { ... } - for i = #compatibleArchetypes, 1, -1 do - local archetype = compatibleArchetypes[i][1] - local records = archetype.records - local shouldRemove = false + local entities = archetype.entities + local row = #entities - for _, componentId in withoutComponents do - if records[componentId] then - shouldRemove = true - break - end - end - - if shouldRemove then - table.remove(compatibleArchetypes, i) + return function(): any + local entity = entities[row] + while not entity do + archetype_id = next(idr_cache, archetype_id) :: number + if not archetype_id then + return end + archetype = archetypes[archetype_id] + entities = archetype.entities + row = #entities + entity = entities[row] end + row -= 1 + return entity + end +end - lastArchetype, compatibleArchetype = next(compatibleArchetypes) - if not lastArchetype then - return EmptyQuery - end +local function world_children(world: ecs_world_t, parent: i53) + return world_each(world, ECS_PAIR(EcsChildOf, parent)) +end - return self +export type Record = { + archetype: Archetype, + row: number, + dense: i24, +} +export type ComponentRecord = { + cache: { [Id]: number }, + counts: { [Id]: number }, + flags: number, + size: number, + hooks: { + on_add: ((entity: Entity, id: Entity, value: T) -> ())?, + on_change: ((entity: Entity, id: Entity, value: T) -> ())?, + on_remove: ((entity: Entity, id: Entity) -> ())?, + }, +} +export type ComponentIndex = Map +export type Archetypes = { [Id]: Archetype } + +export type EntityIndex = { + dense_array: Map, + sparse_array: Map, + alive_count: number, + max_id: number, + range_begin: number?, + range_end: number? +} + +local World = {} +World.__index = World + +World.entity = world_entity +World.query = world_query +World.remove = world_remove +World.clear = world_clear +World.delete = world_delete +World.component = world_component +World.add = world_add +World.set = world_set +World.get = world_get +World.has = world_has +World.target = world_target +World.parent = world_parent +World.contains = world_contains +World.exists = world_exists +World.cleanup = world_cleanup +World.each = world_each +World.children = world_children +World.range = world_range + +local function world_new() + local entity_index = { + dense_array = {}, + sparse_array = {}, + alive_count = 0, + max_id = 0, + } :: ecs_entity_index_t + local self = setmetatable({ + archetype_edges = {}, + + archetype_index = {} :: { [string]: Archetype }, + archetypes = {} :: Archetypes, + component_index = {} :: ComponentIndex, + entity_index = entity_index, + ROOT_ARCHETYPE = (nil :: any) :: Archetype, + + max_archetype_id = 0, + max_component_id = ecs_max_component_id, + + observable = {} :: Observable, + }, World) :: any + + self.ROOT_ARCHETYPE = archetype_create(self, {}, "") + + for i = 1, HI_COMPONENT_ID do + local e = entity_index_new_id(entity_index) + world_add(self, e, EcsComponent) end - local lastRow - local queryOutput = {} + for i = HI_COMPONENT_ID + 1, EcsRest do + -- Initialize built-in components + entity_index_new_id(entity_index) + end - function preparedQuery:__iter() - return function() - local archetype = compatibleArchetype[1] - local row = next(archetype.entities, lastRow) - while row == nil do - lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) - if lastArchetype == nil then - return - end - archetype = compatibleArchetype[1] - row = next(archetype.entities, row) + world_add(self, EcsName, EcsComponent) + world_add(self, EcsOnChange, EcsComponent) + world_add(self, EcsOnAdd, EcsComponent) + world_add(self, EcsOnRemove, EcsComponent) + world_add(self, EcsWildcard, EcsComponent) + world_add(self, EcsRest, EcsComponent) + + world_set(self, EcsOnAdd, EcsName, "jecs.OnAdd") + world_set(self, EcsOnRemove, EcsName, "jecs.OnRemove") + world_set(self, EcsOnChange, EcsName, "jecs.OnChange") + world_set(self, EcsWildcard, EcsName, "jecs.Wildcard") + world_set(self, EcsChildOf, EcsName, "jecs.ChildOf") + world_set(self, EcsComponent, EcsName, "jecs.Component") + world_set(self, EcsOnDelete, EcsName, "jecs.OnDelete") + world_set(self, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget") + world_set(self, EcsDelete, EcsName, "jecs.Delete") + world_set(self, EcsRemove, EcsName, "jecs.Remove") + world_set(self, EcsName, EcsName, "jecs.Name") + world_set(self, EcsRest, EcsRest, "jecs.Rest") + + world_add(self, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) + + for i = EcsRest + 1, ecs_max_tag_id do + entity_index_new_id(entity_index) + end + + for i, bundle in ecs_metadata do + for ty, value in bundle do + if value == NULL then + world_add(self, i, ty) + else + world_set(self, i, ty, value) end - lastRow = row - - local entityId = archetype.entities[row :: number] - local columns = archetype.columns - local tr = compatibleArchetype[2] - - if queryLength == 1 then - return entityId, columns[tr[1]][row] - elseif queryLength == 2 then - return entityId, columns[tr[1]][row], columns[tr[2]][row] - elseif queryLength == 3 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] - elseif queryLength == 4 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row] - elseif queryLength == 5 then - return entityId, - columns[tr[1]][row], - columns[tr[2]][row], - columns[tr[3]][row], - columns[tr[4]][row], - columns[tr[5]][row] - elseif queryLength == 6 then - return entityId, - columns[tr[1]][row], - columns[tr[2]][row], - columns[tr[3]][row], - columns[tr[4]][row], - columns[tr[5]][row], - columns[tr[6]][row] - elseif queryLength == 7 then - return entityId, - columns[tr[1]][row], - columns[tr[2]][row], - columns[tr[3]][row], - columns[tr[4]][row], - columns[tr[5]][row], - columns[tr[6]][row], - columns[tr[7]][row] - elseif queryLength == 8 then - return entityId, - columns[tr[1]][row], - columns[tr[2]][row], - columns[tr[3]][row], - columns[tr[4]][row], - columns[tr[5]][row], - columns[tr[6]][row], - columns[tr[7]][row], - columns[tr[8]][row] - end - - for i in components do - queryOutput[i] = columns[tr[i]][row] - end - - return entityId, unpack(queryOutput, 1, queryLength) end end - return setmetatable({}, preparedQuery) :: any + return self end -function World.component(world: World) - local componentId = world.nextComponentId + 1 - if componentId > HI_COMPONENT_ID then - -- IDs are partitioned into ranges because component IDs are not nominal, - -- so it needs to error when IDs intersect into the entity range. - error("Too many components, consider using world:entity() instead to create components.") - end - world.nextComponentId = componentId - return componentId -end +World.new = world_new -function World.entity(world: World) - local nextEntityId = world.nextEntityId + 1 - world.nextEntityId = nextEntityId - return nextEntityId + REST -end +export type Entity = number | { __T: T } +export type Id = number | { __T: T } +export type Pair = Id

+type ecs_id_t = Id | Pair | Pair<"Tag", T> +export type Item = (self: Query) -> (Entity, T...) +export type Iter = (query: Query) -> () -> (Entity, T...) -function World.delete(world: World, entityId: i53) - local entityIndex = world.entityIndex - local record = entityIndex[entityId] - moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE) - -- Since we just appended an entity to the ROOT_ARCHETYPE we have to remove it from - -- the entities array and delete the record. We know there won't be the hole since - -- we are always removing the last row. - --world.ROOT_ARCHETYPE.entities[record.row] = nil - --entityIndex[entityId] = nil -end - -function World.observer(world: World, ...) - local componentIds = { ... } - local idsCount = #componentIds - local hooks = world.hooks - - return { - event = function(event) - local hook = hooks[event] - hooks[event] = nil - - local last, change - return function() - last, change = next(hook, last) - if not last then - return - end - - local matched = false - local ids = change.ids - - while not matched do - local skip = false - for _, id in ids do - if not table.find(componentIds, id) then - skip = true - break - end - end - - if skip then - last, change = next(hook, last) - ids = change.ids - continue - end - - matched = true - end - - local queryOutput = table.create(idsCount) - local row = change.offset - local archetype = change.archetype - local columns = archetype.columns - local archetypeRecords = archetype.records - for index, id in componentIds do - queryOutput[index] = columns[archetypeRecords[id]][row] - end - - return archetype.entities[row], unpack(queryOutput, 1, idsCount) - end - end, +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 } -end +)) -return table.freeze({ - World = World, - ON_ADD = ON_ADD, - ON_REMOVE = ON_REMOVE, - ON_SET = ON_SET, -}) +export type Observer = { + callback: (archetype: Archetype) -> (), + query: QueryInner, +} + +export type Observable = { + [Id]: { + [Id]: { + { Observer } + } + } +} + +export type World = { + archetype_index: { [string]: Archetype }, + archetypes: Archetypes, + component_index: ComponentIndex, + entity_index: EntityIndex, + ROOT_ARCHETYPE: Archetype, + + max_component_id: number, + max_archetype_id: number, + + observable: any, + + --- Enforce a check on entities to be created within desired range + range: (self: World, range_begin: number, range_end: number?) -> (), + + --- Creates a new entity + entity: (self: World, id: Entity?) -> Entity, + --- Creates a new entity located in the first 256 ids. + --- These should be used for static components for fast access. + component: (self: World) -> Entity, + --- Gets the target of an relationship. For example, when a user calls + --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. + target: (self: World, id: Entity, relation: Id, index: number?) -> Entity?, + --- Deletes an entity and all it's related components and relationships. + delete: (self: World, id: Entity) -> (), + + --- Adds a component to the entity with no value + add: (self: World, id: Entity, component: Id) -> (), + --- Assigns a value to a component on the given entity + set: (self: World, id: Entity, component: Id, data: a) -> (), + + cleanup: (self: World) -> (), + -- Clears an entity from the world + clear: (self: World, id: Id) -> (), + --- Removes a component from the given entity + remove: (self: World, id: Entity, component: Id) -> (), + --- Retrieves the value of up to 4 components. These values may be nil. + get: & ((World, Entity, Id) -> a?) + & ((World, Entity, Id, Id) -> (a?, b?)) + & ((World, Entity, Id, Id, Id) -> (a?, b?, c?)) + & ((World, Entity, Id, Id, Id, Id) -> (a?, b?, c?, d?)), + + --- Returns whether the entity has the ID. + has: ((World, Entity, Id) -> boolean) + & ((World, Entity, Id, Id) -> boolean) + & ((World, Entity, Id, Id, Id) -> boolean) + & (World, Entity, Id, Id, Id, Id) -> boolean, + + --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. + parent: (self: World, entity: Entity) -> Entity, + + --- Checks if the world contains the given entity + contains: (self: World, entity: Entity) -> boolean, + + --- Checks if the entity exists + exists: (self: World, entity: Entity) -> boolean, + + each: (self: World, id: Id) -> () -> Entity, + + children: (self: World, id: Id) -> () -> Entity, + + --- Searches the world for entities that match a given query + query: ((World, Id) -> Query) + & ((World, Id, Id) -> Query) + & ((World, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id, Id, Id, ...Id) -> Query) +} +-- type function ecs_id_t(entity) +-- local ty = entity:components()[2] +-- local __T = ty:readproperty(types.singleton("__T")) +-- if not __T then +-- return ty:readproperty(types.singleton("__jecs_pair_value")) +-- end +-- return __T +-- end + +-- type function ecs_pair_t(first, second) +-- if ecs_id_t(first):is("nil") then +-- return second +-- else +-- return first +-- end +-- end +-- + +return { + World = World :: { new: () -> World }, + world = world_new :: () -> World, + component = (ECS_COMPONENT :: any) :: () -> Entity, + tag = (ECS_TAG :: any) :: () -> Entity, + meta = (ECS_META :: any) :: (id: Entity, id: Id, value: T) -> Entity, + is_tag = (ecs_is_tag :: any) :: (World, Id) -> boolean, + + OnAdd = (EcsOnAdd :: any) :: Entity<(entity: Entity, id: Id, data: T) -> ()>, + OnRemove = (EcsOnRemove :: any) :: Entity<(entity: Entity, id: Id) -> ()>, + OnChange = (EcsOnChange :: any) :: Entity<(entity: Entity, id: Id, data: T) -> ()>, + ChildOf = (EcsChildOf :: any) :: Entity, + Component = (EcsComponent :: any) :: Entity, + Wildcard = (EcsWildcard :: any) :: Entity, + w = (EcsWildcard :: any) :: Entity, + OnDelete = (EcsOnDelete :: any) :: Entity, + OnDeleteTarget = (EcsOnDeleteTarget :: any) :: Entity, + Delete = (EcsDelete :: any) :: Entity, + Remove = (EcsRemove :: any) :: Entity, + Name = (EcsName :: any) :: Entity, + Rest = (EcsRest :: any) :: Entity, + + pair = (ECS_PAIR :: any) :: (first: Id

, second: Id) -> Pair, + + -- Inwards facing API for testing + ECS_ID = ECS_ENTITY_T_LO, + ECS_GENERATION_INC = ECS_GENERATION_INC, + ECS_GENERATION = ECS_GENERATION, + ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD, + ECS_ID_DELETE = ECS_ID_DELETE, + ECS_META_RESET = ECS_META_RESET, + + IS_PAIR = (ECS_IS_PAIR :: any) :: (pair: Pair) -> boolean, + ECS_PAIR_FIRST = ECS_PAIR_FIRST :: (pair: Pair) -> Id

, + ECS_PAIR_SECOND = ECS_PAIR_SECOND :: (pair: Pair) -> Id, + pair_first = (ecs_pair_first :: any) :: (world: World, pair: Pair) -> Id

, + pair_second = (ecs_pair_second :: any) :: (world: World, pair: Pair) -> Id, + entity_index_get_alive = entity_index_get_alive, + + archetype_append_to_records = archetype_append_to_records, + id_record_ensure = id_record_ensure, + archetype_create = archetype_create, + archetype_ensure = archetype_ensure, + find_insert = find_insert, + find_archetype_with = find_archetype_with, + find_archetype_without = find_archetype_without, + create_edge_for_remove = create_edge_for_remove, + archetype_traverse_add = archetype_traverse_add, + archetype_traverse_remove = archetype_traverse_remove, + + entity_move = entity_move, + + entity_index_try_get = entity_index_try_get, + entity_index_try_get_any = entity_index_try_get_any, + entity_index_try_get_fast = entity_index_try_get_fast, + entity_index_is_alive = entity_index_is_alive, + entity_index_new_id = entity_index_new_id, + + query_iter = query_iter, + query_iter_init = query_iter_init, + query_with = query_with, + query_without = query_without, + query_archetypes = query_archetypes, + query_match = query_match, + + find_observers = find_observers, +} diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 diff --git a/package.json b/package.json old mode 100644 new mode 100755 diff --git a/rokit.toml b/rokit.toml old mode 100644 new mode 100755 diff --git a/test/addons/observers.luau b/test/addons/observers.luau old mode 100644 new mode 100755 diff --git a/test/lol.luau b/test/lol.luau old mode 100644 new mode 100755 diff --git a/test/stress.client.luau b/test/stress.client.luau old mode 100644 new mode 100755 diff --git a/test/stress.project.json b/test/stress.project.json old mode 100644 new mode 100755 diff --git a/test/tests.luau b/test/tests.luau old mode 100644 new mode 100755 index eefe422..2f30264 --- a/test/tests.luau +++ b/test/tests.luau @@ -64,7 +64,7 @@ TEST("world:add()", function() end do CASE("archetype move") - local world = jecs.World.new() + local world = jecs.world() local d = dwi(world) @@ -609,7 +609,7 @@ TEST("world:delete()", function() end do CASE "fast delete" - local world = jecs.World.new() + local world = jecs.world() local entities = {} local Health = world:component() @@ -635,7 +635,7 @@ TEST("world:delete()", function() end do CASE "cycle" - local world = jecs.World.new() + local world = jecs.world() local Likes = world:component() world:add(Likes, pair(jecs.OnDeleteTarget, jecs.Delete)) local bob = world:entity() @@ -800,7 +800,7 @@ end) TEST("world:has()", function() do CASE "should find Tag on entity" - local world = jecs.World.new() + local world = jecs.world() local Tag = world:entity() @@ -811,7 +811,7 @@ TEST("world:has()", function() end do CASE "should return false when missing one tag" - local world = jecs.World.new() + local world = jecs.world() local A = world:entity() local B = world:entity() @@ -866,7 +866,7 @@ TEST("world:query()", function() CHECK(#q:archetypes() == 0) end do CASE "multiple iter" - local world = jecs.World.new() + local world = jecs.world() local A = world:component() :: jecs.Entity local B = world:component() :: jecs.Entity local e = world:entity() @@ -883,7 +883,7 @@ TEST("world:query()", function() CHECK(counter == 2) end do CASE "tag" - local world = jecs.World.new() + local world = jecs.world() local A = world:entity() local e = world:entity() CHECK_EXPECT_ERR(function() @@ -897,7 +897,7 @@ TEST("world:query()", function() CHECK(count == 1) end do CASE "pairs" - local world = jecs.World.new() + local world = jecs.world() local C1 = world:component() :: jecs.Id local C2 = world:component() :: jecs.Id @@ -969,7 +969,7 @@ TEST("world:query()", function() do CASE "query single component" do - local world = jecs.World.new() + local world = jecs.world() local A = world:component() local B = world:component() @@ -1125,7 +1125,7 @@ TEST("world:query()", function() end do CASE "should query all entities without B" - local world = jecs.World.new() + local world = jecs.world() local A = world:component() local B = world:component() @@ -1149,7 +1149,7 @@ TEST("world:query()", function() end do CASE "should allow querying for relations" - local world = jecs.World.new() + local world = jecs.world() local Eats = world:component() local Apples = world:component() local bob = world:entity() @@ -1162,7 +1162,7 @@ TEST("world:query()", function() end do CASE "should allow wildcards in queries" - local world = jecs.World.new() + local world = jecs.world() local Eats = world:component() local Apples = world:entity() local bob = world:entity() @@ -1181,7 +1181,7 @@ TEST("world:query()", function() end do CASE "should match against multiple pairs" - local world = jecs.World.new() + local world = jecs.world() local Eats = world:component() local Apples = world:entity() local Oranges = world:entity() @@ -1213,7 +1213,7 @@ TEST("world:query()", function() end do CASE "should only relate alive entities" - local world = jecs.World.new() + local world = jecs.world() local Eats = world:entity() local Apples = world:component() local Oranges = world:component() @@ -1241,7 +1241,7 @@ TEST("world:query()", function() do CASE("should error when setting invalid pair") - local world = jecs.World.new() + local world = jecs.world() local Eats = world:component() local Apples = world:component() local bob = world:entity() @@ -1254,7 +1254,7 @@ TEST("world:query()", function() do CASE("should find target for ChildOf") - local world = jecs.World.new() + local world = jecs.world() local ChildOf = jecs.ChildOf local Name = world:component() @@ -1278,7 +1278,7 @@ TEST("world:query()", function() do CASE("despawning while iterating") - local world = jecs.World.new() + local world = jecs.world() local A = world:component() local B = world:component() @@ -1297,7 +1297,7 @@ TEST("world:query()", function() end do CASE "should not find any entities" - local world = jecs.World.new() + local world = jecs.world() local Hello = world:component() local Bob = world:component() @@ -1316,7 +1316,7 @@ TEST("world:query()", function() do CASE "world:query():without()" -- REGRESSION TEST - local world = jecs.World.new() + local world = jecs.world() local _1, _2, _3 = world:component(), world:component(), world:component() local counter = 0 @@ -1330,7 +1330,7 @@ end) TEST("world:remove()", function() do CASE("should allow remove a component that doesn't exist on entity") - local world = jecs.World.new() + local world = jecs.world() local Health = world:component() local Poison = world:component() @@ -1444,10 +1444,15 @@ TEST("world:target", function() CHECK(jecs.pair_first(world, pair(B, C)) == B) local r = (jecs.entity_index_try_get(world.entity_index :: any, e :: any) :: any) :: jecs.Record local archetype = r.archetype - local records = archetype.records - local counts = archetype.counts - CHECK(counts[pair(A, __)] == 4) - CHECK(records[pair(B, C)] > records[pair(A, E)]) + local function cdr(id) + return assert(jecs.component_record(world, id)) + end + local idr_b_c = cdr(pair(B, C)) + local idr_a_wc = cdr(pair(A, __)) + local idr_a_e = cdr(pair(A, E)) + + CHECK(idr_a_wc.counts[archetype.id] == 4) + CHECK(idr_b_c.records[archetype.id] > idr_a_e.records[archetype.id]) CHECK(world:target(e, A, 0) == B) CHECK(world:target(e, A, 1) == C) CHECK(world:target(e, A, 2) == D) @@ -1457,10 +1462,10 @@ TEST("world:target", function() CHECK(world:target(e, C, 0) == D) CHECK(world:target(e, C, 1) == nil) - CHECK(archetype.records[pair(A, B):: any] == 1) - CHECK(archetype.records[pair(A, C):: any] == 2) - CHECK(archetype.records[pair(A, D):: any] == 3) - CHECK(archetype.records[pair(A, E):: any] == 4) + CHECK(cdr(pair(A, B)).records[archetype.id] == 1) + CHECK(cdr(pair(A, C)).records[archetype.id] == 2) + CHECK(cdr(pair(A, D)).records[archetype.id] == 3) + CHECK(cdr(pair(A, E)).records[archetype.id] == 4) CHECK(world:target(e, C, 0) == D) CHECK(world:target(e, C, 1) == nil) @@ -1582,9 +1587,13 @@ TEST("#repro", function() local function getTargets(relation) local tgts = {} local pairwildcard = pair(relation, jecs.Wildcard) + local idr = assert(jecs.component_record(world, pairwildcard)) + local counts = idr.counts + local records = idr.records for _, archetype in world:query(pairwildcard):archetypes() do - local tr = archetype.records[pairwildcard] - local count = archetype.counts[pairwildcard] + local archetype_id = archetype.id + local count = counts[archetype_id] + local tr = records[archetype_id] local types = archetype.types for _, entity in archetype.entities do for i = 0, count - 1 do diff --git a/test/tools/entity_visualiser.luau b/test/tools/entity_visualiser.luau old mode 100644 new mode 100755 diff --git a/thesis/drafts/1/listings-rust.sty b/thesis/drafts/1/listings-rust.sty old mode 100644 new mode 100755 diff --git a/thesis/drafts/1/paper.aux b/thesis/drafts/1/paper.aux old mode 100644 new mode 100755 diff --git a/thesis/drafts/1/paper.fdb_latexmk b/thesis/drafts/1/paper.fdb_latexmk old mode 100644 new mode 100755 diff --git a/thesis/drafts/1/paper.fls b/thesis/drafts/1/paper.fls old mode 100644 new mode 100755 diff --git a/thesis/drafts/1/paper.log b/thesis/drafts/1/paper.log old mode 100644 new mode 100755 diff --git a/thesis/drafts/1/paper.pdf b/thesis/drafts/1/paper.pdf old mode 100644 new mode 100755 diff --git a/thesis/drafts/1/paper.synctex.gz b/thesis/drafts/1/paper.synctex.gz old mode 100644 new mode 100755 diff --git a/thesis/drafts/1/paper.tex b/thesis/drafts/1/paper.tex old mode 100644 new mode 100755 diff --git a/thesis/drafts/1/paper.toc b/thesis/drafts/1/paper.toc old mode 100644 new mode 100755 diff --git a/thesis/images/archetype_graph.png b/thesis/images/archetype_graph.png old mode 100644 new mode 100755 diff --git a/thesis/images/chrome_IdcpbCveiD.png b/thesis/images/chrome_IdcpbCveiD.png old mode 100644 new mode 100755 diff --git a/thesis/images/chrome_f5DTavXIka.png b/thesis/images/chrome_f5DTavXIka.png old mode 100644 new mode 100755 diff --git a/thesis/images/chrome_giChmd5W4Z.png b/thesis/images/chrome_giChmd5W4Z.png old mode 100644 new mode 100755 diff --git a/thesis/images/insertion.png b/thesis/images/insertion.png old mode 100644 new mode 100755 diff --git a/thesis/images/queries.png b/thesis/images/queries.png old mode 100644 new mode 100755 diff --git a/thesis/images/random_access.png b/thesis/images/random_access.png old mode 100644 new mode 100755 diff --git a/thesis/images/removed.png b/thesis/images/removed.png old mode 100644 new mode 100755 diff --git a/thesis/images/sparseset.png b/thesis/images/sparseset.png old mode 100644 new mode 100755 diff --git a/tools/entity_visualiser.luau b/tools/entity_visualiser.luau old mode 100644 new mode 100755 diff --git a/tools/lifetime_tracker.luau b/tools/lifetime_tracker.luau old mode 100644 new mode 100755 diff --git a/tools/perfgraph.py b/tools/perfgraph.py old mode 100644 new mode 100755 diff --git a/tools/read_lcov.py b/tools/read_lcov.py old mode 100644 new mode 100755 diff --git a/tools/runtime_lints.luau b/tools/runtime_lints.luau old mode 100644 new mode 100755 diff --git a/tools/svg.py b/tools/svg.py old mode 100644 new mode 100755 diff --git a/tools/testkit.luau b/tools/testkit.luau old mode 100644 new mode 100755 diff --git a/tsconfig.json b/tsconfig.json old mode 100644 new mode 100755 diff --git a/wally.toml b/wally.toml old mode 100644 new mode 100755