@aethervtt/shard-core 0.0.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/ecs/commands.d.ts +35 -0
- package/dist/ecs/commands.js +96 -0
- package/dist/ecs/entity.d.ts +11 -0
- package/dist/ecs/entity.js +12 -0
- package/dist/ecs/events.d.ts +34 -0
- package/dist/ecs/events.js +65 -0
- package/dist/ecs/hierarchy.d.ts +7 -0
- package/dist/ecs/hierarchy.js +29 -0
- package/dist/ecs/index.d.ts +8 -0
- package/dist/ecs/index.js +38 -0
- package/dist/ecs/observers.d.ts +40 -0
- package/dist/ecs/observers.js +27 -0
- package/dist/ecs/query.d.ts +44 -0
- package/dist/ecs/query.js +67 -0
- package/dist/ecs/table.d.ts +91 -0
- package/dist/ecs/table.js +286 -0
- package/dist/ecs/world.d.ts +127 -0
- package/dist/ecs/world.js +513 -0
- package/dist/error.d.ts +26 -0
- package/dist/error.js +29 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -0
- package/dist/json.d.ts +15 -0
- package/dist/json.js +22 -0
- package/dist/math/aabb.d.ts +13 -0
- package/dist/math/aabb.js +78 -0
- package/dist/math/affine.d.ts +28 -0
- package/dist/math/affine.js +159 -0
- package/dist/math/affine64.d.ts +11 -0
- package/dist/math/affine64.js +58 -0
- package/dist/math/frustum.d.ts +11 -0
- package/dist/math/frustum.js +59 -0
- package/dist/math/hash.d.ts +14 -0
- package/dist/math/hash.js +38 -0
- package/dist/math/index.d.ts +14 -0
- package/dist/math/index.js +33 -0
- package/dist/math/mat3.d.ts +6 -0
- package/dist/math/mat3.js +37 -0
- package/dist/math/mat4.d.ts +23 -0
- package/dist/math/mat4.js +224 -0
- package/dist/math/quat.d.ts +22 -0
- package/dist/math/quat.js +233 -0
- package/dist/math/ray.d.ts +10 -0
- package/dist/math/ray.js +54 -0
- package/dist/math/rng.d.ts +41 -0
- package/dist/math/rng.js +102 -0
- package/dist/math/types.d.ts +8 -0
- package/dist/math/types.js +4 -0
- package/dist/math/vec2.d.ts +11 -0
- package/dist/math/vec2.js +52 -0
- package/dist/math/vec3.d.ts +23 -0
- package/dist/math/vec3.js +135 -0
- package/dist/math/vec4.d.ts +8 -0
- package/dist/math/vec4.js +47 -0
- package/dist/scheduler/index.d.ts +3 -0
- package/dist/scheduler/index.js +39 -0
- package/dist/scheduler/profiler.d.ts +19 -0
- package/dist/scheduler/profiler.js +44 -0
- package/dist/scheduler/schedule.d.ts +52 -0
- package/dist/scheduler/schedule.js +235 -0
- package/dist/scheduler/system.d.ts +74 -0
- package/dist/scheduler/system.js +106 -0
- package/dist/schema/color.d.ts +7 -0
- package/dist/schema/color.js +34 -0
- package/dist/schema/component.d.ts +71 -0
- package/dist/schema/component.js +120 -0
- package/dist/schema/field.d.ts +145 -0
- package/dist/schema/field.js +591 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.js +40 -0
- package/dist/schema/names.d.ts +27 -0
- package/dist/schema/names.js +39 -0
- package/dist/schema/registry.d.ts +38 -0
- package/dist/schema/registry.js +58 -0
- package/dist/schema/resource.d.ts +48 -0
- package/dist/schema/resource.js +79 -0
- package/dist/test-env.d.ts +31 -0
- package/dist/test-env.js +37 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Dias
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ComponentDef } from '../schema/component.js';
|
|
2
|
+
import type { Fields, InitFields } from '../schema/field.js';
|
|
3
|
+
import type { Entity } from './entity.js';
|
|
4
|
+
import type { ComponentInit, World } from './world.js';
|
|
5
|
+
/**
|
|
6
|
+
* Deferred world changes, applied in issue order at the next sync point. Use these for
|
|
7
|
+
* structural changes (spawn, despawn, add, remove) while iterating a query.
|
|
8
|
+
*
|
|
9
|
+
* Stored in flat, reused arrays: after warm-up, recording a command doesn't allocate
|
|
10
|
+
* beyond the values you pass in.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Commands {
|
|
13
|
+
private readonly ops;
|
|
14
|
+
private readonly targets;
|
|
15
|
+
private readonly defs;
|
|
16
|
+
private readonly payloads;
|
|
17
|
+
/** The world the commands apply to. */
|
|
18
|
+
readonly world: World;
|
|
19
|
+
constructor(world: World);
|
|
20
|
+
get length(): number;
|
|
21
|
+
/** Reserves an entity id now; the entity comes alive when the commands apply. */
|
|
22
|
+
spawn(...inits: ComponentInit[]): Entity;
|
|
23
|
+
/** Despawns the entity and its descendants. Already-dead entities are skipped. */
|
|
24
|
+
despawn(entity: Entity): void;
|
|
25
|
+
despawnSingle(entity: Entity): void;
|
|
26
|
+
add<F extends Fields>(entity: Entity, component: ComponentDef<F>, value?: InitFields<F>): void;
|
|
27
|
+
remove(entity: Entity, component: ComponentDef): void;
|
|
28
|
+
set<F extends Fields>(entity: Entity, component: ComponentDef<F>, value: InitFields<F>): void;
|
|
29
|
+
/** Runs arbitrary code against the world at apply time. */
|
|
30
|
+
run(fn: (world: World) => void): void;
|
|
31
|
+
/** Applies every recorded command in order, then clears the buffer. */
|
|
32
|
+
apply(): void;
|
|
33
|
+
clear(): void;
|
|
34
|
+
private push;
|
|
35
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const SPAWN = 0;
|
|
2
|
+
const DESPAWN = 1;
|
|
3
|
+
const DESPAWN_SINGLE = 2;
|
|
4
|
+
const ADD = 3;
|
|
5
|
+
const REMOVE = 4;
|
|
6
|
+
const SET = 5;
|
|
7
|
+
const RUN = 6;
|
|
8
|
+
class Commands {
|
|
9
|
+
ops = [];
|
|
10
|
+
targets = [];
|
|
11
|
+
defs = [];
|
|
12
|
+
payloads = [];
|
|
13
|
+
/** The world the commands apply to. */
|
|
14
|
+
world;
|
|
15
|
+
constructor(world) {
|
|
16
|
+
this.world = world;
|
|
17
|
+
}
|
|
18
|
+
get length() {
|
|
19
|
+
return this.ops.length;
|
|
20
|
+
}
|
|
21
|
+
/** Reserves an entity id now; the entity comes alive when the commands apply. */
|
|
22
|
+
spawn(...inits) {
|
|
23
|
+
const entity = this.world.reserveEntity();
|
|
24
|
+
this.push(SPAWN, entity, void 0, inits);
|
|
25
|
+
return entity;
|
|
26
|
+
}
|
|
27
|
+
/** Despawns the entity and its descendants. Already-dead entities are skipped. */
|
|
28
|
+
despawn(entity) {
|
|
29
|
+
this.push(DESPAWN, entity, void 0, void 0);
|
|
30
|
+
}
|
|
31
|
+
despawnSingle(entity) {
|
|
32
|
+
this.push(DESPAWN_SINGLE, entity, void 0, void 0);
|
|
33
|
+
}
|
|
34
|
+
add(entity, component, value) {
|
|
35
|
+
this.push(ADD, entity, component, value);
|
|
36
|
+
}
|
|
37
|
+
remove(entity, component) {
|
|
38
|
+
this.push(REMOVE, entity, component, void 0);
|
|
39
|
+
}
|
|
40
|
+
set(entity, component, value) {
|
|
41
|
+
this.push(SET, entity, component, value);
|
|
42
|
+
}
|
|
43
|
+
/** Runs arbitrary code against the world at apply time. */
|
|
44
|
+
run(fn) {
|
|
45
|
+
this.push(RUN, -1, void 0, fn);
|
|
46
|
+
}
|
|
47
|
+
/** Applies every recorded command in order, then clears the buffer. */
|
|
48
|
+
apply() {
|
|
49
|
+
const world = this.world;
|
|
50
|
+
for (let i = 0; i < this.ops.length; i++) {
|
|
51
|
+
const entity = this.targets[i];
|
|
52
|
+
const def = this.defs[i];
|
|
53
|
+
const payload = this.payloads[i];
|
|
54
|
+
switch (this.ops[i]) {
|
|
55
|
+
case SPAWN:
|
|
56
|
+
world.spawnReserved(entity, payload);
|
|
57
|
+
break;
|
|
58
|
+
case DESPAWN:
|
|
59
|
+
if (world.isAlive(entity)) world.despawn(entity);
|
|
60
|
+
break;
|
|
61
|
+
case DESPAWN_SINGLE:
|
|
62
|
+
if (world.isAlive(entity)) world.despawnSingle(entity);
|
|
63
|
+
break;
|
|
64
|
+
case ADD:
|
|
65
|
+
world.add(entity, def, payload);
|
|
66
|
+
break;
|
|
67
|
+
case REMOVE:
|
|
68
|
+
world.remove(entity, def);
|
|
69
|
+
break;
|
|
70
|
+
case SET:
|
|
71
|
+
world.set(entity, def, payload);
|
|
72
|
+
break;
|
|
73
|
+
case RUN:
|
|
74
|
+
;
|
|
75
|
+
payload(world);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this.clear();
|
|
80
|
+
}
|
|
81
|
+
clear() {
|
|
82
|
+
this.ops.length = 0;
|
|
83
|
+
this.targets.length = 0;
|
|
84
|
+
this.defs.length = 0;
|
|
85
|
+
this.payloads.length = 0;
|
|
86
|
+
}
|
|
87
|
+
push(op, entity, def, payload) {
|
|
88
|
+
this.ops.push(op);
|
|
89
|
+
this.targets.push(entity);
|
|
90
|
+
this.defs.push(def);
|
|
91
|
+
this.payloads.push(payload);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
Commands
|
|
96
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An entity id: `index + generation * 2^22`. Plain number, safe integer, serializable.
|
|
3
|
+
* The index addresses per-entity storage; the generation detects stale ids.
|
|
4
|
+
*/
|
|
5
|
+
export type Entity = number;
|
|
6
|
+
export declare const MAX_ENTITIES = 4194304;
|
|
7
|
+
export declare const entityIndex: (e: Entity) => number;
|
|
8
|
+
export declare const entityGeneration: (e: Entity) => number;
|
|
9
|
+
export declare const makeEntity: (index: number, generation: number) => Entity;
|
|
10
|
+
/** Human-readable form, e.g. `12v3` (index 12, generation 3). */
|
|
11
|
+
export declare const formatEntity: (e: Entity) => string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const MAX_ENTITIES = 4194304;
|
|
2
|
+
const entityIndex = (e) => e % MAX_ENTITIES;
|
|
3
|
+
const entityGeneration = (e) => Math.floor(e / MAX_ENTITIES);
|
|
4
|
+
const makeEntity = (index, generation) => generation * MAX_ENTITIES + index;
|
|
5
|
+
const formatEntity = (e) => `${entityIndex(e)}v${entityGeneration(e)}`;
|
|
6
|
+
export {
|
|
7
|
+
MAX_ENTITIES,
|
|
8
|
+
entityGeneration,
|
|
9
|
+
entityIndex,
|
|
10
|
+
formatEntity,
|
|
11
|
+
makeEntity
|
|
12
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EventDef } from '../schema/resource.js';
|
|
2
|
+
/**
|
|
3
|
+
* Double-buffered event storage. Events sent during frame N are readable during frames N and
|
|
4
|
+
* N+1, then dropped. Every event has a sequence number so readers can track what they've seen.
|
|
5
|
+
*/
|
|
6
|
+
export declare class EventQueue<T> {
|
|
7
|
+
private older;
|
|
8
|
+
private newer;
|
|
9
|
+
private olderStart;
|
|
10
|
+
private newerStart;
|
|
11
|
+
private sent;
|
|
12
|
+
readonly def: EventDef<T>;
|
|
13
|
+
constructor(def: EventDef<T>);
|
|
14
|
+
send(event: T): void;
|
|
15
|
+
/** Called once per frame by the app. Drops events older than one frame. */
|
|
16
|
+
update(): void;
|
|
17
|
+
/** Events currently buffered (both frames). */
|
|
18
|
+
get length(): number;
|
|
19
|
+
/** @internal Copies events with sequence >= cursor into `out`. Returns the new cursor. */
|
|
20
|
+
readFrom(cursor: number, out: T[]): number;
|
|
21
|
+
/** @internal */
|
|
22
|
+
get oldestSequence(): number;
|
|
23
|
+
}
|
|
24
|
+
/** Independent cursor over an event queue. Several readers can consume the same events. */
|
|
25
|
+
export declare class EventReader<T> {
|
|
26
|
+
private cursor;
|
|
27
|
+
private readonly scratch;
|
|
28
|
+
private readonly queue;
|
|
29
|
+
constructor(queue: EventQueue<T>);
|
|
30
|
+
/** Events not yet seen by this reader. The array is reused: copy it to keep it. */
|
|
31
|
+
read(): readonly T[];
|
|
32
|
+
/** Marks everything as seen without returning it. */
|
|
33
|
+
clear(): void;
|
|
34
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
class EventQueue {
|
|
2
|
+
older = [];
|
|
3
|
+
newer = [];
|
|
4
|
+
olderStart = 0;
|
|
5
|
+
newerStart = 0;
|
|
6
|
+
sent = 0;
|
|
7
|
+
def;
|
|
8
|
+
constructor(def) {
|
|
9
|
+
this.def = def;
|
|
10
|
+
}
|
|
11
|
+
send(event) {
|
|
12
|
+
this.newer.push(event);
|
|
13
|
+
this.sent++;
|
|
14
|
+
}
|
|
15
|
+
/** Called once per frame by the app. Drops events older than one frame. */
|
|
16
|
+
update() {
|
|
17
|
+
const dropped = this.older;
|
|
18
|
+
this.older = this.newer;
|
|
19
|
+
this.olderStart = this.newerStart;
|
|
20
|
+
dropped.length = 0;
|
|
21
|
+
this.newer = dropped;
|
|
22
|
+
this.newerStart = this.sent;
|
|
23
|
+
}
|
|
24
|
+
/** Events currently buffered (both frames). */
|
|
25
|
+
get length() {
|
|
26
|
+
return this.older.length + this.newer.length;
|
|
27
|
+
}
|
|
28
|
+
/** @internal Copies events with sequence >= cursor into `out`. Returns the new cursor. */
|
|
29
|
+
readFrom(cursor, out) {
|
|
30
|
+
const start = Math.max(cursor, this.olderStart);
|
|
31
|
+
for (let i = start - this.olderStart; i < this.older.length; i++) out.push(this.older[i]);
|
|
32
|
+
const newerFrom = Math.max(cursor, this.newerStart);
|
|
33
|
+
for (let i = newerFrom - this.newerStart; i < this.newer.length; i++) out.push(this.newer[i]);
|
|
34
|
+
return this.sent;
|
|
35
|
+
}
|
|
36
|
+
/** @internal */
|
|
37
|
+
get oldestSequence() {
|
|
38
|
+
return this.olderStart;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
class EventReader {
|
|
42
|
+
cursor;
|
|
43
|
+
scratch = [];
|
|
44
|
+
queue;
|
|
45
|
+
constructor(queue) {
|
|
46
|
+
this.queue = queue;
|
|
47
|
+
this.cursor = queue.oldestSequence;
|
|
48
|
+
}
|
|
49
|
+
/** Events not yet seen by this reader. The array is reused: copy it to keep it. */
|
|
50
|
+
read() {
|
|
51
|
+
this.scratch.length = 0;
|
|
52
|
+
this.cursor = this.queue.readFrom(this.cursor, this.scratch);
|
|
53
|
+
return this.scratch;
|
|
54
|
+
}
|
|
55
|
+
/** Marks everything as seen without returning it. */
|
|
56
|
+
clear() {
|
|
57
|
+
this.scratch.length = 0;
|
|
58
|
+
this.cursor = this.queue.readFrom(this.cursor, this.scratch);
|
|
59
|
+
this.scratch.length = 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
EventQueue,
|
|
64
|
+
EventReader
|
|
65
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const ChildOf: import("..").ComponentDef<{
|
|
2
|
+
readonly parent: import("..").FieldType<number | null, "f64">;
|
|
3
|
+
}>;
|
|
4
|
+
export declare const Children: import("..").ComponentDef<{
|
|
5
|
+
readonly entities: import("..").FieldType<(number | null)[], "object">;
|
|
6
|
+
}>;
|
|
7
|
+
export declare const Derived: import("..").TagDef;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineComponent, defineTag } from "../schema/component.js";
|
|
2
|
+
import { t } from "../schema/field.js";
|
|
3
|
+
const ChildOf = defineComponent(
|
|
4
|
+
"core/ChildOf",
|
|
5
|
+
{ parent: t.entity({ description: "The parent entity." }) },
|
|
6
|
+
{ description: "Makes this entity a child of another. Despawning the parent despawns it." }
|
|
7
|
+
);
|
|
8
|
+
const Children = defineComponent(
|
|
9
|
+
"core/Children",
|
|
10
|
+
{
|
|
11
|
+
entities: t.list(t.entity, {
|
|
12
|
+
readonly: true,
|
|
13
|
+
description: "Maintained automatically from ChildOf. Do not write."
|
|
14
|
+
})
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
description: "The direct children of this entity, in insertion order. Derived from ChildOf.",
|
|
18
|
+
serialize: false
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
const Derived = defineTag("core/Derived", {
|
|
22
|
+
description: "An entity the engine rebuilds from other state every run (terrain chunks). Saved games and world hashes leave it out.",
|
|
23
|
+
serialize: false
|
|
24
|
+
});
|
|
25
|
+
export {
|
|
26
|
+
ChildOf,
|
|
27
|
+
Children,
|
|
28
|
+
Derived
|
|
29
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Commands } from './commands.js';
|
|
2
|
+
export { type Entity, entityGeneration, entityIndex, formatEntity, MAX_ENTITIES, makeEntity, } from './entity.js';
|
|
3
|
+
export { EventQueue, EventReader } from './events.js';
|
|
4
|
+
export { ChildOf, Children, Derived } from './hierarchy.js';
|
|
5
|
+
export { type LifecycleEvent, type LifecycleKind, type LifecycleObserver, type LifecycleTrigger, onAdd, onRemove, onSet, type TriggerEvent, type TriggerObserver, } from './observers.js';
|
|
6
|
+
export { Query, type QueryDescriptor } from './query.js';
|
|
7
|
+
export { type ColumnOf, ComponentStorage, Table, type TickSource } from './table.js';
|
|
8
|
+
export { type ComponentInit, World, type WorldStats } from './world.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Commands } from "./commands.js";
|
|
2
|
+
import {
|
|
3
|
+
entityGeneration,
|
|
4
|
+
entityIndex,
|
|
5
|
+
formatEntity,
|
|
6
|
+
MAX_ENTITIES,
|
|
7
|
+
makeEntity
|
|
8
|
+
} from "./entity.js";
|
|
9
|
+
import { EventQueue, EventReader } from "./events.js";
|
|
10
|
+
import { ChildOf, Children, Derived } from "./hierarchy.js";
|
|
11
|
+
import {
|
|
12
|
+
onAdd,
|
|
13
|
+
onRemove,
|
|
14
|
+
onSet
|
|
15
|
+
} from "./observers.js";
|
|
16
|
+
import { Query } from "./query.js";
|
|
17
|
+
import { ComponentStorage, Table } from "./table.js";
|
|
18
|
+
import { World } from "./world.js";
|
|
19
|
+
export {
|
|
20
|
+
ChildOf,
|
|
21
|
+
Children,
|
|
22
|
+
Commands,
|
|
23
|
+
ComponentStorage,
|
|
24
|
+
Derived,
|
|
25
|
+
EventQueue,
|
|
26
|
+
EventReader,
|
|
27
|
+
MAX_ENTITIES,
|
|
28
|
+
Query,
|
|
29
|
+
Table,
|
|
30
|
+
World,
|
|
31
|
+
entityGeneration,
|
|
32
|
+
entityIndex,
|
|
33
|
+
formatEntity,
|
|
34
|
+
makeEntity,
|
|
35
|
+
onAdd,
|
|
36
|
+
onRemove,
|
|
37
|
+
onSet
|
|
38
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ComponentDef } from '../schema/component.js';
|
|
2
|
+
import type { Fields, InferFields } from '../schema/field.js';
|
|
3
|
+
import type { EventDef } from '../schema/resource.js';
|
|
4
|
+
import type { Entity } from './entity.js';
|
|
5
|
+
import type { World } from './world.js';
|
|
6
|
+
export type LifecycleKind = 'add' | 'remove' | 'set';
|
|
7
|
+
export interface LifecycleTrigger<F extends Fields = Fields> {
|
|
8
|
+
readonly kind: LifecycleKind;
|
|
9
|
+
readonly component: ComponentDef<F>;
|
|
10
|
+
}
|
|
11
|
+
/** Fires after the component is added (not when an existing one is replaced; that's `onSet`). */
|
|
12
|
+
export declare const onAdd: <F extends Fields>(component: ComponentDef<F>) => LifecycleTrigger<F>;
|
|
13
|
+
/** Fires before the component is removed, including on despawn. The value is still readable. */
|
|
14
|
+
export declare const onRemove: <F extends Fields>(component: ComponentDef<F>) => LifecycleTrigger<F>;
|
|
15
|
+
/** Fires after `world.set`, or after `world.add` replaces an existing value. */
|
|
16
|
+
export declare const onSet: <F extends Fields>(component: ComponentDef<F>) => LifecycleTrigger<F>;
|
|
17
|
+
export interface LifecycleEvent<F extends Fields = Fields> {
|
|
18
|
+
readonly kind: LifecycleKind;
|
|
19
|
+
readonly entity: Entity;
|
|
20
|
+
readonly component: ComponentDef<F>;
|
|
21
|
+
readonly world: World;
|
|
22
|
+
/** For `set`: the value before the write. */
|
|
23
|
+
readonly previous: InferFields<F> | undefined;
|
|
24
|
+
}
|
|
25
|
+
export interface TriggerEvent<T> {
|
|
26
|
+
readonly event: EventDef<T>;
|
|
27
|
+
readonly data: T;
|
|
28
|
+
readonly entity: Entity | undefined;
|
|
29
|
+
readonly world: World;
|
|
30
|
+
}
|
|
31
|
+
export type LifecycleObserver<F extends Fields = Fields> = (event: LifecycleEvent<F>) => void;
|
|
32
|
+
export type TriggerObserver<T> = (event: TriggerEvent<T>) => void;
|
|
33
|
+
/** Observer lists indexed by component or event id. */
|
|
34
|
+
export declare class ObserverTable {
|
|
35
|
+
readonly add: (LifecycleObserver[] | undefined)[];
|
|
36
|
+
readonly remove: (LifecycleObserver[] | undefined)[];
|
|
37
|
+
readonly set: (LifecycleObserver[] | undefined)[];
|
|
38
|
+
readonly custom: (TriggerObserver<unknown>[] | undefined)[];
|
|
39
|
+
listFor(kind: LifecycleKind): (LifecycleObserver[] | undefined)[];
|
|
40
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const onAdd = (component) => ({
|
|
2
|
+
kind: "add",
|
|
3
|
+
component
|
|
4
|
+
});
|
|
5
|
+
const onRemove = (component) => ({
|
|
6
|
+
kind: "remove",
|
|
7
|
+
component
|
|
8
|
+
});
|
|
9
|
+
const onSet = (component) => ({
|
|
10
|
+
kind: "set",
|
|
11
|
+
component
|
|
12
|
+
});
|
|
13
|
+
class ObserverTable {
|
|
14
|
+
add = [];
|
|
15
|
+
remove = [];
|
|
16
|
+
set = [];
|
|
17
|
+
custom = [];
|
|
18
|
+
listFor(kind) {
|
|
19
|
+
return kind === "add" ? this.add : kind === "remove" ? this.remove : this.set;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
ObserverTable,
|
|
24
|
+
onAdd,
|
|
25
|
+
onRemove,
|
|
26
|
+
onSet
|
|
27
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ComponentDef } from '../schema/component.js';
|
|
2
|
+
import type { Entity } from './entity.js';
|
|
3
|
+
import type { Table } from './table.js';
|
|
4
|
+
export interface QueryDescriptor {
|
|
5
|
+
/** Entities must have all of these. */
|
|
6
|
+
with?: readonly ComponentDef[];
|
|
7
|
+
/** Entities must have none of these. */
|
|
8
|
+
without?: readonly ComponentDef[];
|
|
9
|
+
/** Documents components the system may read if present. Doesn't affect matching. */
|
|
10
|
+
optional?: readonly ComponentDef[];
|
|
11
|
+
/** Row filter: component added since the `since` tick passed to `each`/`count`. Implies `with`. */
|
|
12
|
+
added?: readonly ComponentDef[];
|
|
13
|
+
/** Row filter: component changed since the `since` tick. Implies `with`. */
|
|
14
|
+
changed?: readonly ComponentDef[];
|
|
15
|
+
}
|
|
16
|
+
export declare function queryKey(desc: QueryDescriptor): string;
|
|
17
|
+
/**
|
|
18
|
+
* A cached list of tables matching a descriptor. The world keeps `tables` up to date as new
|
|
19
|
+
* archetypes appear. Create queries once (in system setup) and reuse them.
|
|
20
|
+
*
|
|
21
|
+
* Hot loops should walk `tables` directly; `each` is for cold code.
|
|
22
|
+
*/
|
|
23
|
+
export declare class Query {
|
|
24
|
+
readonly tables: Table[];
|
|
25
|
+
readonly with: readonly ComponentDef[];
|
|
26
|
+
readonly without: readonly ComponentDef[];
|
|
27
|
+
readonly optional: readonly ComponentDef[];
|
|
28
|
+
readonly added: readonly ComponentDef[];
|
|
29
|
+
readonly changed: readonly ComponentDef[];
|
|
30
|
+
private readonly withIds;
|
|
31
|
+
private readonly withoutIds;
|
|
32
|
+
constructor(desc: QueryDescriptor);
|
|
33
|
+
/** @internal Called by the world for every table, once. */
|
|
34
|
+
consider(table: Table): void;
|
|
35
|
+
/** Whether a row passes the `added` / `changed` filters for a given `since` tick. */
|
|
36
|
+
passes(table: Table, row: number, since: number): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Calls `fn` for each matching row. `since` is the tick for `added`/`changed` filters
|
|
39
|
+
* (a system passes `ctx.lastRunTick`). Don't make structural changes inside `fn`; use commands.
|
|
40
|
+
*/
|
|
41
|
+
each(fn: (entity: Entity, row: number, table: Table) => void, since?: number): void;
|
|
42
|
+
count(since?: number): number;
|
|
43
|
+
entities(since?: number): Entity[];
|
|
44
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function queryKey(desc) {
|
|
2
|
+
const ids = (defs) => (defs ?? []).map((d) => d.id).sort((a, b) => a - b).join(",");
|
|
3
|
+
return `w${ids(desc.with)}|x${ids(desc.without)}|a${ids(desc.added)}|c${ids(desc.changed)}`;
|
|
4
|
+
}
|
|
5
|
+
class Query {
|
|
6
|
+
tables = [];
|
|
7
|
+
with;
|
|
8
|
+
without;
|
|
9
|
+
optional;
|
|
10
|
+
added;
|
|
11
|
+
changed;
|
|
12
|
+
withIds;
|
|
13
|
+
withoutIds;
|
|
14
|
+
constructor(desc) {
|
|
15
|
+
this.added = desc.added ?? [];
|
|
16
|
+
this.changed = desc.changed ?? [];
|
|
17
|
+
this.with = [.../* @__PURE__ */ new Set([...desc.with ?? [], ...this.added, ...this.changed])];
|
|
18
|
+
this.without = desc.without ?? [];
|
|
19
|
+
this.optional = desc.optional ?? [];
|
|
20
|
+
this.withIds = this.with.map((d) => d.id);
|
|
21
|
+
this.withoutIds = this.without.map((d) => d.id);
|
|
22
|
+
}
|
|
23
|
+
/** @internal Called by the world for every table, once. */
|
|
24
|
+
consider(table) {
|
|
25
|
+
for (const id of this.withIds) if (!table.hasId(id)) return;
|
|
26
|
+
for (const id of this.withoutIds) if (table.hasId(id)) return;
|
|
27
|
+
this.tables.push(table);
|
|
28
|
+
}
|
|
29
|
+
/** Whether a row passes the `added` / `changed` filters for a given `since` tick. */
|
|
30
|
+
passes(table, row, since) {
|
|
31
|
+
for (const def of this.added) if (!table.isAdded(def, row, since)) return false;
|
|
32
|
+
for (const def of this.changed) if (!table.isChanged(def, row, since)) return false;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Calls `fn` for each matching row. `since` is the tick for `added`/`changed` filters
|
|
37
|
+
* (a system passes `ctx.lastRunTick`). Don't make structural changes inside `fn`; use commands.
|
|
38
|
+
*/
|
|
39
|
+
each(fn, since = 0) {
|
|
40
|
+
const filtered = this.added.length > 0 || this.changed.length > 0;
|
|
41
|
+
for (const table of this.tables) {
|
|
42
|
+
for (let row = 0; row < table.count; row++) {
|
|
43
|
+
if (filtered && !this.passes(table, row, since)) continue;
|
|
44
|
+
fn(table.entities[row], row, table);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
count(since = 0) {
|
|
49
|
+
if (this.added.length === 0 && this.changed.length === 0) {
|
|
50
|
+
let n2 = 0;
|
|
51
|
+
for (const table of this.tables) n2 += table.count;
|
|
52
|
+
return n2;
|
|
53
|
+
}
|
|
54
|
+
let n = 0;
|
|
55
|
+
this.each(() => n++, since);
|
|
56
|
+
return n;
|
|
57
|
+
}
|
|
58
|
+
entities(since = 0) {
|
|
59
|
+
const out = [];
|
|
60
|
+
this.each((e) => out.push(e), since);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
Query,
|
|
66
|
+
queryKey
|
|
67
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ComponentDef } from '../schema/component.js';
|
|
2
|
+
import type { Column, Fields, FieldType, InferFields, TypedArrayFor } from '../schema/field.js';
|
|
3
|
+
import type { Entity } from './entity.js';
|
|
4
|
+
/** Anything that knows the current change-detection tick (the world). */
|
|
5
|
+
export interface TickSource {
|
|
6
|
+
readonly tick: number;
|
|
7
|
+
}
|
|
8
|
+
/** The column type for a field: a TypedArray for numeric storage, a JS array otherwise. */
|
|
9
|
+
export type ColumnOf<T> = T extends FieldType<infer V, infer S> ? S extends 'object' ? (V | undefined)[] : TypedArrayFor<Exclude<S, 'object'>> : never;
|
|
10
|
+
/** Storage for one component inside one table. */
|
|
11
|
+
export declare class ComponentStorage {
|
|
12
|
+
readonly columns: Column[];
|
|
13
|
+
/** Columns by field name. Same arrays as `columns`. */
|
|
14
|
+
readonly byName: Record<string, Column>;
|
|
15
|
+
added: Uint32Array;
|
|
16
|
+
changed: Uint32Array;
|
|
17
|
+
/** The newest tick in `changed`, so a system can skip a table where nothing changed. */
|
|
18
|
+
lastChanged: number;
|
|
19
|
+
def: ComponentDef;
|
|
20
|
+
constructor(def: ComponentDef, capacity: number);
|
|
21
|
+
grow(capacity: number): void;
|
|
22
|
+
private reindex;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* All entities that have exactly one set of components (an archetype), stored as columns.
|
|
26
|
+
*
|
|
27
|
+
* Column arrays are replaced when the table grows, so fetch them each frame rather than
|
|
28
|
+
* holding on to them.
|
|
29
|
+
*/
|
|
30
|
+
export declare class Table {
|
|
31
|
+
count: number;
|
|
32
|
+
capacity: number;
|
|
33
|
+
entities: Float64Array;
|
|
34
|
+
/** Component ids in this archetype, sorted. */
|
|
35
|
+
readonly ids: readonly number[];
|
|
36
|
+
readonly storages: readonly ComponentStorage[];
|
|
37
|
+
/** Cached archetype transitions: component id → table with it added / removed. */
|
|
38
|
+
readonly addEdges: Map<number, Table>;
|
|
39
|
+
readonly removeEdges: Map<number, Table>;
|
|
40
|
+
private readonly idSet;
|
|
41
|
+
private readonly byId;
|
|
42
|
+
readonly id: number;
|
|
43
|
+
/**
|
|
44
|
+
* The tick a row last moved in or out. Rows that move in keep their change ticks, so "nothing
|
|
45
|
+
* changed" needs this too.
|
|
46
|
+
*/
|
|
47
|
+
lastStructural: number;
|
|
48
|
+
readonly components: readonly ComponentDef[];
|
|
49
|
+
private readonly clock;
|
|
50
|
+
constructor(id: number, components: readonly ComponentDef[], clock: TickSource, capacity?: number);
|
|
51
|
+
has(def: ComponentDef): boolean;
|
|
52
|
+
hasId(id: number): boolean;
|
|
53
|
+
storage(def: ComponentDef): ComponentStorage | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* The raw column for one field. Numeric fields are TypedArrays with `stride` values per row
|
|
56
|
+
* (a vec3 at row `i` is at `[i * 3]`, `[i * 3 + 1]`, `[i * 3 + 2]`).
|
|
57
|
+
*/
|
|
58
|
+
column<F extends Fields, K extends keyof F & string>(def: ComponentDef<F>, field: K): ColumnOf<F[K]>;
|
|
59
|
+
addedTicks(def: ComponentDef): Uint32Array;
|
|
60
|
+
changedTicks(def: ComponentDef): Uint32Array;
|
|
61
|
+
/** Marks one row, or every row, as changed at the current tick. */
|
|
62
|
+
markChanged(def: ComponentDef, row?: number): void;
|
|
63
|
+
/**
|
|
64
|
+
* The newest change tick of any row of `def`: when it's not after `since`, no row changed. Code
|
|
65
|
+
* that writes `changedTicks()` directly must call `touch` too.
|
|
66
|
+
*/
|
|
67
|
+
lastChanged(def: ComponentDef): number;
|
|
68
|
+
/** Records that rows of `def` changed this tick, after writing `changedTicks()` directly. */
|
|
69
|
+
touch(def: ComponentDef): void;
|
|
70
|
+
isAdded(def: ComponentDef, row: number, since: number): boolean;
|
|
71
|
+
isChanged(def: ComponentDef, row: number, since: number): boolean;
|
|
72
|
+
pushRow(entity: Entity): number;
|
|
73
|
+
/** Swap-removes a row. Returns the entity moved into `row`, or -1 if it was the last row. */
|
|
74
|
+
removeRow(row: number): Entity;
|
|
75
|
+
/** Copies the components both tables share from `row` here into `targetRow` there. */
|
|
76
|
+
copySharedTo(row: number, target: Table, targetRow: number): void;
|
|
77
|
+
/** Writes a full component value; fields missing from `init` take their defaults. */
|
|
78
|
+
initComponent(def: ComponentDef, row: number, init: Record<string, unknown> | undefined): void;
|
|
79
|
+
/** Writes only the fields present in `values`. */
|
|
80
|
+
writeComponent(def: ComponentDef, row: number, values: Record<string, unknown>): void;
|
|
81
|
+
readComponent<F extends Fields>(def: ComponentDef<F>, row: number): InferFields<F>;
|
|
82
|
+
/**
|
|
83
|
+
* Replaces a component's definition with one sharing its id (hot reload). Without `migrate`
|
|
84
|
+
* the layout must be the same and only the definition changes; with it, every row's value is
|
|
85
|
+
* rebuilt into new columns. Added ticks carry over; changed ticks become the current tick.
|
|
86
|
+
*/
|
|
87
|
+
redefine(def: ComponentDef, migrate?: (value: Record<string, unknown>, row: number) => Record<string, unknown>): void;
|
|
88
|
+
/** Approximate bytes used by TypedArray columns (object columns count as 8 bytes a slot). */
|
|
89
|
+
bytes(): number;
|
|
90
|
+
private grow;
|
|
91
|
+
}
|