@oasys/oecs 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -267
- package/dist/array-CxbPyiHO.cjs +1 -0
- package/dist/array-uFR7Q8fU.js +132 -0
- package/dist/core/ecs/access_check.d.ts +77 -0
- package/dist/core/ecs/access_check.d.ts.map +1 -0
- package/dist/core/ecs/archetype.d.ts +474 -0
- package/dist/core/ecs/archetype.d.ts.map +1 -0
- package/dist/core/ecs/builtin_relations.d.ts +43 -0
- package/dist/core/ecs/builtin_relations.d.ts.map +1 -0
- package/dist/core/ecs/command_log.d.ts +113 -0
- package/dist/core/ecs/command_log.d.ts.map +1 -0
- package/dist/core/ecs/component.d.ts +106 -0
- package/dist/core/ecs/component.d.ts.map +1 -0
- package/dist/core/ecs/compute_backend.d.ts +37 -0
- package/dist/core/ecs/compute_backend.d.ts.map +1 -0
- package/dist/core/ecs/dispatch_trace.d.ts +93 -0
- package/dist/core/ecs/dispatch_trace.d.ts.map +1 -0
- package/dist/core/ecs/ecs.d.ts +533 -0
- package/dist/core/ecs/ecs.d.ts.map +1 -0
- package/dist/core/ecs/ecs_memory.d.ts +179 -0
- package/dist/core/ecs/ecs_memory.d.ts.map +1 -0
- package/dist/core/ecs/entity.d.ts +28 -0
- package/dist/core/ecs/entity.d.ts.map +1 -0
- package/dist/core/ecs/event.d.ts +54 -0
- package/dist/core/ecs/event.d.ts.map +1 -0
- package/dist/core/ecs/frame_trace.d.ts +133 -0
- package/dist/core/ecs/frame_trace.d.ts.map +1 -0
- package/dist/core/ecs/host_commands.d.ts +252 -0
- package/dist/core/ecs/host_commands.d.ts.map +1 -0
- package/dist/core/ecs/index.d.ts +41 -0
- package/dist/core/ecs/index.d.ts.map +1 -0
- package/dist/core/ecs/observer.d.ts +142 -0
- package/dist/core/ecs/observer.d.ts.map +1 -0
- package/dist/core/ecs/query.d.ts +557 -0
- package/dist/core/ecs/query.d.ts.map +1 -0
- package/dist/core/ecs/ref.d.ts +31 -0
- package/dist/core/ecs/ref.d.ts.map +1 -0
- package/dist/core/ecs/relation.d.ts +231 -0
- package/dist/core/ecs/relation.d.ts.map +1 -0
- package/dist/core/ecs/resource.d.ts +33 -0
- package/dist/core/ecs/resource.d.ts.map +1 -0
- package/dist/core/ecs/resume.d.ts +85 -0
- package/dist/core/ecs/resume.d.ts.map +1 -0
- package/dist/core/ecs/run_condition.d.ts +75 -0
- package/dist/core/ecs/run_condition.d.ts.map +1 -0
- package/dist/core/ecs/schedule.d.ts +133 -0
- package/dist/core/ecs/schedule.d.ts.map +1 -0
- package/dist/core/ecs/sparse_store.d.ts +107 -0
- package/dist/core/ecs/sparse_store.d.ts.map +1 -0
- package/dist/core/ecs/store.d.ts +1149 -0
- package/dist/core/ecs/store.d.ts.map +1 -0
- package/dist/core/ecs/store_layout_listener.d.ts +23 -0
- package/dist/core/ecs/store_layout_listener.d.ts.map +1 -0
- package/dist/core/ecs/system.d.ts +134 -0
- package/dist/core/ecs/system.d.ts.map +1 -0
- package/dist/core/ecs/utils/arrays.d.ts +7 -0
- package/dist/core/ecs/utils/arrays.d.ts.map +1 -0
- package/dist/core/ecs/utils/constants.d.ts +12 -0
- package/dist/core/ecs/utils/constants.d.ts.map +1 -0
- package/dist/core/ecs/utils/error.d.ts +51 -0
- package/dist/core/ecs/utils/error.d.ts.map +1 -0
- package/dist/core/reactive/array.d.ts +24 -0
- package/dist/core/reactive/array.d.ts.map +1 -0
- package/dist/core/reactive/index.cjs +1 -0
- package/dist/core/reactive/index.d.ts +10 -0
- package/dist/core/reactive/index.d.ts.map +1 -0
- package/dist/core/reactive/index.js +17 -0
- package/dist/core/reactive/interop.d.ts +19 -0
- package/dist/core/reactive/interop.d.ts.map +1 -0
- package/dist/core/reactive/kernel.d.ts +71 -0
- package/dist/core/reactive/kernel.d.ts.map +1 -0
- package/dist/core/reactive/map.d.ts +16 -0
- package/dist/core/reactive/map.d.ts.map +1 -0
- package/dist/core/reactive/struct.d.ts +10 -0
- package/dist/core/reactive/struct.d.ts.map +1 -0
- package/dist/core/store/__generated__/abi.d.ts +43 -0
- package/dist/core/store/__generated__/abi.d.ts.map +1 -0
- package/dist/core/store/action_ring.d.ts +136 -0
- package/dist/core/store/action_ring.d.ts.map +1 -0
- package/dist/core/store/allocator.d.ts +238 -0
- package/dist/core/store/allocator.d.ts.map +1 -0
- package/dist/core/store/buffer_backed_column.d.ts +69 -0
- package/dist/core/store/buffer_backed_column.d.ts.map +1 -0
- package/dist/core/store/column_store.d.ts +265 -0
- package/dist/core/store/column_store.d.ts.map +1 -0
- package/dist/core/store/command_dispatch.d.ts +52 -0
- package/dist/core/store/command_dispatch.d.ts.map +1 -0
- package/dist/core/store/command_ring.d.ts +107 -0
- package/dist/core/store/command_ring.d.ts.map +1 -0
- package/dist/core/store/descriptor.d.ts +80 -0
- package/dist/core/store/descriptor.d.ts.map +1 -0
- package/dist/core/store/entity_index.d.ts +108 -0
- package/dist/core/store/entity_index.d.ts.map +1 -0
- package/dist/core/store/event_ring.d.ts +95 -0
- package/dist/core/store/event_ring.d.ts.map +1 -0
- package/dist/core/store/extend.d.ts +109 -0
- package/dist/core/store/extend.d.ts.map +1 -0
- package/dist/core/store/grow.d.ts +39 -0
- package/dist/core/store/grow.d.ts.map +1 -0
- package/dist/core/store/header.d.ts +64 -0
- package/dist/core/store/header.d.ts.map +1 -0
- package/dist/core/store/index.d.ts +16 -0
- package/dist/core/store/index.d.ts.map +1 -0
- package/dist/core/store/region_table.d.ts +74 -0
- package/dist/core/store/region_table.d.ts.map +1 -0
- package/dist/core/store/snapshot.d.ts +43 -0
- package/dist/core/store/snapshot.d.ts.map +1 -0
- package/dist/core/store/state_hash.d.ts +38 -0
- package/dist/core/store/state_hash.d.ts.map +1 -0
- package/dist/core/store/store_regions.d.ts +38 -0
- package/dist/core/store/store_regions.d.ts.map +1 -0
- package/dist/extensions/editor/editor.d.ts +149 -0
- package/dist/extensions/editor/editor.d.ts.map +1 -0
- package/dist/extensions/editor/field_handle.d.ts +35 -0
- package/dist/extensions/editor/field_handle.d.ts.map +1 -0
- package/dist/extensions/editor/index.cjs +1 -0
- package/dist/extensions/editor/index.d.ts +21 -0
- package/dist/extensions/editor/index.d.ts.map +1 -0
- package/dist/extensions/editor/index.js +209 -0
- package/dist/extensions/reactive/ecs_sync.d.ts +210 -0
- package/dist/extensions/reactive/ecs_sync.d.ts.map +1 -0
- package/dist/extensions/reactive/index.cjs +1 -0
- package/dist/extensions/reactive/index.d.ts +23 -0
- package/dist/extensions/reactive/index.d.ts.map +1 -0
- package/dist/extensions/reactive/index.js +225 -0
- package/dist/extensions/solid/index.cjs +1 -0
- package/dist/extensions/solid/index.d.ts +6 -0
- package/dist/extensions/solid/index.d.ts.map +1 -0
- package/dist/extensions/solid/index.js +32 -0
- package/dist/extensions/solid/kernel_solid.d.ts +42 -0
- package/dist/extensions/solid/kernel_solid.d.ts.map +1 -0
- package/dist/index.cjs +2 -1
- package/dist/index.d.ts +16 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8565 -1310
- package/dist/interop-CT-REx0W.cjs +1 -0
- package/dist/interop-CcY6ASQc.js +18 -0
- package/dist/kernel-DgyrLFjW.js +227 -0
- package/dist/kernel-yWV3XnAb.cjs +1 -0
- package/dist/log/console_sink.d.ts +4 -0
- package/dist/log/console_sink.d.ts.map +1 -0
- package/dist/log/index.d.ts +3 -0
- package/dist/log/index.d.ts.map +1 -0
- package/dist/log/logger.d.ts +27 -0
- package/dist/log/logger.d.ts.map +1 -0
- package/dist/primitives.cjs +1 -0
- package/dist/primitives.d.ts +18 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +44 -0
- package/dist/shared-BXSZnxx4.cjs +1 -0
- package/dist/shared-C678TAPY.js +99 -0
- package/dist/shared.cjs +1 -0
- package/dist/shared.d.ts +22 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +7 -0
- package/dist/topological_sort-DlRpSrxu.js +391 -0
- package/dist/topological_sort-WAT-VHb-.cjs +1 -0
- package/dist/type_primitives/assertions.d.ts +12 -8
- package/dist/type_primitives/assertions.d.ts.map +1 -1
- package/dist/type_primitives/binary_heap/binary_heap.d.ts +6 -2
- package/dist/type_primitives/binary_heap/binary_heap.d.ts.map +1 -1
- package/dist/type_primitives/bitset/bitset.d.ts +16 -4
- package/dist/type_primitives/bitset/bitset.d.ts.map +1 -1
- package/dist/type_primitives/brand.d.ts +6 -1
- package/dist/type_primitives/brand.d.ts.map +1 -1
- package/dist/type_primitives/error.d.ts +4 -0
- package/dist/type_primitives/error.d.ts.map +1 -1
- package/dist/type_primitives/index.d.ts +3 -0
- package/dist/type_primitives/index.d.ts.map +1 -1
- package/dist/type_primitives/sparse_map/sparse_map.d.ts +7 -3
- package/dist/type_primitives/sparse_map/sparse_map.d.ts.map +1 -1
- package/dist/type_primitives/sparse_set/sparse_set.d.ts +4 -0
- package/dist/type_primitives/sparse_set/sparse_set.d.ts.map +1 -1
- package/dist/type_primitives/topological_sort/topological_sort.d.ts +7 -3
- package/dist/type_primitives/topological_sort/topological_sort.d.ts.map +1 -1
- package/dist/type_primitives/typed_arrays/typed_arrays.d.ts +53 -16
- package/dist/type_primitives/typed_arrays/typed_arrays.d.ts.map +1 -1
- package/dist/utils/arrays.d.ts +1 -1
- package/dist/utils/arrays.d.ts.map +1 -1
- package/dist/utils/error.d.ts +2 -20
- package/dist/utils/error.d.ts.map +1 -1
- package/package.json +36 -9
- package/dist/archetype.d.ts +0 -108
- package/dist/archetype.d.ts.map +0 -1
- package/dist/component.d.ts +0 -45
- package/dist/component.d.ts.map +0 -1
- package/dist/ecs.d.ts +0 -104
- package/dist/ecs.d.ts.map +0 -1
- package/dist/entity.d.ts +0 -11
- package/dist/entity.d.ts.map +0 -1
- package/dist/event.d.ts +0 -30
- package/dist/event.d.ts.map +0 -1
- package/dist/query.d.ts +0 -94
- package/dist/query.d.ts.map +0 -1
- package/dist/ref.d.ts +0 -23
- package/dist/ref.d.ts.map +0 -1
- package/dist/resource.d.ts +0 -23
- package/dist/resource.d.ts.map +0 -1
- package/dist/schedule.d.ts +0 -45
- package/dist/schedule.d.ts.map +0 -1
- package/dist/store.d.ts +0 -118
- package/dist/store.d.ts.map +0 -1
- package/dist/system.d.ts +0 -16
- package/dist/system.d.ts.map +0 -1
|
@@ -0,0 +1,1149 @@
|
|
|
1
|
+
import { EntityID } from './entity';
|
|
2
|
+
import { FrameTraceSink } from './frame_trace';
|
|
3
|
+
import { ComponentDef, ComponentHandle, ComponentSchema, FieldValues } from './component';
|
|
4
|
+
import { SparseComponentDef, SparseComponentID } from './sparse_store';
|
|
5
|
+
import { RelationDef, RelationOptions } from './relation';
|
|
6
|
+
import { EmptyEventSchema, EventDef, EventReader, EventSchema } from './event';
|
|
7
|
+
import { BitSet, TypedArrayTag } from '../../type_primitives';
|
|
8
|
+
import { Archetype, ArchetypeID } from './archetype';
|
|
9
|
+
import { Query } from './query';
|
|
10
|
+
import { InPlaceBufferAllocator, ColumnStoreRegionHandle, StoreRegionSpec, ColumnStore } from '../store';
|
|
11
|
+
import { ECSMemoryCapContext } from './ecs_memory';
|
|
12
|
+
/**
|
|
13
|
+
* Effective `(component, entity)` structural events for one fixed-point round,
|
|
14
|
+
* collected during `_flushAdds` / `_flushRemoves` and handed to the observer
|
|
15
|
+
* dispatch hook. Flat parallel arrays, count-bounded (`*_len`), reused across
|
|
16
|
+
* rounds — never reallocated in the flush. This is a scheduling artifact: it is
|
|
17
|
+
* NOT part of `stateHash` or snapshot. See `observer.ts`.
|
|
18
|
+
*/
|
|
19
|
+
export interface StructuralObserverEvents {
|
|
20
|
+
addComp: number[];
|
|
21
|
+
addEid: number[];
|
|
22
|
+
addLen: number;
|
|
23
|
+
remComp: number[];
|
|
24
|
+
remEid: number[];
|
|
25
|
+
remLen: number;
|
|
26
|
+
/** Effective disable events (#677) — collected during the toggle drain
|
|
27
|
+
* (`_flushToggles`), one per `(component, entity)` of each net-disabled
|
|
28
|
+
* entity's mask. Empty on a structural (add/remove/destroy) round. */
|
|
29
|
+
disComp: number[];
|
|
30
|
+
disEid: number[];
|
|
31
|
+
disLen: number;
|
|
32
|
+
/** Effective enable events (#677), symmetric with the disable arrays. */
|
|
33
|
+
enaComp: number[];
|
|
34
|
+
enaEid: number[];
|
|
35
|
+
enaLen: number;
|
|
36
|
+
}
|
|
37
|
+
/** Extracts the schema out of a `ComponentDef` handle. */
|
|
38
|
+
type SchemaOf<D extends ComponentDef> = D extends ComponentDef<infer S> ? S : ComponentSchema;
|
|
39
|
+
/** One typed template entry: `values` is checked against the def's schema —
|
|
40
|
+
* a misspelled field is a compile error. Fields may be omitted (they default
|
|
41
|
+
* to 0), so the map is `Partial`. */
|
|
42
|
+
export type TemplateEntry<D extends ComponentDef = ComponentDef> = {
|
|
43
|
+
readonly def: D;
|
|
44
|
+
readonly values?: Partial<FieldValues<SchemaOf<D>>>;
|
|
45
|
+
};
|
|
46
|
+
/** The entries tuple for `ECS.template` — each element's `values` is checked
|
|
47
|
+
* against its own `def`'s schema. */
|
|
48
|
+
export type TemplateEntries<Defs extends readonly ComponentDef[]> = readonly [
|
|
49
|
+
...{
|
|
50
|
+
[K in keyof Defs]: TemplateEntry<Defs[K]>;
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
/** Union of every field name owned by a component in `Defs` (distributes
|
|
54
|
+
* over the def list). */
|
|
55
|
+
type TemplateFieldNames<Defs extends readonly ComponentDef[]> = Defs[number] extends ComponentDef<infer S> ? keyof S & string : never;
|
|
56
|
+
/** Flat per-instance override map for `ECS.spawn`: any field of any
|
|
57
|
+
* component in the template, each optional. A misspelled field is a compile
|
|
58
|
+
* error (and still a `__DEV__` throw at runtime for untyped call sites). */
|
|
59
|
+
export type TemplateOverrides<Defs extends readonly ComponentDef[]> = {
|
|
60
|
+
readonly [K in TemplateFieldNames<Defs>]?: number;
|
|
61
|
+
};
|
|
62
|
+
declare const __templateDefs: unique symbol;
|
|
63
|
+
/** A resolved template (#462) — an archetype template produced by
|
|
64
|
+
* `ECS.template(...)`. **Opaque** apart from `defs`: callers hold it and pass
|
|
65
|
+
* it to `ECS.spawn` / `ECS.spawnMany` (and may reference it in a system's
|
|
66
|
+
* `spawns` / `despawns` access declaration — the scheduler expands it to
|
|
67
|
+
* `defs`); the remaining fields are engine-internal and may change. `spawn`
|
|
68
|
+
* lands an entity directly in `archetype_id` with zero archetype transitions,
|
|
69
|
+
* writing `flatValues` (defaults in `_flatColumns` order) in one append
|
|
70
|
+
* pass. See ADR-0010. */
|
|
71
|
+
export interface Template<Defs extends readonly ComponentDef[] = readonly ComponentDef[]> {
|
|
72
|
+
readonly archetypeId: ArchetypeID;
|
|
73
|
+
readonly flatValues: number[];
|
|
74
|
+
readonly overrideIndex: Map<string, number>;
|
|
75
|
+
/** The component set this template spawns into, in entry order. */
|
|
76
|
+
readonly defs: readonly ComponentDef[];
|
|
77
|
+
readonly [__templateDefs]?: Defs;
|
|
78
|
+
}
|
|
79
|
+
export interface StoreOptions {
|
|
80
|
+
initialCapacity?: number;
|
|
81
|
+
/** Pluggable SAB buffer source. When provided, `createColumnStore`,
|
|
82
|
+
* `extendColumnStore`, and `growColumnStore` route through it. Default is
|
|
83
|
+
* `growableSabAllocator()`. Typed `InPlaceBufferAllocator` (#682): a live
|
|
84
|
+
* Store's flush loops hoist entity-index views across grows, so only
|
|
85
|
+
* in-place allocators may back one (ADR-0008) — the constructor also
|
|
86
|
+
* runtime-asserts the marker for untyped JS callers. Consumers normally
|
|
87
|
+
* don't touch this directly; `ECSOptions.memory` resolves to it. */
|
|
88
|
+
bufferAllocator?: InPlaceBufferAllocator;
|
|
89
|
+
/** Sizing intent the world was constructed with (#682), used to phrase
|
|
90
|
+
* allocator-cap and entity-index-overflow errors in the caller's own
|
|
91
|
+
* terms ("3.2× the declared budget") instead of raw bytes. Wired by
|
|
92
|
+
* `ECS` from `resolveECSMemory`; absent for bare test Stores. */
|
|
93
|
+
capContext?: ECSMemoryCapContext;
|
|
94
|
+
/** Fired after every SAB resize (extend or grow). The new SAB has
|
|
95
|
+
* already been built and archetypes have already refreshed their
|
|
96
|
+
* views by the time this fires. Used by ECS to call
|
|
97
|
+
* `sim.setLayout(0)` so WASM-side cached pointers re-walk. */
|
|
98
|
+
onBufferResized?: () => void;
|
|
99
|
+
/** Max live entities the SAB entity-index region holds (#245 / PR 4B).
|
|
100
|
+
* Default `ENTITY_INDEX_DEFAULT_CAPACITY` (`1 << 20` — the full EntityID
|
|
101
|
+
* index space). Exceeding this at runtime throws `EID_MAX_INDEX_OVERFLOW`.
|
|
102
|
+
* Tests with small entity counts may set lower to bench the SAB region size
|
|
103
|
+
* or to make index exhaustion reachable; a 1000-entity workload fits
|
|
104
|
+
* comfortably in the default. */
|
|
105
|
+
entityIndexCapacity?: number;
|
|
106
|
+
/** Consumer-declared SAB regions (#623), forwarded verbatim to
|
|
107
|
+
* `createColumnStore`. Each `StoreRegionSpec` carries an opaque `region_id`,
|
|
108
|
+
* a precomputed byte size, and an `init` closure; the engine lays them out
|
|
109
|
+
* generically and exposes them via `regionHandle(id)` / `regionOffset(id)`.
|
|
110
|
+
* A game (e.g. `@internal/sim`'s region specs) supplies these — the engine
|
|
111
|
+
* ships no game regions of its own. Omitted ⇒ none. */
|
|
112
|
+
regions?: readonly StoreRegionSpec[];
|
|
113
|
+
/** Byte size of the opt-in sim-bindings region (#625), forwarded verbatim to
|
|
114
|
+
* `createColumnStore`. A consumer that attaches a WASM backend passes its own
|
|
115
|
+
* size (`@internal/sim`'s `SIM_BINDINGS_BYTES`, computed from the binding
|
|
116
|
+
* manifest); the host then writes the `(component_id, field_id)` IDs into the
|
|
117
|
+
* region. Omitted / 0 ⇒ no region (a pure-TS game pays nothing for the WASM
|
|
118
|
+
* seam). De-welded from the engine ABI in #625 so a manifest edit doesn't
|
|
119
|
+
* drift an engine golden. */
|
|
120
|
+
bindingsRegionBytes?: number;
|
|
121
|
+
/** Opt into the **determinism surface** (#626 / ADR-0020). Default `false`.
|
|
122
|
+
* Gates the three methods that fold/serialize state in canonical (sorted)
|
|
123
|
+
* order: `stateHash`, `snapshotSparse`, `restoreSparse`. When `false`
|
|
124
|
+
* those throw `DETERMINISM_DISABLED` — the canonical-ordering tax (sparse
|
|
125
|
+
* `canonicalIndices` sort + relation target-set sort) is never paid, and a
|
|
126
|
+
* consumer can't accidentally read a non-canonical digest. When `true`,
|
|
127
|
+
* today's behavior is reproduced bit-for-bit. This is the ONLY effect of the
|
|
128
|
+
* flag: it does not touch the per-tick path, the in-place-allocator invariant
|
|
129
|
+
* (ADR-0008, a memory-safety requirement that holds regardless), or the
|
|
130
|
+
* always-on `enabled_count` partition maintenance. The flag's value is a
|
|
131
|
+
* capability gate, not a hot-path switch — `stateHash`/snapshot are never
|
|
132
|
+
* called per tick. */
|
|
133
|
+
deterministic?: boolean;
|
|
134
|
+
}
|
|
135
|
+
export declare class Store {
|
|
136
|
+
private entityGenerations;
|
|
137
|
+
private entityHighWater;
|
|
138
|
+
private readonly entityFreeIndices;
|
|
139
|
+
private entityAliveCount;
|
|
140
|
+
private readonly componentMetas;
|
|
141
|
+
private componentCount;
|
|
142
|
+
private readonly sparseStores;
|
|
143
|
+
private readonly relations;
|
|
144
|
+
private _hasTargetCleanup;
|
|
145
|
+
private readonly eventChannels;
|
|
146
|
+
private readonly dirtyEventChannels;
|
|
147
|
+
private eventCount;
|
|
148
|
+
private readonly archetypes;
|
|
149
|
+
private readonly archetypeMap;
|
|
150
|
+
private nextArchetypeId;
|
|
151
|
+
private readonly componentIndex;
|
|
152
|
+
private readonly registeredQueries;
|
|
153
|
+
private emptyArchetypeId;
|
|
154
|
+
private entityArchetype;
|
|
155
|
+
private entityRow;
|
|
156
|
+
private readonly pendingDestroy;
|
|
157
|
+
private readonly pendingAddIds;
|
|
158
|
+
private readonly pendingAddDefs;
|
|
159
|
+
private readonly pendingAddValues;
|
|
160
|
+
private readonly pendingRemoveIds;
|
|
161
|
+
private readonly pendingRemoveDefs;
|
|
162
|
+
private readonly pendingToggleIds;
|
|
163
|
+
private readonly pendingToggleDisable;
|
|
164
|
+
_tick: number;
|
|
165
|
+
/** Per-world frame-trace sink (ADR-0030), installed via `ECS.setTrace`.
|
|
166
|
+
* `null` unless a consumer attaches a recorder. Every call site is
|
|
167
|
+
* `if (__DEV__) store._trace?.…`, so production builds dead-code-eliminate
|
|
168
|
+
* the seam and pay only this one nullable field. The sink observes; it never
|
|
169
|
+
* folds into `stateHash` (a scheduling artifact, like `_changedTick` / the
|
|
170
|
+
* observer state below). */
|
|
171
|
+
_trace: FrameTraceSink | null;
|
|
172
|
+
private _structuralObserverCount;
|
|
173
|
+
/** Count of components with any onDisable/onEnable observer (#677). While 0
|
|
174
|
+
* (with `_structuralObserverCount` also 0), `flushStructural` takes the
|
|
175
|
+
* byte-for-byte fast path and the toggle drain skips event collection. */
|
|
176
|
+
private _toggleObserverCount;
|
|
177
|
+
/** Reused effective-event scratch for the current flush round. */
|
|
178
|
+
private readonly _obsEvents;
|
|
179
|
+
/** Installed by `ECS` — dispatches a round's collected events to the observer
|
|
180
|
+
* registry (ordering + callbacks), which may enqueue further structural ops. */
|
|
181
|
+
_structuralObserverHook: ((ev: StructuralObserverEvents) => void) | null;
|
|
182
|
+
/** Re-entrancy guard for the observed fixed-point loop. An observer callback
|
|
183
|
+
* must enqueue (add/remove) and let the loop settle it — calling `ctx.flush()`
|
|
184
|
+
* from inside a callback would re-enter `flushStructural` and corrupt the
|
|
185
|
+
* shared event scratch. The guard turns that nested call into a no-op; the
|
|
186
|
+
* outer loop drains whatever the callback queued. */
|
|
187
|
+
private _flushingStructural;
|
|
188
|
+
private _collectDestroyEid;
|
|
189
|
+
private readonly _collectDestroyRemoveBit;
|
|
190
|
+
private _collectToggleEid;
|
|
191
|
+
private readonly _collectDisableBit;
|
|
192
|
+
private readonly _collectEnableBit;
|
|
193
|
+
/** Net-transition snapshot for the toggle drain (#677): entity → its disabled
|
|
194
|
+
* state at the START of the drain. Reused, cleared each drain. Lets
|
|
195
|
+
* `_flushToggles` emit one event per *net* transition (disable→enable→disable
|
|
196
|
+
* within a tick = a single onDisable) instead of one per buffered op — required
|
|
197
|
+
* because the radix canonical-order pass would otherwise reorder duplicate eids
|
|
198
|
+
* and mis-sequence a consumer's delete/republish. */
|
|
199
|
+
private readonly _toggleInitial;
|
|
200
|
+
_anyDirtyTracked: boolean;
|
|
201
|
+
private readonly _dirtyTrackedCids;
|
|
202
|
+
private readonly _dirtyLists;
|
|
203
|
+
private readonly _dirtyMarks;
|
|
204
|
+
/** Set by any path that changes a SAB-backed archetype's live row count
|
|
205
|
+
* (`flushStructural`/`flushDestroyed` when they did work; immediate
|
|
206
|
+
* `destroyEntity`, `addComponent(s)`, `removeComponent(s)` on the
|
|
207
|
+
* Store). Cleared by `publishRowCountsToDescriptor`. Lets read-only
|
|
208
|
+
* phases' `ctx.flush()` skip the descriptor walk entirely (#324). */
|
|
209
|
+
private _rowCountsDirty;
|
|
210
|
+
/** Monotonic counter bumped by every membership-changing path (immediate
|
|
211
|
+
* `addComponent(s)`, `removeComponent(s)`, `destroyEntity`,
|
|
212
|
+
* `batchAddComponent`, `batchRemoveComponent`, `flushStructural`,
|
|
213
|
+
* `flushDestroyed`, and new-archetype installs in `archInstall`).
|
|
214
|
+
* Read by `Query._nonEmpty()` via `QueryResolver._getQueryDirtyEpoch`
|
|
215
|
+
* — a query whose stored `_lastSeenEpoch` matches the current epoch
|
|
216
|
+
* reuses its cached non-empty list (#327). Replaces the previous walk
|
|
217
|
+
* over `registeredQueries` that wrote one dirty bit per query per
|
|
218
|
+
* mutation; 5000 startup adds × Q queries used to be 5000×Q writes,
|
|
219
|
+
* now it's 5000 integer increments. Public so ECS can forward through
|
|
220
|
+
* its `QueryResolver` impl; not part of the user-facing API. */
|
|
221
|
+
_queryDirtyEpoch: number;
|
|
222
|
+
private readonly initialCapacity;
|
|
223
|
+
private readonly _scratchTargetMask;
|
|
224
|
+
private readonly _hierarchyRadixOut;
|
|
225
|
+
private readonly _hierarchyRadixC0;
|
|
226
|
+
private readonly _hierarchyRadixC1;
|
|
227
|
+
private _columnStore;
|
|
228
|
+
/** Single-slot Uint32Array view over the entity-index region's `length`
|
|
229
|
+
* header field. `createEntity`'s hot path writes `entityHighWater`
|
|
230
|
+
* here on every high-water bump; using a pre-built typed-array view
|
|
231
|
+
* skips the DataView lookup + setUint32 dispatch that #368 traced to
|
|
232
|
+
* ~25 ns/op on `frame_loop_focused: createEntity` (closes the
|
|
233
|
+
* post-#361 gap to oecs entirely). Rebuilt on SAB resize. */
|
|
234
|
+
private _entityIndexLengthView;
|
|
235
|
+
/** Installed on every SAB-backed Archetype so the Archetype can
|
|
236
|
+
* request a SAB grow when an insertion would exceed its column
|
|
237
|
+
* capacity. Doubles the offending archetype's row capacity (or jumps
|
|
238
|
+
* to whatever fits `arch.length + additional`, whichever is larger),
|
|
239
|
+
* reallocs the SAB via `growColumnStore` (live rows of every archetype
|
|
240
|
+
* are carried forward), and republishes column views to every
|
|
241
|
+
* SAB-backed archetype. Plan §8.1 / §8.3 / §8.4. (#171 §6.1.9 Phase 3) */
|
|
242
|
+
private readonly _growHandler;
|
|
243
|
+
/** Build the intent-aware fatal for an allocator cap hit (#682). The
|
|
244
|
+
* allocator can only name raw bytes; the Store knows what the caller
|
|
245
|
+
* declared (`capContext`) and how many entities are live, so the error
|
|
246
|
+
* says "3.2× the declared budget — runaway creation upstream?" instead
|
|
247
|
+
* of leaving the caller to reverse-engineer byte counts. Fatality is
|
|
248
|
+
* unchanged (#380: no grow-beyond-cap fallback). */
|
|
249
|
+
private _capExceededError;
|
|
250
|
+
private readonly _bufferAllocator;
|
|
251
|
+
private readonly _capContext;
|
|
252
|
+
private readonly _onBufferResized;
|
|
253
|
+
/** Construct with an `initialCapacity` number (legacy form) or an
|
|
254
|
+
* options object (new in PR 3D — adds `bufferAllocator` and
|
|
255
|
+
* `onBufferResized` callback). Both signatures coexist so test fixtures
|
|
256
|
+
* that pass `new Store(4)` keep working. */
|
|
257
|
+
constructor(arg?: number | StoreOptions);
|
|
258
|
+
/** Capacity of the entity-index SAB region (max slots ≈ max live
|
|
259
|
+
* entities). Fixed at construction in #245's PR 4B scope; a future
|
|
260
|
+
* follow-up will grow it via `growColumnStore` when `entityHighWater`
|
|
261
|
+
* hits the cap. */
|
|
262
|
+
private readonly _entityIndexCapacity;
|
|
263
|
+
/** Consumer-declared SAB regions (#623), captured so the realloc path
|
|
264
|
+
* re-lays them out. `undefined` when no consumer regions were declared.
|
|
265
|
+
* The region contents survive a grow via the self-describing region table
|
|
266
|
+
* (`extend.ts` snapshot/restore), so this is only the layout recipe. */
|
|
267
|
+
private readonly _regions;
|
|
268
|
+
/** Byte size of the opt-in sim-bindings region (#625). 0 ⇒ no region (the
|
|
269
|
+
* pure-TS default). Captured so the initial `createColumnStore` reserves it;
|
|
270
|
+
* across a realloc the size is re-derived from the old header by
|
|
271
|
+
* `optionsFromOld`, so it is not threaded through the grow/extend path. */
|
|
272
|
+
private readonly _bindingsRegionBytes;
|
|
273
|
+
/** Determinism opt-in (#626 / ADR-0020). When `false` (the default), the
|
|
274
|
+
* canonical-ordering determinism surface (`stateHash` / `snapshotSparse` /
|
|
275
|
+
* `restoreSparse`) throws `DETERMINISM_DISABLED` rather than running its
|
|
276
|
+
* sort. Memory-safety invariants (the in-place allocator, ADR-0008) and the
|
|
277
|
+
* `enabled_count` partition are unaffected — they hold regardless. */
|
|
278
|
+
private readonly _deterministic;
|
|
279
|
+
/** Whether the determinism surface is enabled (#626). `false` ⇒ `stateHash`
|
|
280
|
+
* / `snapshotSparse` / `restoreSparse` throw `DETERMINISM_DISABLED`. */
|
|
281
|
+
get deterministic(): boolean;
|
|
282
|
+
/** Guard the canonical-ordering determinism surface. Throws
|
|
283
|
+
* `DETERMINISM_DISABLED` when determinism wasn't opted into, naming the
|
|
284
|
+
* method so the caller knows to pass `{ deterministic: true }`. Always on
|
|
285
|
+
* (not `__DEV__`-gated): the surface is cold (never per-tick) so one boolean
|
|
286
|
+
* check is free, and a silent non-canonical digest is the failure mode we're
|
|
287
|
+
* preventing. */
|
|
288
|
+
private _requireDeterministic;
|
|
289
|
+
/** Reject `f32`/`f64` fields on a `deterministic: true` world at registration
|
|
290
|
+
* (#777). IEEE-754 rounds differently across V8 / Bun / Zig at the 1-ULP
|
|
291
|
+
* level, so a float column in a fixed-update path is a silent per-tick
|
|
292
|
+
* `stateHash` divergence between client and server — the one thing the
|
|
293
|
+
* determinism opt-in (ADR-0020) exists to prevent. Non-deterministic worlds
|
|
294
|
+
* skip this entirely (floats stay allowed), so it costs the default path
|
|
295
|
+
* nothing. `kind` names the storage class in the error ("component" /
|
|
296
|
+
* "sparse component"); the array shorthand's `f64` default lands here too, so
|
|
297
|
+
* a deterministic world must pass an explicit integer type. */
|
|
298
|
+
private _rejectNonDeterministicFields;
|
|
299
|
+
/** Rebuild the Int32Array views over the SAB entity-index region
|
|
300
|
+
* after a host-side SAB realloc (extend / grow). Called from
|
|
301
|
+
* `_handleBufferResized` BEFORE the user-supplied `onBufferResized`
|
|
302
|
+
* callback fires so any downstream reader sees coherent views. */
|
|
303
|
+
private _refreshEntityIndexViews;
|
|
304
|
+
/** Centralised "SAB was just reallocated" handler. Refreshes the
|
|
305
|
+
* Int32Array views FIRST (so user callbacks observe valid views),
|
|
306
|
+
* then mirrors `entityHighWater` into the region's length header,
|
|
307
|
+
* then fires the user-supplied callback. */
|
|
308
|
+
private _handleBufferResized;
|
|
309
|
+
/** SAB backing every archetype's column views. Read-only handle; the
|
|
310
|
+
* live mutation happens through `archGetOrCreateFromMask`.
|
|
311
|
+
* Exposed for tests, snapshot/restore, and the upcoming
|
|
312
|
+
* `columnStoreStateHash` wire-up. Production reads of column data should
|
|
313
|
+
* still go through `Archetype.getColumnRead` (which sources from this
|
|
314
|
+
* SAB under the hood). */
|
|
315
|
+
get columnStore(): ColumnStore;
|
|
316
|
+
/** Resolve a consumer-declared SAB region's byte offset by `region_id`, or
|
|
317
|
+
* 0 when the region is absent (no region was declared with that id). The
|
|
318
|
+
* generic, de-gamed replacement (#623) for the removed game-named accessors
|
|
319
|
+
* (`terrain_view` / `spatial_grid_view` / … ); a consumer pairs this with
|
|
320
|
+
* its own region module (e.g. `@internal/sim`'s region helpers) to
|
|
321
|
+
* materialise a typed view. TS twin of Zig `abi.find_region`. */
|
|
322
|
+
regionOffset(regionId: number): number;
|
|
323
|
+
/** A handle to a consumer-declared SAB region resolved by `region_id`, or
|
|
324
|
+
* `null` when absent. Carries the live `buffer`/`view` plus the region's byte
|
|
325
|
+
* `offset` and `bytes`, so a consumer's region module can build a TypedArray
|
|
326
|
+
* view over exactly the region's span without re-reading the directory.
|
|
327
|
+
* Re-fetch after a SAB grow (the offset/view may have moved). (#623) */
|
|
328
|
+
regionHandle(regionId: number): ColumnStoreRegionHandle | null;
|
|
329
|
+
/**
|
|
330
|
+
* Stamp every SAB-backed archetype's live `length` into its descriptor's
|
|
331
|
+
* `row_count` field (#252 / Phase 4 PR 4E). `extendColumnStore` /
|
|
332
|
+
* `growColumnStore` are the only other writers of `row_count`, and they
|
|
333
|
+
* record the count at the moment of the resize — `Archetype.addEntity`
|
|
334
|
+
* does not update it, so any insertion after the most recent resize
|
|
335
|
+
* leaves the descriptor stale. Zig systems that drive their per-row loop
|
|
336
|
+
* off `arch_hdr.row_count` (every `tick_*` export added since PR 3B)
|
|
337
|
+
* read those stale bytes and silently skip the just-spawned rows.
|
|
338
|
+
*
|
|
339
|
+
* Lockstep walk: SAB descriptors are written by `extendColumnStore` in
|
|
340
|
+
* the order non-SAB archetypes are promoted, which is the same id-order
|
|
341
|
+
* those archetypes occupy in `this.archetypes`. Iterating that array
|
|
342
|
+
* once, skipping non-SAB entries, and advancing an `archAddr` cursor
|
|
343
|
+
* by the descriptor's `column_count` lets us write `row_count` without
|
|
344
|
+
* the throwaway `Map<archId, length>` the previous version allocated
|
|
345
|
+
* on every call (#323). Cheap: descriptor-region seeks only, no column
|
|
346
|
+
* I/O.
|
|
347
|
+
*
|
|
348
|
+
* Gated by `_rowCountsDirty` (#324) — mutation paths
|
|
349
|
+
* (`flushStructural`, `flushDestroyed`, immediate `destroyEntity`,
|
|
350
|
+
* `addComponent(s)`, `removeComponent(s)`) set the flag; this method
|
|
351
|
+
* clears it. Read-only phases that flush only to drain empty buffers
|
|
352
|
+
* pay nothing. */
|
|
353
|
+
publishRowCountsToDescriptor(): void;
|
|
354
|
+
/** FNV-1a-style 32-bit digest over (archetype_id, live_row_count, live
|
|
355
|
+
* column bytes) for each archetype in id order, followed by the sparse
|
|
356
|
+
* stores (out-of-identity components, ADR-0011) in registration order.
|
|
357
|
+
* Replaces the per-networked-component fold that compute_state_hash used
|
|
358
|
+
* pre-#171 §6.1.9 Phase 5 — this is the canonical "live ECS state digest"
|
|
359
|
+
* for cross-replay determinism.
|
|
360
|
+
*
|
|
361
|
+
* **Sparse coverage (#470).** Sparse data lives outside the archetype
|
|
362
|
+
* graph, so it is folded separately after the archetype loop — per store:
|
|
363
|
+
* the sparse-component id, the member count, then each member's source
|
|
364
|
+
* entity index + f64 field words, walked in CANONICAL ascending-index order
|
|
365
|
+
* (`SparseComponentStore.canonicalIndices`). Canonical order is what makes
|
|
366
|
+
* the digest insertion-order-independent: two worlds with identical sparse
|
|
367
|
+
* contents built by different add/remove sequences agree. Keyed by entity
|
|
368
|
+
* index, and destruction purges the slot, so a recycled index never carries
|
|
369
|
+
* a stale occupant's data into the hash.
|
|
370
|
+
*
|
|
371
|
+
* It is strictly broader than the prior per-networked-component fold
|
|
372
|
+
* (covers every column, not just a hand-picked subset of networked
|
|
373
|
+
* components), and strictly tighter than `columnStoreStateHash(...)`
|
|
374
|
+
* which scans the full SAB including trailing unused capacity.
|
|
375
|
+
*
|
|
376
|
+
* **Per-word fold (#326).** The inner column loop folds one 32-bit
|
|
377
|
+
* word at a time using FNV-1a's `xor + imul(PRIME)` step. This is NOT
|
|
378
|
+
* byte-for-byte FNV-1a-32 of the column bytes — it's a deterministic
|
|
379
|
+
* digest with the same equality semantics, ~4× faster than the per-
|
|
380
|
+
* byte loop it replaces. Trailing 0–3 tail bytes (only possible for
|
|
381
|
+
* u8/u16 columns at odd row counts) are folded together as a single
|
|
382
|
+
* little-endian word so the algorithm stays branch-free in the inner
|
|
383
|
+
* loop. The 4-byte `id` and `len` headers are folded as words for the
|
|
384
|
+
* same reason. Byte order is little-endian to match the platform's
|
|
385
|
+
* native TypedArray layout; the digest is opaque (no consumer compares
|
|
386
|
+
* against a literal value), so endianness is an implementation detail
|
|
387
|
+
* rather than wire contract.
|
|
388
|
+
*
|
|
389
|
+
* Determinism: same store ⇒ same digest within a process, and across
|
|
390
|
+
* processes on the same architecture (which is all `replay_match`
|
|
391
|
+
* needs — both replays run the same algorithm on the same words).
|
|
392
|
+
*
|
|
393
|
+
* **Opt-in (#626 / ADR-0020).** Throws `DETERMINISM_DISABLED` unless the
|
|
394
|
+
* Store was constructed with `{ deterministic: true }`. The canonical
|
|
395
|
+
* ordering this fold relies on (sparse `canonicalIndices`, sorted relation
|
|
396
|
+
* target sets) is the determinism tax the flag gates. */
|
|
397
|
+
stateHash(): number;
|
|
398
|
+
private archGet;
|
|
399
|
+
/** Look up the `EntityID` at `row` in archetype `archetype_id`. Used
|
|
400
|
+
* by a WASM system to resolve an
|
|
401
|
+
* `EntityID` from an event-ring payload — Zig writes
|
|
402
|
+
* `(archId, row, …)` to the event ring,
|
|
403
|
+
* and TS bridges it back through `ctx.emit(...)` via this method. (#250 /
|
|
404
|
+
* Phase 4 PR 4D)
|
|
405
|
+
*
|
|
406
|
+
* Throws `ECSError` if `archetype_id` is out of range or `row` is
|
|
407
|
+
* past the archetype's live row count — these would indicate a
|
|
408
|
+
* ring-payload corruption or a stale row index (extend / grow
|
|
409
|
+
* happened mid-tick), both of which are bugs the parity test would
|
|
410
|
+
* surface. */
|
|
411
|
+
entityIdAtRow(archetypeId: number, row: number): EntityID;
|
|
412
|
+
/**
|
|
413
|
+
* Find or create an archetype for the given component mask.
|
|
414
|
+
* Also updates the componentIndex and pushes into matching registered queries.
|
|
415
|
+
*
|
|
416
|
+
* Hot single-mask path. The bulk batched variant — used by Phase C
|
|
417
|
+
* pre-warming at `world.startup()` — is `archCreateManyFromMasks`;
|
|
418
|
+
* see #213 / `ECS.startup()` for how it gets called and why.
|
|
419
|
+
*/
|
|
420
|
+
private archGetOrCreateFromMask;
|
|
421
|
+
/**
|
|
422
|
+
* Bulk variant of `archGetOrCreateFromMask` — Phase C of issue #213.
|
|
423
|
+
*
|
|
424
|
+
* Given a set of masks, creates Archetypes for the ones not already
|
|
425
|
+
* planted, in a SINGLE `extendColumnStore` call (instead of one per
|
|
426
|
+
* archetype). Single-mask creation is O(N) in archetypes-so-far because
|
|
427
|
+
* the extend has to copy every existing archetype's live rows forward;
|
|
428
|
+
* N such calls compound to O(N²). Batching collapses the per-archetype
|
|
429
|
+
* setup-and-copy down to one pass — the per-startup cost the design doc
|
|
430
|
+
* §5.2 calls out as "O(N²) → O(N)" for in-tree systems whose archetype
|
|
431
|
+
* set is known at registration time via `spawns` + `transitions`.
|
|
432
|
+
*
|
|
433
|
+
* Masks already in the map are skipped. Returns the resolved
|
|
434
|
+
* `ArchetypeID` per input mask in input order — callers that don't need
|
|
435
|
+
* the ids can ignore the return value.
|
|
436
|
+
*/
|
|
437
|
+
archCreateManyFromMasks(masks: readonly BitSet[]): ArchetypeID[];
|
|
438
|
+
/** Hash-bucketed mask → ArchetypeID lookup; null when the mask isn't planted. */
|
|
439
|
+
private archLookup;
|
|
440
|
+
/** Walk the mask's set bits and resolve each non-tag component to its
|
|
441
|
+
* layout. Used by both single and bulk archetype-create paths. */
|
|
442
|
+
private archBuildLayouts;
|
|
443
|
+
/** Snapshot every existing archetype's SAB rows, call `extendColumnStore`
|
|
444
|
+
* once with `newSpecs`, then refresh every pre-existing SAB-backed
|
|
445
|
+
* Archetype's TypedArray views. The single `existing` snapshot is the
|
|
446
|
+
* key win in the bulk variant — single-mask creation rebuilds it per
|
|
447
|
+
* call (i.e. N times for N new archetypes). */
|
|
448
|
+
private archExtendStoreWithNewSpecs;
|
|
449
|
+
/** Materialise the `Archetype` for `id`, register in the archetype map +
|
|
450
|
+
* component index, and fan into any matching registered queries.
|
|
451
|
+
*
|
|
452
|
+
* Clones `mask` before storing — callers may pass scratch BitSets that
|
|
453
|
+
* they intend to reuse (e.g. `addComponents` / `removeComponents`'s
|
|
454
|
+
* `_scratchTargetMask`). The Archetype and the archetypeMap bucket
|
|
455
|
+
* both keep references long-term, so the clone is required for
|
|
456
|
+
* correctness — and is the previously-implicit guarantee made by the
|
|
457
|
+
* mask-allocating callers (`copyWithSet`, `copyWithClear`, etc.). */
|
|
458
|
+
private archInstall;
|
|
459
|
+
/** Resolve "add component_id to archetype_id" → target ArchetypeID. Caches the edge. */
|
|
460
|
+
private archResolveAdd;
|
|
461
|
+
/** Resolve "remove component_id from archetype_id" → target ArchetypeID. Caches the edge. */
|
|
462
|
+
private archResolveRemove;
|
|
463
|
+
/** Cache a bidirectional add/remove edge between two archetypes. */
|
|
464
|
+
private archCacheEdge;
|
|
465
|
+
createEntity(): EntityID;
|
|
466
|
+
/** Slot index of the entity allocated by the most recent `_allocEntity`
|
|
467
|
+
* call. Read immediately after the call (alloc-free out-param). */
|
|
468
|
+
private _spawnIndex;
|
|
469
|
+
/** Allocate an entity slot WITHOUT placing it in the empty archetype, for
|
|
470
|
+
* the template spawn paths. Returns the packed `EntityID`; the slot index is
|
|
471
|
+
* left in `_spawnIndex`. Mirrors `createEntity`'s allocator but skips the
|
|
472
|
+
* empty-archetype membership write (the caller installs the real archetype
|
|
473
|
+
* + row). Kept separate from `createEntity` so that hot path stays inline
|
|
474
|
+
* (#368). This *commits* the slot (bumps counts, stamps the generation so
|
|
475
|
+
* `isAlive` is already true), so the caller MUST have reserved the column
|
|
476
|
+
* capacity for the row first (`Archetype.ensureRowCapacity`) — otherwise a
|
|
477
|
+
* cap throw from the subsequent append leaves the slot phantom-alive (#775). */
|
|
478
|
+
private _allocEntity;
|
|
479
|
+
/** Pre-check that `count` fresh entity slots can be allocated without
|
|
480
|
+
* exhausting the entity-index space, so `spawnMany` commits all-or-nothing
|
|
481
|
+
* (#775). `_allocEntity`'s own per-call high-water guard would otherwise
|
|
482
|
+
* throw `EID_MAX_INDEX_OVERFLOW` partway through the alloc loop, leaving the
|
|
483
|
+
* slots it already committed phantom-alive. Free-list reuse covers the first
|
|
484
|
+
* `entityFreeIndices.length` slots; only the remainder draws down the
|
|
485
|
+
* high-water headroom. */
|
|
486
|
+
private _ensureEntityIndexCapacity;
|
|
487
|
+
/** Resolve a template: compute the target archetype (creating it if absent —
|
|
488
|
+
* fits the prewarm model), pre-flatten default field values into
|
|
489
|
+
* `_flatColumns` order, and build the override index (field name → flat
|
|
490
|
+
* column index; `TEMPLATE_OVERRIDE_AMBIGUOUS` for a name shared by more than
|
|
491
|
+
* one component, which a flat override cannot target). */
|
|
492
|
+
resolveTemplate<Defs extends readonly ComponentDef[]>(entries: TemplateEntries<Defs>): Template<Defs>;
|
|
493
|
+
/** Apply per-instance overrides to the freshly-spawned row. Each key is a
|
|
494
|
+
* field name resolved through the template's override index. */
|
|
495
|
+
private _applyOverrides;
|
|
496
|
+
/** Spawn one entity directly into the template's archetype (zero archetype
|
|
497
|
+
* transitions). Writes the template defaults in a single append pass, then
|
|
498
|
+
* applies any per-instance overrides. */
|
|
499
|
+
spawn(p: Template, overrides?: Record<string, number | undefined>): EntityID;
|
|
500
|
+
/** Bulk-spawn `count` identical entities into the template's archetype. The
|
|
501
|
+
* field writes are O(columns) — one `TypedArray.fill` per column via
|
|
502
|
+
* `addEntitiesWithValues` — not O(count×columns). Returns the new ids in
|
|
503
|
+
* spawn order. */
|
|
504
|
+
spawnMany(p: Template, count: number): EntityID[];
|
|
505
|
+
/** Immediately destroy an entity, removing it from its archetype.
|
|
506
|
+
*
|
|
507
|
+
* With no `delete`/`clear` target-cleanup policy registered (the common
|
|
508
|
+
* case) this tears the one entity down and returns — no allocation. When a
|
|
509
|
+
* policy is in play, a `delete`-target's sources are appended to a local
|
|
510
|
+
* work-list this method then drains in the same iterative pass (#473, #492):
|
|
511
|
+
* the `work.length` re-read drives chains and trees out without recursion, so
|
|
512
|
+
* depth is bounded by entity count, not tree depth. This mirrors the deferred
|
|
513
|
+
* `flushDestroyed` buffer mechanism — both paths are iterative and reach the
|
|
514
|
+
* identical end state; the only difference is the shared `pendingDestroy`
|
|
515
|
+
* buffer there vs. a local work-list here. `isAlive` dedups a source reached
|
|
516
|
+
* twice (diamonds) and terminates cycles, exactly as the generation guard does
|
|
517
|
+
* in the deferred loop. */
|
|
518
|
+
destroyEntity(id: EntityID): void;
|
|
519
|
+
/** Tear a single entity out of its archetype, relation, and sparse stores,
|
|
520
|
+
* then recycle (or retire) its slot. Shared by both immediate-destroy entry
|
|
521
|
+
* points (the fast no-cascade path and the work-list driver in
|
|
522
|
+
* `destroyEntity`). When `cascade` is non-null, a `delete`-policy target's
|
|
523
|
+
* surviving sources are appended to it for the driver to drain (#473, #492);
|
|
524
|
+
* `null` skips that collection for callers that cannot cascade. The caller
|
|
525
|
+
* must have already confirmed `id` is alive. */
|
|
526
|
+
private _destroyOne;
|
|
527
|
+
/**
|
|
528
|
+
* Liveness check, **fail-closed** against forged / retired / out-of-bounds
|
|
529
|
+
* handles (#778). For a general-purpose engine that may receive a handle from
|
|
530
|
+
* serialization, IPC, or any untrusted caller, three malformed inputs must read
|
|
531
|
+
* dead rather than alias a slot:
|
|
532
|
+
* - **Out of range** — an `id` outside the 31-bit packed space (`< 0` or
|
|
533
|
+
* `> MAX_ENTITY_ID`). Without this, the 20-bit index mask below silently
|
|
534
|
+
* folds garbage high bits onto a valid slot. (Same bound the snapshot /
|
|
535
|
+
* postMessage decode applies, #723.)
|
|
536
|
+
* - **Tombstone generation** — a handle carrying `RETIRED_GENERATION`, which
|
|
537
|
+
* the allocator stamps into a retired slot and never issues to a live
|
|
538
|
+
* entity, would otherwise match a retired slot's parked generation and read
|
|
539
|
+
* alive (the ABA tombstone, previously documented as a known gap).
|
|
540
|
+
* Both guards are comparisons predicted not-taken on the live path, so a
|
|
541
|
+
* well-formed handle pays two branches and nothing else (#778 measured).
|
|
542
|
+
*/
|
|
543
|
+
isAlive(id: EntityID): boolean;
|
|
544
|
+
get entityCount(): number;
|
|
545
|
+
/** An archetype's row count moved from `preLen` to its current
|
|
546
|
+
* `arch.length` on a **shrink** (rows removed: the source of a transition, a
|
|
547
|
+
* destroy, a batch-source drain). Always marks SAB row counts dirty (#324 —
|
|
548
|
+
* the descriptor walk just needs "something moved"); bumps the query-dirty
|
|
549
|
+
* epoch (#327) only on a `length` 0/non-zero crossing, the only case where
|
|
550
|
+
* `Query._nonEmptyArchetypes` can change on a shrink (#328). Mutations that
|
|
551
|
+
* move row counts within the same side (6→5) leave the non-empty set unchanged
|
|
552
|
+
* and skip the bump.
|
|
553
|
+
*
|
|
554
|
+
* A shrink does **not** need the `enabledCount` crossing test (#812): the
|
|
555
|
+
* only enabled-count move it can make is 1→0 (the last enabled row leaves an
|
|
556
|
+
* archetype that keeps disabled rows), which leaves the archetype in a default
|
|
557
|
+
* query's non-empty list as a harmless stale *inclusion* — `count`/`forEach`
|
|
558
|
+
* bound on `enabledCount` (now 0) iterate it zero times. Only a **grow** into
|
|
559
|
+
* an all-disabled archetype can stale-*exclude* a live row, so the enabled
|
|
560
|
+
* crossing lives in `_onArchGrow`, off this path.
|
|
561
|
+
*
|
|
562
|
+
* **Inlining-sensitive — keep the body tiny.** This function is called
|
|
563
|
+
* once or twice per immediate-mode `addComponent` / `removeComponent` and the
|
|
564
|
+
* mutation hot path depends on it being inlined at every call site.
|
|
565
|
+
* #351 / PR #353 added an `if (registeredQueries.length === 0) return;`
|
|
566
|
+
* gate to skip the bump for no-query workloads — bench showed it
|
|
567
|
+
* regressed `mutation: churn_loop` 16-18% because the extra statement
|
|
568
|
+
* pushed the function past V8's per-call inlining budget. Reverted in
|
|
569
|
+
* PR #355. Any future change here needs a `bench-vs-commit` run before
|
|
570
|
+
* merging, not just code review. */
|
|
571
|
+
private _onArchLenChange;
|
|
572
|
+
/** An archetype **grew** — rows were appended (the target of a transition, a
|
|
573
|
+
* spawn, a batch-target fill). Like `_onArchLenChange` it marks row counts
|
|
574
|
+
* dirty and bumps the query-dirty epoch on a `length` 0/non-zero crossing
|
|
575
|
+
* (`includeDisabled` membership), but it *also* bumps on an `enabledCount`
|
|
576
|
+
* 0→1 crossing (#812). The non-empty filter is field-split (#577): a default
|
|
577
|
+
* query keeps archetypes with `enabledCount > 0`. An enabled row appended to
|
|
578
|
+
* an archetype that is non-empty but all-disabled (`length > 0,
|
|
579
|
+
* enabledCount == 0`) crosses `enabledCount` 0→1 without touching `length`,
|
|
580
|
+
* so the `preLen` test alone (the valid-while-`enabledCount === length`
|
|
581
|
+
* pre-#577 proxy) misses it and a cached default query keeps a stale
|
|
582
|
+
* `_nonEmpty` list. Only grows can do this, so only grow sites carry the test.
|
|
583
|
+
*
|
|
584
|
+
* **Precondition: ≥1 row was appended** (every caller adds at least one row),
|
|
585
|
+
* so `arch.length > 0` afterward — which is why the crossings simplify and the
|
|
586
|
+
* body stays inlinable (the inlining caveat on `_onArchLenChange` applies
|
|
587
|
+
* here too; verified with `bench-vs-commit mutation`). The general
|
|
588
|
+
* `(pre === 0) !== (post === 0)` boundary test collapses given the post side:
|
|
589
|
+
* - `length`: post > 0 always ⇒ a crossing iff `preLen === 0`.
|
|
590
|
+
* - `enabledCount`: non-decreasing on a grow ⇒ a 0-crossing iff it was 0
|
|
591
|
+
* before and is non-zero now (`preEnabled === 0 && enabledCount !== 0`);
|
|
592
|
+
* a disabled-row append leaves it 0 and correctly skips. The `enabledCount`
|
|
593
|
+
* read is short-circuited away on the hot path (`preLen` or `preEnabled`
|
|
594
|
+
* non-zero), so a no-disabled workload pays only two scalar compares. */
|
|
595
|
+
private _onArchGrow;
|
|
596
|
+
/** Dirty bookkeeping for an enable/disable toggle (#577). `length` is
|
|
597
|
+
* unchanged (no row added/removed) but `enabled_count` moved, so: republish
|
|
598
|
+
* row counts (the descriptor's `enabled_count` changed, so the WASM sim and
|
|
599
|
+
* snapshot see the new partition), and bump the query epoch only when the
|
|
600
|
+
* *enabled* count crossed 0 — the boundary at which an archetype enters/leaves
|
|
601
|
+
* a query's non-empty set (`Query._nonEmpty` filters on `entityCount`, which
|
|
602
|
+
* is now `enabled_count`). */
|
|
603
|
+
private _onArchEnabledChange;
|
|
604
|
+
/** Immediately disable an entity (idempotent). The entity must hold at least
|
|
605
|
+
* one component — a component-less entity occupies no archetype row, so it
|
|
606
|
+
* cannot be partitioned (a `__DEV__` error; prod no-op). */
|
|
607
|
+
disableEntity(id: EntityID): void;
|
|
608
|
+
/** Immediately enable an entity (idempotent). */
|
|
609
|
+
enableEntity(id: EntityID): void;
|
|
610
|
+
/** Whether `id` is currently disabled. A component-less entity is never
|
|
611
|
+
* disabled (it has no row to partition). */
|
|
612
|
+
isDisabled(id: EntityID): boolean;
|
|
613
|
+
/** 0-crossing detection for the per-entity flush paths (`_flushAdds`,
|
|
614
|
+
* `_flushRemoves`) without per-entity Map traffic — the same cost the
|
|
615
|
+
* destroy drain shed in #457. Each touched archetype is stamped with the
|
|
616
|
+
* current flush epoch (`Archetype._flushSeenEpoch`), its pre-length and
|
|
617
|
+
* pre-enabled-count recorded on first sight (`_flushPreLen` /
|
|
618
|
+
* `_flushPreEnabled`), and pushed onto this scratch list;
|
|
619
|
+
* `_settleFlushDirty` walks the list once after the loop. The field
|
|
620
|
+
* accesses per entity replace a `Map.has` + `Map.set` hash probe pair. The
|
|
621
|
+
* epoch is bumped at settle so the next flush re-records. */
|
|
622
|
+
private _flushEpoch;
|
|
623
|
+
private readonly _flushTouched;
|
|
624
|
+
/** Resolve dirty flags for a per-entity batch flush from the captured
|
|
625
|
+
* pre-counts. Marks row counts dirty if any archetype was touched; bumps
|
|
626
|
+
* the query epoch once if any touched archetype crossed the 0 boundary on
|
|
627
|
+
* *either* `length` (includeDisabled membership) or `enabledCount`
|
|
628
|
+
* (default-query membership) — the deferred analog of the immediate
|
|
629
|
+
* `_onArchLenChange` two-field check (#812). A single bump is sufficient
|
|
630
|
+
* (queries only need to know "something changed"). Clears the touched list
|
|
631
|
+
* and advances the flush epoch on exit. */
|
|
632
|
+
private _settleFlushDirty;
|
|
633
|
+
destroyEntityDeferred(id: EntityID): void;
|
|
634
|
+
/** Buffer an enable/disable toggle for the phase flush (#577). The row swap a
|
|
635
|
+
* toggle performs would corrupt a `forEach` over that archetype if applied
|
|
636
|
+
* mid-system, so it is deferred like add/remove. */
|
|
637
|
+
disableEntityDeferred(id: EntityID): void;
|
|
638
|
+
enableEntityDeferred(id: EntityID): void;
|
|
639
|
+
/** Drain buffered enable/disable toggles, applying each in operation order via
|
|
640
|
+
* the immediate path (which is idempotent and updates dirty flags). Called at
|
|
641
|
+
* the flush boundary after structural adds/removes settle, so a toggle sees the
|
|
642
|
+
* entity's final archetype placement for the tick.
|
|
643
|
+
*
|
|
644
|
+
* When an onDisable/onEnable observer is registered (`_toggleObserverCount >
|
|
645
|
+
* 0`, #677) this also collects effective toggle events into `_obsEvents` for
|
|
646
|
+
* the dispatch hook, collapsed to one event per *net* transition across the
|
|
647
|
+
* drain (see `_toggleInitial`). The no-observer path is byte-for-byte the
|
|
648
|
+
* pre-#677 drain. */
|
|
649
|
+
private _flushToggles;
|
|
650
|
+
/** Fan one entity's net toggle transition out to an onDisable / onEnable event
|
|
651
|
+
* per carried component (#677). Walks the entity's archetype mask through the
|
|
652
|
+
* matching pre-bound bit visitor; a component-less entity (no row) carries
|
|
653
|
+
* nothing and is skipped. */
|
|
654
|
+
private _collectToggle;
|
|
655
|
+
get pendingToggleCount(): number;
|
|
656
|
+
/** Flush all buffered entity destructions in batch.
|
|
657
|
+
*
|
|
658
|
+
* When onRemove observers are registered (`_structuralObserverCount > 0`),
|
|
659
|
+
* a destroy fires onRemove for every component the entity carried — a destroy
|
|
660
|
+
* *is* a remove of the whole mask — collected here and dispatched by the
|
|
661
|
+
* `flushStructural` fixed-point loop, the only caller in that mode (it drains
|
|
662
|
+
* `pendingDestroy` each round so the trailing `ctx.flush()` call is a no-op).
|
|
663
|
+
* Same commit-then-observe discipline as `_flushRemoves`: the entity is fully
|
|
664
|
+
* freed before the callback runs, so onRemove receives the (now dead) eid as
|
|
665
|
+
* the identity of what was destroyed, not a live handle to read. The
|
|
666
|
+
* no-observer path is byte-for-byte unchanged (`collecting` gate). #531.
|
|
667
|
+
*
|
|
668
|
+
* Re-entrancy: while the observed fixed point owns the flush
|
|
669
|
+
* (`_flushingStructural`), the loop drains destroys itself via
|
|
670
|
+
* `_drainDestroyed`, so a re-entrant `ctx.flush()` from a callback no-ops
|
|
671
|
+
* here — otherwise it would collect into the shared `_obsEvents` scratch
|
|
672
|
+
* mid-dispatch and corrupt it (mirrors the `flushStructural` guard). */
|
|
673
|
+
flushDestroyed(): void;
|
|
674
|
+
private _drainDestroyed;
|
|
675
|
+
get pendingDestroyCount(): number;
|
|
676
|
+
addComponentDeferred(entityId: EntityID, def: ComponentDef<Record<string, never>>): void;
|
|
677
|
+
addComponentDeferred<S extends ComponentSchema>(entityId: EntityID, def: ComponentDef<S>, values: FieldValues<S>): void;
|
|
678
|
+
removeComponentDeferred(entityId: EntityID, def: ComponentDef): void;
|
|
679
|
+
flushStructural(): void;
|
|
680
|
+
/** Batch-apply all deferred component additions. */
|
|
681
|
+
private _flushAdds;
|
|
682
|
+
/** Batch-apply all deferred component removals. */
|
|
683
|
+
private _flushRemoves;
|
|
684
|
+
get pendingStructuralCount(): number;
|
|
685
|
+
/** Set the per-component observation flags from the registry's aggregate of
|
|
686
|
+
* live observers for `cid`. Maintains `_structuralObserverCount` and
|
|
687
|
+
* `_toggleObserverCount` (#677) (the fast-path gates) and lazily allocates the
|
|
688
|
+
* dirty list when per-entity onSet tracking turns on. */
|
|
689
|
+
_configureComponentObservation(cid: number, hasAdd: boolean, hasRem: boolean, hasDisable: boolean, hasEnable: boolean, trackDirty: boolean): void;
|
|
690
|
+
/** Record a per-entity onSet "changed" event for the entity. Called from the
|
|
691
|
+
* field-write path (`SystemContext.setField` / `markChanged`) and gated by
|
|
692
|
+
* the caller on `_anyDirtyTracked`. Appends to the dirty list only if the
|
|
693
|
+
* dedup bit was clear (the ADR-0012 list + dedup-bit mechanism). */
|
|
694
|
+
_noteSet(def: ComponentHandle, eid: EntityID): void;
|
|
695
|
+
private _growDirtyMarks;
|
|
696
|
+
/** Detach and return the dirty-row list for `cid`, clearing its dedup bits and
|
|
697
|
+
* leaving the store with a fresh empty list (so re-dirties during the drain
|
|
698
|
+
* accumulate for the NEXT tick, not this one). Returns a shared empty array
|
|
699
|
+
* when nothing is dirty. Caller owns the returned array. */
|
|
700
|
+
_takeDirty(cid: number): EntityID[];
|
|
701
|
+
/** Clear any dirty dedup bits for a freed entity index across every tracked
|
|
702
|
+
* component, so a recycled slot at the same index can be marked afresh. Gated
|
|
703
|
+
* by `_anyDirtyTracked` at the destroy call sites. */
|
|
704
|
+
private _clearDirtyForIndex;
|
|
705
|
+
/** Visit every non-empty archetype containing `cid` whose component-column
|
|
706
|
+
* changed at or after `baseline`, in canonical (ascending archetype-id) order
|
|
707
|
+
* — the archetype-granular onSet detection point. Reuses the existing
|
|
708
|
+
* per-archetype change tick (free; no write-path cost). */
|
|
709
|
+
_forEachChangedArchetype(cid: number, baseline: number, cb: (arch: Archetype) => void): void;
|
|
710
|
+
/** Enabled live entities currently carrying `cid`, used by `yieldExisting` to
|
|
711
|
+
* replay onAdd on registration. Bounded by `enabled_count` (#677): a disabled
|
|
712
|
+
* entity is excluded from default queries, so seeding it via onAdd would
|
|
713
|
+
* publish a row that an immediate onDisable should have removed — it is simply
|
|
714
|
+
* absent at seed (the "delete on disable" semantics). Unordered here — the
|
|
715
|
+
* registry radix-sorts. */
|
|
716
|
+
_collectEntitiesWithComponent(cid: number): EntityID[];
|
|
717
|
+
registerComponent<S extends Record<string, TypedArrayTag>>(schema: S): ComponentDef<S>;
|
|
718
|
+
/** Return the field index assigned to `(def, fieldName)` at component
|
|
719
|
+
* registration. Indexes are insertion-order, zero-based, and stable for
|
|
720
|
+
* the lifetime of the ECS. Used by systems that pass `(component_id,
|
|
721
|
+
* field_id)` pairs across the WASM FFI (PR 3C / #231). */
|
|
722
|
+
fieldIdOf(def: ComponentHandle, fieldName: string): number;
|
|
723
|
+
/** Register a sparse component or tag. Unlike `registerComponent`, this
|
|
724
|
+
* allocates from a separate id space and never touches the archetype mask,
|
|
725
|
+
* so it does **not** count against `STORE_DESCRIPTOR_COMPONENT_LIMIT`. See
|
|
726
|
+
* ADR-0011 and `sparse_store.ts`. */
|
|
727
|
+
registerSparseComponent<S extends Record<string, TypedArrayTag>>(schema: S): SparseComponentDef<S>;
|
|
728
|
+
/** Allocate the backing sparse store WITHOUT the #777 float guard, for
|
|
729
|
+
* engine-internal backings whose `f64` holds an EXACT integer rather than a
|
|
730
|
+
* user quantity: the exclusive-relation `{ target }` slot stores an `EntityID`
|
|
731
|
+
* (≤ 2^53, so f64 is bit-exact and cross-host identical — the ban targets float
|
|
732
|
+
* *arithmetic* rounding, which a target slot never undergoes). User schemas go
|
|
733
|
+
* through `registerSparseComponent`, which guards first. */
|
|
734
|
+
private _pushSparseStore;
|
|
735
|
+
private sparseStoreOf;
|
|
736
|
+
/** Add (or overwrite) a sparse component on an entity. No archetype
|
|
737
|
+
* transition, no row copy — the entity's `archetype_id` is unchanged. */
|
|
738
|
+
addSparse(entityId: EntityID, def: SparseComponentDef, values?: Record<string, number>): void;
|
|
739
|
+
/** Remove a sparse component from an entity. No-op if absent. */
|
|
740
|
+
removeSparse(entityId: EntityID, def: SparseComponentDef): void;
|
|
741
|
+
hasSparse(entityId: EntityID, def: SparseComponentDef): boolean;
|
|
742
|
+
getSparseField(entityId: EntityID, def: SparseComponentDef, field: string): number;
|
|
743
|
+
setSparseField(entityId: EntityID, def: SparseComponentDef, field: string, value: number): void;
|
|
744
|
+
/** Drop all sparse data for a destroyed entity index so a recycled slot
|
|
745
|
+
* can't inherit it. Gated by the caller on `sparseStores.length > 0` to
|
|
746
|
+
* keep the destroy hot path free when sparse storage is unused. */
|
|
747
|
+
private _purgeSparse;
|
|
748
|
+
/** Serialize the sparse stores **and** relation side data to a self-contained
|
|
749
|
+
* byte buffer — the sparse half of a world snapshot (the dense half is the
|
|
750
|
+
* SAB snapshot). Two framed sections: the sparse stores (`snapshot_sparse_-
|
|
751
|
+
* stores` — exclusive relation targets + multi membership ride here) followed
|
|
752
|
+
* by the relation side data (`snapshotRelations` — multi forward target
|
|
753
|
+
* sets, which live outside the sparse store). Both are written in canonical
|
|
754
|
+
* entity-index order, so two worlds with identical contents inserted in
|
|
755
|
+
* different orders snapshot byte-for-byte the same (#470). The reverse index
|
|
756
|
+
* is derived and never serialized — `restoreSparse` rebuilds it. Pairs with
|
|
757
|
+
* `restoreSparse`.
|
|
758
|
+
*
|
|
759
|
+
* **Opt-in (#626 / ADR-0020).** Throws `DETERMINISM_DISABLED` unless the
|
|
760
|
+
* Store was constructed with `{ deterministic: true }` — the canonical
|
|
761
|
+
* entity-index ordering is the determinism tax the flag gates. */
|
|
762
|
+
snapshotSparse(): Uint8Array;
|
|
763
|
+
/** Repopulate the sparse stores from `snapshotSparse` bytes, replacing all
|
|
764
|
+
* current sparse data (full-equality round-trip of membership + data), then
|
|
765
|
+
* rebuild every relation's derived side indices: multi forward sets from the
|
|
766
|
+
* relation section, and the reverse index for both cardinalities (exclusive
|
|
767
|
+
* from the just-restored sparse target field, multi from the rebuilt forward
|
|
768
|
+
* sets). The sparse components and relations must already be registered in
|
|
769
|
+
* the same order — restore carries data, not the registration (which is
|
|
770
|
+
* code). Throws `SparseRestoreError` if the snapshot's shape, field identity,
|
|
771
|
+
* entity-index bounds, or frame length don't validate.
|
|
772
|
+
*
|
|
773
|
+
* **Opt-in (#626 / ADR-0020).** Throws `DETERMINISM_DISABLED` unless the
|
|
774
|
+
* Store was constructed with `{ deterministic: true }`; paired with
|
|
775
|
+
* `snapshotSparse`, which produces the canonical bytes restore consumes. */
|
|
776
|
+
restoreSparse(bytes: Uint8Array): void;
|
|
777
|
+
/** Rebuild every relation's derived side indices after the sparse stores have
|
|
778
|
+
* been restored. `restoreRelations` resets all relations, rebuilds the multi
|
|
779
|
+
* forward sets + their reverse edges from `relBytes`, and validates shape.
|
|
780
|
+
* The exclusive reverse index can't be carried in the bytes (it's derivable),
|
|
781
|
+
* so it's rebuilt here from the backing sparse store: every member row holds
|
|
782
|
+
* `(source index → target EntityID)`, which is exactly one reverse edge. */
|
|
783
|
+
private _rebuildRelationIndices;
|
|
784
|
+
/**
|
|
785
|
+
* Capture the full live world to one self-contained byte buffer that
|
|
786
|
+
* `restoreInto` can mount back onto a live, ticking world ("rewind a running
|
|
787
|
+
* world and keep ticking"). Three sections (see `resume.ts`): the dense SAB
|
|
788
|
+
* column bytes (`snapshotColumnStore`), the sparse + relation bytes
|
|
789
|
+
* (`snapshotSparse`), and the host-side bookkeeping the SAB omits — the world
|
|
790
|
+
* tick, the entity recycle free-list (in live order; no byte source, and its
|
|
791
|
+
* order is load-bearing for byte-identical resume), the alive count, and each
|
|
792
|
+
* archetype's `length` / `enabledCount`.
|
|
793
|
+
*
|
|
794
|
+
* **Opt-in (ADR-0020).** Throws `DETERMINISM_DISABLED` unless constructed with
|
|
795
|
+
* `{ deterministic: true }` — the sparse section rides the canonical-ordering
|
|
796
|
+
* surface and byte-identical resume is a determinism property. Pairs with
|
|
797
|
+
* `restoreInto`.
|
|
798
|
+
*
|
|
799
|
+
* **v1 scope.** Resources + events are NOT captured (resume requires
|
|
800
|
+
* resource-free per-tick state; events are tick-cleared). Change-detection /
|
|
801
|
+
* scheduler baselines (`changed()` queries) are likewise not captured — they
|
|
802
|
+
* are scheduling artifacts, never folded into `stateHash`. Take the snapshot
|
|
803
|
+
* at a tick boundary (between `update()`s). See the ADR. */
|
|
804
|
+
snapshot(): Uint8Array;
|
|
805
|
+
/** Gather the host-side state a snapshot carries alongside the dense + sparse
|
|
806
|
+
* bytes — see `snapshot()`. The free-list is copied (it's a live mutable). */
|
|
807
|
+
private _collectHostState;
|
|
808
|
+
/**
|
|
809
|
+
* Mount a `snapshot()` buffer onto this live world and leave it ready to keep
|
|
810
|
+
* ticking. Fails closed on a malformed frame or a registration mismatch
|
|
811
|
+
* BEFORE any live state is touched (the archetype/component graph is rebuilt
|
|
812
|
+
* from code, not the snapshot — same contract as `restoreSparse`). On
|
|
813
|
+
* success the world's dense + sparse state, entity allocator, and tick are
|
|
814
|
+
* exactly the captured world's.
|
|
815
|
+
*
|
|
816
|
+
* Requires a world whose SAB-backed archetype set + column layout match the
|
|
817
|
+
* snapshot's exactly (prewarm so the archetype set is stable) and the same
|
|
818
|
+
* entity-index capacity. **Opt-in (ADR-0020):** throws `DETERMINISM_DISABLED`
|
|
819
|
+
* unless `{ deterministic: true }`. See `snapshot()` for the v1 scope. */
|
|
820
|
+
restoreInto(bytes: Uint8Array): void;
|
|
821
|
+
/** Read-only validation of a `snapshotSparse` SECTION (the framed
|
|
822
|
+
* `[sparseLen][relLen][sparse][rel]` buffer) against the live registry, used
|
|
823
|
+
* by `restoreInto` to fail closed on a sparse-registration mismatch BEFORE the
|
|
824
|
+
* dense mount commits. Mirrors `restoreSparse`'s frame check, then validates the
|
|
825
|
+
* sparse-stores sub-section without mutating. A relation-registration difference
|
|
826
|
+
* surfaces here too: every `registerRelation` adds a backing sparse store, so a
|
|
827
|
+
* differing relation set changes the sparse store count / schema. Throws
|
|
828
|
+
* `SparseRestoreError` on a mismatch. */
|
|
829
|
+
private _assertSparseSectionMatches;
|
|
830
|
+
/** Rebuild each SAB-backed archetype's host-side `length` / `enabledCount` /
|
|
831
|
+
* `_entityIds` after the dense backing was swapped in `restoreInto`. `length`
|
|
832
|
+
* + the per-row entity-id back-reference come from a scan of the restored
|
|
833
|
+
* entity-index region (which entity occupies which row); `enabledCount` comes
|
|
834
|
+
* from the captured host-state (the #577 partition boundary is positional only
|
|
835
|
+
* — it has no per-entity byte source). */
|
|
836
|
+
private _reconstructHostRows;
|
|
837
|
+
/** Register a relation kind. `exclusive` (the default) → one target per
|
|
838
|
+
* source, stored in a backing `{ target: f64 }` sparse component, so the
|
|
839
|
+
* forward index rides the sparse store and inherits query membership (#469),
|
|
840
|
+
* `stateHash` + snapshot/restore (#470) for free. `multi` → a set of
|
|
841
|
+
* targets per source, backed by a sparse tag for membership plus a side
|
|
842
|
+
* forward index the relation owns. The two cardinalities are mutually
|
|
843
|
+
* exclusive. `onDeleteTarget` selects the cleanup policy run when a target
|
|
844
|
+
* is destroyed — `delete` (cascade-destroy sources), `clear` (drop the link,
|
|
845
|
+
* sources survive), or `orphan` (default: leave it dangling, #473). See
|
|
846
|
+
* ADR-0011 and `relation.ts`.
|
|
847
|
+
*
|
|
848
|
+
* The backing sparse store is resolved and handed to the relation so it can
|
|
849
|
+
* drive forward/membership rows directly — the cardinality-specific
|
|
850
|
+
* interaction is `ExclusiveRelationStore` / `MultiRelationStore`'s, not a
|
|
851
|
+
* branch here (#498). */
|
|
852
|
+
registerRelation(opts?: RelationOptions): RelationDef;
|
|
853
|
+
private relationOf;
|
|
854
|
+
/** Number of registered relations. Visible to tests asserting the
|
|
855
|
+
* no-transition invariant alongside `archetype_count`. */
|
|
856
|
+
get relationCount(): number;
|
|
857
|
+
/** Add a `(R, tgt)` pair to `src`. No archetype transition. Exclusive:
|
|
858
|
+
* replaces any existing target (engine-enforced one-per-source), a no-op if
|
|
859
|
+
* `tgt` is already the target. Multi: adds `tgt` to the set, a no-op if
|
|
860
|
+
* already present. A dead `src` *or* `tgt` is caller error: it throws in
|
|
861
|
+
* `__DEV__` and is a no-op in production — symmetric, so a production build
|
|
862
|
+
* never links a reverse-index entry keyed by a destroyed handle (#495). */
|
|
863
|
+
addRelation(src: EntityID, def: RelationDef, tgt: EntityID): void;
|
|
864
|
+
/** Remove a `(R, tgt)` pair from `src`. No archetype transition. Exclusive:
|
|
865
|
+
* `tgt` is optional and the removal is a no-op when it names a target other
|
|
866
|
+
* than the current one. Multi: omitting `tgt` removes *all* of `src`'s
|
|
867
|
+
* targets; passing one removes just that pair (dropping membership when the
|
|
868
|
+
* set empties). */
|
|
869
|
+
removeRelation(src: EntityID, def: RelationDef, tgt?: EntityID): void;
|
|
870
|
+
/** The single target of `src` under an exclusive relation, or `undefined`.
|
|
871
|
+
* Throws in `__DEV__` on a multi-target relation (use `targetsOf`). */
|
|
872
|
+
targetOf(src: EntityID, def: RelationDef): EntityID | undefined;
|
|
873
|
+
/** All targets of `src` under `R` — one or zero for exclusive, the full set
|
|
874
|
+
* for multi — ascending by id. */
|
|
875
|
+
targetsOf(src: EntityID, def: RelationDef): EntityID[];
|
|
876
|
+
/** Sources that point at `tgt` under `R` (the reverse index), ascending by
|
|
877
|
+
* id; empty when none. */
|
|
878
|
+
sourcesOf(def: RelationDef, tgt: EntityID): EntityID[];
|
|
879
|
+
/** Whether `src` holds any pair under `R`. */
|
|
880
|
+
hasRelation(src: EntityID, def: RelationDef): boolean;
|
|
881
|
+
/** All `(source, target)` pairs of relation `R` — the `(R, *)` wildcard
|
|
882
|
+
* (#472). Sources are emitted in **canonical entity-index order** (the #470
|
|
883
|
+
* determinism convention): exclusive relations ride the backing sparse
|
|
884
|
+
* store's `canonicalIndices`, multi relations ride the same
|
|
885
|
+
* `forEachCanonicalTargetSet` traversal `stateHash` / `snapshotRelations`
|
|
886
|
+
* use; a multi source's targets follow ascending by id. Empty when the
|
|
887
|
+
* relation holds no pairs. Cold path — allocates the result array (and, for
|
|
888
|
+
* multi, sorts each source's target set); not for per-tick use. The
|
|
889
|
+
* point-query forms are `targetOf` / `targetsOf`. */
|
|
890
|
+
pairsOf(def: RelationDef): [EntityID, EntityID][];
|
|
891
|
+
/** Every `(relation, source)` pointing at `tgt`, across **all** registered
|
|
892
|
+
* relation kinds — the `(*, T)` wildcard (#472). Walks the relation registry
|
|
893
|
+
* in id order (each relation's reverse index already returns sources
|
|
894
|
+
* ascending by id), so the result is ordered by relation id then source id.
|
|
895
|
+
* Empty when nothing targets `tgt`. The single-relation form is
|
|
896
|
+
* `sourcesOf(def, tgt)`. */
|
|
897
|
+
sourcesOfAny(tgt: EntityID): [RelationDef, EntityID][];
|
|
898
|
+
/** The backing sparse component id of relation `R` — the membership store a
|
|
899
|
+
* `(R, *)` wildcard term (`Query.withRelation`) drives through the shared
|
|
900
|
+
* sparse-match path. Exclusive relations back a `{ target: f64 }` sparse
|
|
901
|
+
* component, multi a tag; both carry per-source membership, so "has any
|
|
902
|
+
* `(R, *)` pair" is exactly membership in this store (including an
|
|
903
|
+
* orphan-dangling source, whose membership row persists — consistent with
|
|
904
|
+
* `pairsOf`). The def is engine-owned and never handed to callers; this hands
|
|
905
|
+
* the query builder only its erased id for the `sparseInclude` list. */
|
|
906
|
+
relationBackingSparseId(def: RelationDef): SparseComponentID;
|
|
907
|
+
/** Drive a `(*, T)` wildcard query (`Query.forEachRelatedTo`): every source
|
|
908
|
+
* related to `target` under **any** relation, intersected with the query's
|
|
909
|
+
* dense mask + sparse require/exclude terms + the default enabled-row filter,
|
|
910
|
+
* each source yielded once. Unions `sourcesOf(R, target)` across every
|
|
911
|
+
* relation into a `Set` (dedup by full `EntityID` — a source related to `T`
|
|
912
|
+
* via two relations is yielded once), then sorts ascending: the cross-relation
|
|
913
|
+
* union has no inherent order, so one cold sort gives a deterministic,
|
|
914
|
+
* canonical `(*, T)` order matching `sourcesOf` / `sourcesOfAny` (which sort
|
|
915
|
+
* the same way). Cold/structural — not a per-tick hot loop over many targets.
|
|
916
|
+
* `sparseInclude`/`sparseExclude` carry both raw sparse terms and the backing
|
|
917
|
+
* stores of any composed `(R, *)` terms, so it intersects with them uniformly. */
|
|
918
|
+
_forEachRelationTargetMatch(target: EntityID, include: BitSet, exclude: BitSet | null, anyOf: BitSet | null, sparseInclude: readonly SparseComponentID[], sparseExclude: readonly SparseComponentID[], includeDisabled: boolean, cb: (entityId: EntityID) => void): void;
|
|
919
|
+
/** Reclaim reverse-index memory: drop every relation's reverse entries whose
|
|
920
|
+
* **target** has been destroyed, returning the total dropped across all
|
|
921
|
+
* relations (#491). Under the default `orphan` policy a destroyed target
|
|
922
|
+
* leaves its reverse entry intact until each source re-targets or dies, so a
|
|
923
|
+
* long-lived source that orphan-points at a churn of short-lived targets and
|
|
924
|
+
* never re-targets accumulates dead-target keys without bound. This cold-path
|
|
925
|
+
* hook drops them on demand — call it at scene or snapshot boundaries.
|
|
926
|
+
*
|
|
927
|
+
* Purely a memory reclaim with no observable state change: forward links stay
|
|
928
|
+
* dangling (so `orphan`'s `targetOf`-returns-the-dead-handle contract is
|
|
929
|
+
* unchanged), `stateHash` is unaffected (the reverse index is derived, never
|
|
930
|
+
* folded), and the dropped entries are faithfully rebuilt by snapshot/restore
|
|
931
|
+
* from the surviving forward links (`_rebuildRelationIndices`). The only
|
|
932
|
+
* difference a caller can see is `sourcesOf(R, deadHandle)` going from the
|
|
933
|
+
* dangling sources to `[]` — both meaningless once the target is gone.
|
|
934
|
+
* No-op (returns 0) when no relations are registered. */
|
|
935
|
+
compactRelations(): number;
|
|
936
|
+
/** Walk exclusive relation `R` from `src` toward the root, returning the
|
|
937
|
+
* **up**-chain `[src, parent, grandparent, …, root]` (nearest-ancestor-first,
|
|
938
|
+
* inclusive of both endpoints). A source with no target returns `[src]`. The
|
|
939
|
+
* root is the first entity in the chain with no `R`-target, **or** a dangling
|
|
940
|
+
* dead target handle (see below). Throws `RELATION_MODE_MISMATCH` on a multi
|
|
941
|
+
* relation and `RELATION_CYCLE` on a cycle (both `__DEV__`-only; in production
|
|
942
|
+
* a cycle stops at the repeated node).
|
|
943
|
+
*
|
|
944
|
+
* **Dangling links terminate the chain.** Under the `orphan` policy a source
|
|
945
|
+
* keeps pointing at a destroyed target (a dead handle). The walk must *not*
|
|
946
|
+
* advance through such a handle: the backing store is keyed by entity
|
|
947
|
+
* **index**, so reading the dead handle's index would return whatever now
|
|
948
|
+
* occupies that recycled slot — splicing the chain onto an unrelated entity
|
|
949
|
+
* (the ABA the `EntityID`-keyed reverse index avoids for `cascadeOf`). So a
|
|
950
|
+
* dead next-hop is appended as the chain's (dangling) terminus and the walk
|
|
951
|
+
* stops; the caller can detect it with `isAlive`, exactly as `targetOf`
|
|
952
|
+
* returns a dead handle. */
|
|
953
|
+
ancestorsOf(src: EntityID, def: RelationDef): EntityID[];
|
|
954
|
+
/** The root of `src`'s exclusive-relation chain — the last entity of
|
|
955
|
+
* `ancestorsOf` (the one with no `R`-target). `src` itself when it has no
|
|
956
|
+
* target. If the chain ends in a dangling dead target handle (orphan policy),
|
|
957
|
+
* that handle is the root — `isAlive`-check the result if dangling links are
|
|
958
|
+
* possible. Same `__DEV__` guards as `ancestorsOf`. */
|
|
959
|
+
rootOf(src: EntityID, def: RelationDef): EntityID;
|
|
960
|
+
/** Walk exclusive relation `R` **down** from `root` over the reverse index,
|
|
961
|
+
* returning the subtree (including `root`) breadth-first — **parents before
|
|
962
|
+
* children** (the `cascade` order). Children of each node come from
|
|
963
|
+
* `sourcesOf` (ascending by id), so the traversal is deterministic. Throws
|
|
964
|
+
* `RELATION_MODE_MISMATCH` on a multi relation and `RELATION_CYCLE` on a cycle
|
|
965
|
+
* (both `__DEV__`-only; in production an already-visited node is skipped, so
|
|
966
|
+
* it never hangs). */
|
|
967
|
+
cascadeOf(root: EntityID, def: RelationDef): EntityID[];
|
|
968
|
+
/** Purge a destroyed entity from the relation indices — its **source** role
|
|
969
|
+
* only: drop its forward target(s) and unlink it from every target's reverse
|
|
970
|
+
* set. The backing sparse membership row is dropped separately by
|
|
971
|
+
* `_purgeSparse`, so this must run *before* it (it reads the sparse target
|
|
972
|
+
* field for exclusive relations). The entity's **target** role is left
|
|
973
|
+
* intact: the reverse index is keyed by full `EntityID`, so a recycled slot
|
|
974
|
+
* never aliases the dead target's sources. The destroyed entity's **target**
|
|
975
|
+
* role is handled separately by `_cleanupRelationTargets` per each
|
|
976
|
+
* relation's `OnDeleteTarget` policy (#473). Gated by the caller on
|
|
977
|
+
* `relations.length > 0`. */
|
|
978
|
+
private _purgeRelations;
|
|
979
|
+
/** Apply each relation's `OnDeleteTarget` policy for a destroyed **target**
|
|
980
|
+
* `targetId` (#473). Walks the registry; for every relation whose reverse
|
|
981
|
+
* index has sources pointing at the dead target:
|
|
982
|
+
*
|
|
983
|
+
* - **`delete`** — append each source to `cascade`; the caller destroys them
|
|
984
|
+
* through the same path, so chains/trees cascade recursively. The sources'
|
|
985
|
+
* own source-role purge (`_purgeRelations`) drops their forward link and
|
|
986
|
+
* unlinks them from `targetId`'s reverse set, so no reverse entry leaks.
|
|
987
|
+
* - **`clear`** — drop each source's link to the dead target in place via
|
|
988
|
+
* `rs.unlink(src, targetId)` (exclusive drops the row; multi removes the
|
|
989
|
+
* target from the set, dropping membership when it empties). Sources
|
|
990
|
+
* survive — the cardinality bookkeeping is the relation's, not branched here.
|
|
991
|
+
* - **`orphan`** — skipped (the link is left dangling; safe because the
|
|
992
|
+
* reverse key carries the generation).
|
|
993
|
+
*
|
|
994
|
+
* `sourcesOf` returns a fresh snapshot, so mutating the reverse index while
|
|
995
|
+
* iterating is safe. Gated by the caller on `_hasTargetCleanup`, so the
|
|
996
|
+
* whole walk is skipped when no relation opts into a non-`orphan` policy. */
|
|
997
|
+
private _cleanupRelationTargets;
|
|
998
|
+
/** Second query-match path (#469 / ADR-0011): iterate entities matching a
|
|
999
|
+
* dense mask **and** sparse-membership terms, invoking `cb` per entity.
|
|
1000
|
+
* Yields `EntityID`s, not archetype spans — sparse members are scattered
|
|
1001
|
+
* across archetypes, so there is no SoA column to hand back. Driven by the
|
|
1002
|
+
* cheapest candidate set:
|
|
1003
|
+
*
|
|
1004
|
+
* - **sparse require present** → walk the *smallest* required store's
|
|
1005
|
+
* `indices` (an upper bound on the result), filtering each by the other
|
|
1006
|
+
* required stores, the excluded stores, and the dense mask resolved from
|
|
1007
|
+
* the entity's own archetype. Independent of archetype count.
|
|
1008
|
+
* - **sparse exclude only** → walk `denseArchetypes` (already dense-mask-
|
|
1009
|
+
* matched and non-empty by the caller), skipping rows in any excluded
|
|
1010
|
+
* store.
|
|
1011
|
+
* - **neither** → walk `denseArchetypes`' entity ids (dense-only fallback).
|
|
1012
|
+
*
|
|
1013
|
+
* Only reached via `Query.forEachEntity`; dense `forEach` never consults
|
|
1014
|
+
* the sparse stores, so dense-only queries are unaffected (#469 AC). */
|
|
1015
|
+
_forEachSparseMatch(include: BitSet, exclude: BitSet | null, anyOf: BitSet | null, sparseInclude: readonly SparseComponentID[], sparseExclude: readonly SparseComponentID[], denseArchetypes: readonly Archetype[], cb: (entityId: EntityID) => void, includeDisabled: boolean): void;
|
|
1016
|
+
/** Fourth query-match path (#581): yield the matched entities — the exact
|
|
1017
|
+
* `_forEachSparseMatch` intersection (dense mask + sparse require/exclude +
|
|
1018
|
+
* the default enabled-row filter) — in canonical **hierarchy depth order** over
|
|
1019
|
+
* exclusive relation `R`: depth ascending (parents before children), **entity
|
|
1020
|
+
* index ascending within each depth band**. Entities deeper than `maxDepth`
|
|
1021
|
+
* are skipped (`Infinity` = unbounded). Only reached via `Query.forEachEntity`
|
|
1022
|
+
* on a query carrying a `.hierarchy(R)` term.
|
|
1023
|
+
*
|
|
1024
|
+
* `.hierarchy(R)` does not narrow the matched set — it reorders it — so an
|
|
1025
|
+
* entity with no `R`-parent is a root (depth 0) and is yielded first. Depth is a
|
|
1026
|
+
* structural property of the *full* tree (an ancestor outside the matched set
|
|
1027
|
+
* still counts toward depth), computed by a memoised upward walk shared across
|
|
1028
|
+
* the whole batch, so a shared/deep chain costs O(nodes), not O(nodes²).
|
|
1029
|
+
*
|
|
1030
|
+
* The canonical order is produced without a comparator sort (which the observer
|
|
1031
|
+
* bench measured at 2–4× — `observer.ts`): (1) collect the matched ids via
|
|
1032
|
+
* `_forEachSparseMatch`; (2) `radixSortByIndex` → entity-index ascending;
|
|
1033
|
+
* (3) stable-bucket by depth — since the input is index-ascending and the bucket
|
|
1034
|
+
* append is stable, each depth band stays index-ascending. Tuned for the
|
|
1035
|
+
* motivating per-tick case (transform propagation): the radix scratch is reused
|
|
1036
|
+
* instance state (`_hierarchy_radix_*`), so a per-tick pass churns no histograms;
|
|
1037
|
+
* the working set (`matched` / `buckets` / `depthMemo` / `visiting`) is still
|
|
1038
|
+
* allocated per call, as it must stay call-local for re-entrancy.
|
|
1039
|
+
*
|
|
1040
|
+
* Exclusive-only — a multi relation throws `RELATION_MODE_MISMATCH` in `__DEV__`
|
|
1041
|
+
* (mirrors `cascadeOf` / `ancestorsOf`); a cycle is a loud `RELATION_CYCLE` in
|
|
1042
|
+
* `__DEV__` and a safe break in production. */
|
|
1043
|
+
_forEachHierarchyMatch(include: BitSet, exclude: BitSet | null, anyOf: BitSet | null, sparseInclude: readonly SparseComponentID[], sparseExclude: readonly SparseComponentID[], denseArchetypes: readonly Archetype[], relation: RelationDef, maxDepth: number, includeDisabled: boolean, cb: (entityId: EntityID) => void): void;
|
|
1044
|
+
/** Depth of entity index `idx` in the exclusive-relation tree backing `store`
|
|
1045
|
+
* (root = 0), for `_forEachHierarchyMatch`. Memoised across the batch (`memo`),
|
|
1046
|
+
* so a shared ancestor chain is walked once. Walks upward via the backing store's
|
|
1047
|
+
* target field 0 (mirroring `ancestorsOf`), stopping at: a root (no target), an
|
|
1048
|
+
* already-memoised node, or a dangling/dead parent (the child is then treated as
|
|
1049
|
+
* a root — never advance through a recycled slot, the ABA `ancestorsOf` guards).
|
|
1050
|
+
* `visiting` flags the nodes on the current upward path to catch a cycle —
|
|
1051
|
+
* `RELATION_CYCLE` in `__DEV__`, treated as a root in production — and is emptied
|
|
1052
|
+
* on the way back down so it can be reused for the next entity. */
|
|
1053
|
+
private _hierarchyDepthOf;
|
|
1054
|
+
addComponent(entityId: EntityID, def: ComponentDef<Record<string, never>>): void;
|
|
1055
|
+
addComponent<S extends ComponentSchema>(entityId: EntityID, def: ComponentDef<S>, values: FieldValues<S>): void;
|
|
1056
|
+
/** Add multiple components in one transition (resolves final archetype, then moves once).
|
|
1057
|
+
*
|
|
1058
|
+
* Final-mask resolve, not graph walk. The previous implementation called
|
|
1059
|
+
* `archResolveAdd` once per entry, which threaded through every
|
|
1060
|
+
* intermediate archetype on the path — and each unseen intermediate
|
|
1061
|
+
* triggered a fresh `extendColumnStore` even though no entity ever lived
|
|
1062
|
+
* there. Computing the union mask up front and resolving once via
|
|
1063
|
+
* `archGetOrCreateFromMask` collapses N-1 intermediate-archetype
|
|
1064
|
+
* creations into zero for the batched case (#211 follow-up). The lazy
|
|
1065
|
+
* single-mask path remains the same; this just avoids feeding it
|
|
1066
|
+
* archetypes the entity never visits.
|
|
1067
|
+
*
|
|
1068
|
+
* #659 — composite-add edge cache. The final-mask resolve, unlike the
|
|
1069
|
+
* single-add `edges[]` walk, re-pays a per-call `mask.hash()`, `archLookup`
|
|
1070
|
+
* (the Map-of-buckets + `equals` scan), and `getBatchTransitionMap` on
|
|
1071
|
+
* every call — a ~2× gap vs a cached edge walk that the decomposition probe
|
|
1072
|
+
* pinned on the two `Map.get`s, not the hash. So a repeated (source, added-
|
|
1073
|
+
* set) add now resolves through `currentArch`'s composite-add cache: one
|
|
1074
|
+
* `Map.get` on an exact packed key yields the target + transition map, and we
|
|
1075
|
+
* skip the union-mask build entirely. First call per key still resolves via
|
|
1076
|
+
* the final-mask path below (no intermediate planting) and plants the edge.
|
|
1077
|
+
* See docs/reports/bench/regressions/add-components-composite-edge.md. */
|
|
1078
|
+
addComponents(entityId: EntityID, entries: {
|
|
1079
|
+
def: ComponentDef;
|
|
1080
|
+
values?: Record<string, number>;
|
|
1081
|
+
}[]): void;
|
|
1082
|
+
/** Shared move+write tail of `addComponents` (#659): place the entity into
|
|
1083
|
+
* the already-resolved `targetArch` — a `moveEntityFrom` along the cached
|
|
1084
|
+
* `map` when it has a row, else a fresh append (the rowless empty-archetype
|
|
1085
|
+
* source ignores `map`) — then write every entry's fields. Both the
|
|
1086
|
+
* composite-edge-cache hit and the final-mask cold path funnel through here so
|
|
1087
|
+
* the placement logic lives once. */
|
|
1088
|
+
private _addComponentsInto;
|
|
1089
|
+
removeComponent(entityId: EntityID, def: ComponentDef): void;
|
|
1090
|
+
/** Remove multiple components in one transition (resolves final archetype, then moves once).
|
|
1091
|
+
*
|
|
1092
|
+
* Final-mask resolve, not graph walk. Same rationale as `addComponents`
|
|
1093
|
+
* above — the previous per-step path threaded `archResolveRemove`
|
|
1094
|
+
* once per def, which materialised every intermediate archetype on the
|
|
1095
|
+
* removal path. Computing the difference mask up front and resolving
|
|
1096
|
+
* once avoids planting N-1 intermediates the entity never lives in. */
|
|
1097
|
+
removeComponents(entityId: EntityID, defs: ComponentDef[]): void;
|
|
1098
|
+
hasComponent(entityId: EntityID, def: ComponentDef): boolean;
|
|
1099
|
+
/**
|
|
1100
|
+
* Bulk add a component to ALL entities in the given archetype.
|
|
1101
|
+
* Uses TypedArray.set() for O(columns) instead of O(N×columns).
|
|
1102
|
+
* The archetype must not already contain this component.
|
|
1103
|
+
*/
|
|
1104
|
+
batchAddComponent(src: ArchetypeID, def: ComponentDef, values?: Record<string, number>): void;
|
|
1105
|
+
/**
|
|
1106
|
+
* Bulk remove a component from ALL entities in the given archetype.
|
|
1107
|
+
* Uses TypedArray.set() for O(columns) instead of O(N×columns).
|
|
1108
|
+
* The archetype must contain this component.
|
|
1109
|
+
*/
|
|
1110
|
+
batchRemoveComponent(src: ArchetypeID, def: ComponentDef): void;
|
|
1111
|
+
getEntityArchetype(entityId: EntityID): Archetype;
|
|
1112
|
+
getEntityRow(entityId: EntityID): number;
|
|
1113
|
+
/**
|
|
1114
|
+
* Find all archetypes matching the given masks.
|
|
1115
|
+
* Uses the inverted componentIndex to start from the component with the
|
|
1116
|
+
* fewest archetypes, minimizing the number of superset checks.
|
|
1117
|
+
*/
|
|
1118
|
+
getMatchingArchetypes(required: BitSet, excluded?: BitSet, anyOf?: BitSet): readonly Archetype[];
|
|
1119
|
+
/**
|
|
1120
|
+
* Register a live query. Returns a mutable Archetype[] that this Store will
|
|
1121
|
+
* push newly-created matching archetypes into, keeping the query always up-to-date.
|
|
1122
|
+
*/
|
|
1123
|
+
registerQuery(include: BitSet, exclude?: BitSet, anyOf?: BitSet): Archetype[];
|
|
1124
|
+
updateQueryRef(result: Archetype[], query: Query<any>): void;
|
|
1125
|
+
get archetypeCount(): number;
|
|
1126
|
+
registerEvent<S extends EventSchema>(fields: readonly (keyof S & string)[]): EventDef<S>;
|
|
1127
|
+
emitEvent(def: EventDef, values: Record<string, number>): void;
|
|
1128
|
+
emitSignal(def: EventDef<EmptyEventSchema>): void;
|
|
1129
|
+
getEventReader<S extends EventSchema>(def: EventDef<S>): EventReader<S>;
|
|
1130
|
+
clearEvents(): void;
|
|
1131
|
+
/** `__DEV__`-only: total events currently buffered across the dirty channels.
|
|
1132
|
+
* `ECS.update` samples this either side of `dispatchSet` to assert an onSet
|
|
1133
|
+
* observer emitted nothing — its emissions would be wiped by the tick-tail
|
|
1134
|
+
* `clearEvents` and break the empty-channel-at-boundary invariant snapshot /
|
|
1135
|
+
* restore relies on (#586). Walks only the dirty list, never the hot emit path. */
|
|
1136
|
+
_devBufferedEventCount(): number;
|
|
1137
|
+
private readonly eventKeyMap;
|
|
1138
|
+
registerEventByKey<S extends EventSchema>(key: symbol, fields: readonly (keyof S & string)[]): EventDef<S>;
|
|
1139
|
+
getEventDefByKey(key: symbol): EventDef<any>;
|
|
1140
|
+
hasEventKey(key: symbol): boolean;
|
|
1141
|
+
private readonly resourceKeyMap;
|
|
1142
|
+
registerResource(key: symbol, value: unknown): void;
|
|
1143
|
+
getResource(key: symbol): unknown;
|
|
1144
|
+
setResource(key: symbol, value: unknown): void;
|
|
1145
|
+
removeResource(key: symbol): void;
|
|
1146
|
+
hasResource(key: symbol): boolean;
|
|
1147
|
+
}
|
|
1148
|
+
export {};
|
|
1149
|
+
//# sourceMappingURL=store.d.ts.map
|