@driftengine/script 3.61.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +81 -0
- package/dist/bindings/ai.d.ts +66 -0
- package/dist/bindings/ai.js +173 -0
- package/dist/bindings/animation.d.ts +13 -0
- package/dist/bindings/animation.js +177 -0
- package/dist/bindings/audio.d.ts +56 -0
- package/dist/bindings/audio.js +126 -0
- package/dist/bindings/behavior.d.ts +36 -0
- package/dist/bindings/behavior.js +101 -0
- package/dist/bindings/chemistry.d.ts +72 -0
- package/dist/bindings/chemistry.js +253 -0
- package/dist/bindings/core.d.ts +172 -0
- package/dist/bindings/core.js +814 -0
- package/dist/bindings/editor.d.ts +53 -0
- package/dist/bindings/editor.js +248 -0
- package/dist/bindings/entities.d.ts +73 -0
- package/dist/bindings/entities.js +339 -0
- package/dist/bindings/interface.d.ts +55 -0
- package/dist/bindings/interface.js +200 -0
- package/dist/bindings/navigation.d.ts +36 -0
- package/dist/bindings/navigation.js +255 -0
- package/dist/bindings/network.d.ts +51 -0
- package/dist/bindings/network.js +96 -0
- package/dist/bindings/render.d.ts +71 -0
- package/dist/bindings/render.js +143 -0
- package/dist/bindings/rollback.d.ts +41 -0
- package/dist/bindings/rollback.js +74 -0
- package/dist/bindings/sprites.d.ts +53 -0
- package/dist/bindings/sprites.js +186 -0
- package/dist/bindings/terrain.d.ts +54 -0
- package/dist/bindings/terrain.js +138 -0
- package/dist/bindings/xr.d.ts +56 -0
- package/dist/bindings/xr.js +155 -0
- package/dist/entityHost.d.ts +80 -0
- package/dist/entityHost.js +159 -0
- package/dist/host.d.ts +153 -0
- package/dist/host.js +233 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +18 -0
- package/package.json +68 -0
- package/src/bindings/ai.ts +306 -0
- package/src/bindings/animation.ts +346 -0
- package/src/bindings/audio.ts +185 -0
- package/src/bindings/behavior.ts +181 -0
- package/src/bindings/chemistry.ts +548 -0
- package/src/bindings/core.ts +1604 -0
- package/src/bindings/editor.ts +393 -0
- package/src/bindings/entities.ts +550 -0
- package/src/bindings/interface.ts +300 -0
- package/src/bindings/navigation.ts +367 -0
- package/src/bindings/network.ts +178 -0
- package/src/bindings/render.ts +191 -0
- package/src/bindings/rollback.ts +110 -0
- package/src/bindings/sprites.ts +310 -0
- package/src/bindings/terrain.ts +180 -0
- package/src/bindings/xr.ts +272 -0
- package/src/entityHost.ts +219 -0
- package/src/host.ts +465 -0
- package/src/index.ts +41 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `drift/xr` — what a script may ask about a headset, and what it may not.
|
|
3
|
+
*
|
|
4
|
+
* **The last surface the linker refused, and it refused it since 1.0.** `drift/xr` has been in the
|
|
5
|
+
* language's `SPECIFIED_MODULES` the whole time with nothing here describing it, which is exactly
|
|
6
|
+
* the arrangement the language exists to make possible: a file naming it parsed, type-checked and
|
|
7
|
+
* declined only at link time, and the same file links now without a character changing.
|
|
8
|
+
*
|
|
9
|
+
* ## Nothing here is deterministic, and that is the property
|
|
10
|
+
*
|
|
11
|
+
* A head moves because somebody moved their head. A controller reports where it is because a camera
|
|
12
|
+
* saw it. None of that is a function of the simulation, so none of it may be read from inside the
|
|
13
|
+
* fixed step: a `@deterministic` system branching on where a hand is would take the other branch on
|
|
14
|
+
* replay, and a stored run would stop meaning anything.
|
|
15
|
+
*
|
|
16
|
+
* `drift/ui` settled the same question for a pointer, and the reasoning transfers unchanged: its
|
|
17
|
+
* `hovered` and `pressed` are fields on a tree that `scene.read` would have type-checked and would
|
|
18
|
+
* have been a lie about what they carry, so they were declared `input.read` and left outside
|
|
19
|
+
* `DETERMINISTIC_EFFECTS`. A head and a hand are the same kind of fact arriving through the same
|
|
20
|
+
* door.
|
|
21
|
+
*
|
|
22
|
+
* ## No matrices, and the argument is `drift/render`'s
|
|
23
|
+
*
|
|
24
|
+
* There is no `xr.projection` and no `xr.viewMatrix`. An eye's projection is a fact about somebody's
|
|
25
|
+
* optics, and its view is a fact about where their head is in a room the script has never seen;
|
|
26
|
+
* handing either to a script would be `drift/render`'s refused dial read in a different costume, a
|
|
27
|
+
* machine-dependent number registered where a script could compare two runs with it.
|
|
28
|
+
*
|
|
29
|
+
* What a script gets is where the head is, whether a session is running, and what the hands are
|
|
30
|
+
* doing. Drawing the two eyes is the host's, because the host owns the renderer and the loop.
|
|
31
|
+
*
|
|
32
|
+
* ## `presenting` is `scene.read` and the rest is `input.read`
|
|
33
|
+
*
|
|
34
|
+
* Whether a session is running is a fact about the view: it decides what is drawn and where, and it
|
|
35
|
+
* changes when somebody puts a headset on. `scene.read` sits outside `DETERMINISTIC_EFFECTS` for
|
|
36
|
+
* that reason, which is the one this needs.
|
|
37
|
+
*/
|
|
38
|
+
import { defineCapability } from 'driftscript';
|
|
39
|
+
import { JOINT_INDEX } from '@driftengine/xr';
|
|
40
|
+
export const XR_MODULE = 'drift/xr';
|
|
41
|
+
export const XR_TYPES = [];
|
|
42
|
+
const define = (name, params, returns, effects, doc, implementation) => defineCapability({
|
|
43
|
+
module: XR_MODULE,
|
|
44
|
+
name,
|
|
45
|
+
signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
|
|
46
|
+
params: [...params],
|
|
47
|
+
returns,
|
|
48
|
+
effects,
|
|
49
|
+
/*
|
|
50
|
+
* Never deterministic, on every capability here. See the header: a head moves because a person
|
|
51
|
+
* moved, and a `@deterministic` system reading one would take a different branch on replay.
|
|
52
|
+
*/
|
|
53
|
+
deterministic: false,
|
|
54
|
+
doc,
|
|
55
|
+
implementation,
|
|
56
|
+
});
|
|
57
|
+
/** `left`, `right`, or anything else for the source with no handedness. */
|
|
58
|
+
const HAND = { name: 'hand', type: 'String' };
|
|
59
|
+
export const XR_CAPABILITIES = [
|
|
60
|
+
define('presenting', [], 'bool', ['scene.read'], 'Whether a session is running. False before one starts and after the user takes the headset off.', 'XrRuntime.presenting'),
|
|
61
|
+
define('supported', [], 'bool', ['scene.read'], 'Whether this device could present at all. False on a machine with no headset attached.', 'XrRuntime.supported'),
|
|
62
|
+
/*
|
|
63
|
+
* The head, as three reads rather than one call returning a triple.
|
|
64
|
+
*
|
|
65
|
+
* The language has no tuple and no out parameter, and a `data` record would be a value the host
|
|
66
|
+
* allocates per call inside a frame loop. Three floats is the shape every other position on this
|
|
67
|
+
* surface takes, and `drift/navigation`'s `steerX`/`steerY`/`steerZ` set the precedent.
|
|
68
|
+
*/
|
|
69
|
+
/*
|
|
70
|
+
* `f32` and not `float`. The language refuses a width-polymorphic return with no `float`
|
|
71
|
+
* parameter to fix it against, which `drift/navigation.steerX` never meets because it takes the
|
|
72
|
+
* position it steers from. These take a hand or nothing, so the width is stated: every position
|
|
73
|
+
* this engine stores is a `Float32Array`, and answering `f64` would widen a number that was
|
|
74
|
+
* never that precise.
|
|
75
|
+
*/
|
|
76
|
+
define('headX', [], 'f32', ['input.read'], 'Where the viewer’s head is, in metres. Zero outside a session.', 'XrRuntime.headX'),
|
|
77
|
+
define('headY', [], 'f32', ['input.read'], 'Where the viewer’s head is, in metres. Zero outside a session.', 'XrRuntime.headY'),
|
|
78
|
+
define('headZ', [], 'f32', ['input.read'], 'Where the viewer’s head is, in metres. Zero outside a session.', 'XrRuntime.headZ'),
|
|
79
|
+
define('trigger', [HAND], 'f32', ['input.read'], 'How far a hand’s trigger is pulled, 0 to 1. Zero for a hand that is not there.', 'XrRuntime.trigger'),
|
|
80
|
+
define('squeeze', [HAND], 'f32', ['input.read'], 'How far a hand’s grip is squeezed, 0 to 1. Zero for a hand that is not there.', 'XrRuntime.squeeze'),
|
|
81
|
+
define('holding', [HAND], 'bool', ['input.read'], 'Whether a hand is tracked this frame. False while a controller is set down or out of view.', 'XrRuntime.holding'),
|
|
82
|
+
/*
|
|
83
|
+
* A joint by name and not by index.
|
|
84
|
+
*
|
|
85
|
+
* `HAND_JOINTS` fixes an order and a script could be handed it, and should not be: an index is a
|
|
86
|
+
* number a script would have to keep its own copy of and watch drift against a specification.
|
|
87
|
+
* A name is checked at the call and answers false for one nobody has heard of.
|
|
88
|
+
*/
|
|
89
|
+
define('jointX', [HAND, { name: 'joint', type: 'String' }], 'f32', ['input.read'], 'Where a named hand joint is, in metres. Zero for a joint that is not tracked.', 'XrRuntime.jointX'),
|
|
90
|
+
define('jointY', [HAND, { name: 'joint', type: 'String' }], 'f32', ['input.read'], 'Where a named hand joint is, in metres. Zero for a joint that is not tracked.', 'XrRuntime.jointY'),
|
|
91
|
+
define('jointZ', [HAND, { name: 'joint', type: 'String' }], 'f32', ['input.read'], 'Where a named hand joint is, in metres. Zero for a joint that is not tracked.', 'XrRuntime.jointZ'),
|
|
92
|
+
define('pinching', [HAND], 'bool', ['input.read'], 'Whether a hand’s thumb and index tips are close enough to count as a pinch.', 'XrRuntime.pinching'),
|
|
93
|
+
];
|
|
94
|
+
/** How close two fingertips count as a pinch, in metres. */
|
|
95
|
+
const PINCH_METRES = 0.025;
|
|
96
|
+
function handOf(value) {
|
|
97
|
+
return value === 'left' ? 'left' : value === 'right' ? 'right' : 'none';
|
|
98
|
+
}
|
|
99
|
+
export function xrImplementation(runtime) {
|
|
100
|
+
const at = new Float32Array(3);
|
|
101
|
+
const other = new Float32Array(3);
|
|
102
|
+
const head = (axis) => runtime.headPosition?.[axis] ?? 0;
|
|
103
|
+
const joint = (hand, name, axis) => {
|
|
104
|
+
const skeleton = runtime.hand(handOf(hand));
|
|
105
|
+
if (skeleton === null)
|
|
106
|
+
return 0;
|
|
107
|
+
/* The flat arrays the package already fills are read directly. `jointPosition` would answer the
|
|
108
|
+
same question through a `Float32Array` this would then have to unpack, three times per joint
|
|
109
|
+
per frame. */
|
|
110
|
+
const index = jointIndexOf(name);
|
|
111
|
+
if (index < 0 || skeleton.tracked[index] !== 1)
|
|
112
|
+
return 0;
|
|
113
|
+
return skeleton.matrices[index * 16 + 12 + axis] ?? 0;
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
presenting: () => runtime.headPosition !== null,
|
|
117
|
+
supported: () => runtime.support?.immersiveVr === true || runtime.support?.immersiveAr === true,
|
|
118
|
+
headX: () => head(0),
|
|
119
|
+
headY: () => head(1),
|
|
120
|
+
headZ: () => head(2),
|
|
121
|
+
trigger: (hand) => runtime.controller(handOf(hand))?.buttons[0] ?? 0,
|
|
122
|
+
squeeze: (hand) => runtime.controller(handOf(hand))?.buttons[1] ?? 0,
|
|
123
|
+
holding: (hand) => runtime.controller(handOf(hand))?.tracked ?? false,
|
|
124
|
+
jointX: (hand, name) => joint(hand, name, 0),
|
|
125
|
+
jointY: (hand, name) => joint(hand, name, 1),
|
|
126
|
+
jointZ: (hand, name) => joint(hand, name, 2),
|
|
127
|
+
pinching: (hand) => {
|
|
128
|
+
const skeleton = runtime.hand(handOf(hand));
|
|
129
|
+
if (skeleton === null)
|
|
130
|
+
return false;
|
|
131
|
+
const thumb = jointIndexOf('thumb-tip');
|
|
132
|
+
const index = jointIndexOf('index-finger-tip');
|
|
133
|
+
if (skeleton.tracked[thumb] !== 1 || skeleton.tracked[index] !== 1)
|
|
134
|
+
return false;
|
|
135
|
+
for (let i = 0; i < 3; i++) {
|
|
136
|
+
at[i] = skeleton.matrices[thumb * 16 + 12 + i] ?? 0;
|
|
137
|
+
other[i] = skeleton.matrices[index * 16 + 12 + i] ?? 0;
|
|
138
|
+
}
|
|
139
|
+
const dx = at[0] - other[0];
|
|
140
|
+
const dy = at[1] - other[1];
|
|
141
|
+
const dz = at[2] - other[2];
|
|
142
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz) <= PINCH_METRES;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* A joint's index by name.
|
|
148
|
+
*
|
|
149
|
+
* **Read from `@driftengine/xr` rather than written again here.** That package owns the order, an
|
|
150
|
+
* index is written into recordings, and a second copy of the list would be a second answer to what
|
|
151
|
+
* a stored hand means the first time somebody added a joint to one of them.
|
|
152
|
+
*/
|
|
153
|
+
function jointIndexOf(name) {
|
|
154
|
+
return JOINT_INDEX[name] ?? -1;
|
|
155
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building a world from what a module declares.
|
|
3
|
+
*
|
|
4
|
+
* A compiled `.drs` file carries its components, entities, systems and prefabs as **description**
|
|
5
|
+
* rather than code. This turns that description into the engine's own objects: a `ComponentType`
|
|
6
|
+
* per component, a `Prefab` per prefab, and a `SystemDefinition` per system.
|
|
7
|
+
*
|
|
8
|
+
* ## A system definition calls through `exports`, and that is what makes it hot-reloadable
|
|
9
|
+
*
|
|
10
|
+
* `runSchedule` caches one `BoundSystem` per world and definition, and calls `definition.run(view)`.
|
|
11
|
+
* A definition holding the generated function *directly* would pin the pre-patch body in the
|
|
12
|
+
* schedule forever — and the symptom is an edit that compiles, reloads, reports success and changes
|
|
13
|
+
* nothing, which is the one failure hot reload exists to prevent.
|
|
14
|
+
*
|
|
15
|
+
* So `run` reaches the function through `module.exports` on every tick. That is one property lookup
|
|
16
|
+
* per system per tick, which is the same bargain `runtime/module.ts` documents at its own
|
|
17
|
+
* declaration and the reason that indirection exists at all.
|
|
18
|
+
*
|
|
19
|
+
* ## A host-declared component is asserted, never created
|
|
20
|
+
*
|
|
21
|
+
* `component Transform from host { … }` says the host registered one and this is its shape. The
|
|
22
|
+
* module creates nothing for it; it is checked when the module binds, and a mismatch is refused
|
|
23
|
+
* naming the field and both types. **The refusal is the whole value of the declaration** — without
|
|
24
|
+
* it the two descriptions drift, and the first thing anybody notices is a field reading zeroes.
|
|
25
|
+
*
|
|
26
|
+
* ## A system's resources are resolved here, once, and `@driftengine/entities` never learns about them
|
|
27
|
+
*
|
|
28
|
+
* `system Walk { uses graph: NavGraph … }` asks to be handed a value this host owns. Without it a
|
|
29
|
+
* handle reaches a script only as a *function* parameter, and every per-entity walk has to be
|
|
30
|
+
* driven from TypeScript. The generated function takes a second parameter: a table keyed by **type
|
|
31
|
+
* name**, so a host supplies "the `NavGraph` this world has" and never learns what any script
|
|
32
|
+
* called its binding.
|
|
33
|
+
*
|
|
34
|
+
* The table is built at registration and captured, rather than assembled per tick. A resource does
|
|
35
|
+
* not change between ticks, and an object per system per tick is the allocation this package refuses
|
|
36
|
+
* everywhere else.
|
|
37
|
+
*
|
|
38
|
+
* **A missing resource is refused at registration, not left as `undefined`.** Passing nothing would
|
|
39
|
+
* reach a capability as an absent argument and produce a wrong *result* rather than an error, which
|
|
40
|
+
* is the trap `host.ts` step four is about. Init is where a person is watching.
|
|
41
|
+
*
|
|
42
|
+
* `SystemDefinition` gains nothing for this. A resource is a host's object, and the ECS has no
|
|
43
|
+
* business knowing one exists — so the table lives in the closure that already reaches through
|
|
44
|
+
* `module.exports`.
|
|
45
|
+
*/
|
|
46
|
+
import { type ComponentType, type Prefab, type SystemDefinition } from '@driftengine/entities';
|
|
47
|
+
import type { DriftModule } from 'driftscript';
|
|
48
|
+
/** Components a host already registered, by the name a script writes. */
|
|
49
|
+
export type ComponentRegistry = Map<string, ComponentType>;
|
|
50
|
+
/**
|
|
51
|
+
* Values a host lends to systems, by the **type name** a script writes in a `uses` clause.
|
|
52
|
+
*
|
|
53
|
+
* By type rather than by binding name, which is the language's own rule: a resource is one per type,
|
|
54
|
+
* so two systems asking for a `NavGraph` are handed the same object whatever each file calls it, and
|
|
55
|
+
* a script renaming its binding changes nothing here.
|
|
56
|
+
*/
|
|
57
|
+
export type ResourceRegistry = ReadonlyMap<string, unknown>;
|
|
58
|
+
export interface RegisteredModule {
|
|
59
|
+
/** Component types this module declared, in declaration order. Host-asserted ones are absent. */
|
|
60
|
+
readonly components: readonly ComponentType[];
|
|
61
|
+
readonly prefabs: readonly Prefab[];
|
|
62
|
+
/** Ready for `buildSchedule`, in declaration order. */
|
|
63
|
+
readonly systems: readonly SystemDefinition[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check every `from host` declaration against what the host registered.
|
|
67
|
+
*
|
|
68
|
+
* Throws on the first mismatch rather than collecting: a shape disagreement means the module was
|
|
69
|
+
* compiled against a different world than the one it is being loaded into, and the second error
|
|
70
|
+
* would be a consequence of the first.
|
|
71
|
+
*/
|
|
72
|
+
export declare function assertHostShapes(module: DriftModule, registry: ComponentRegistry): void;
|
|
73
|
+
/**
|
|
74
|
+
* Build every component, prefab and system a module declares.
|
|
75
|
+
*
|
|
76
|
+
* Components are registered into `registry` as they are made, so a prefab or a system later in the
|
|
77
|
+
* same module finds them — and so a second module loaded against the same registry shares them
|
|
78
|
+
* rather than making a second store for one name.
|
|
79
|
+
*/
|
|
80
|
+
export declare function registerEntityModule(module: DriftModule, registry: ComponentRegistry, resources?: ResourceRegistry): RegisteredModule;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building a world from what a module declares.
|
|
3
|
+
*
|
|
4
|
+
* A compiled `.drs` file carries its components, entities, systems and prefabs as **description**
|
|
5
|
+
* rather than code. This turns that description into the engine's own objects: a `ComponentType`
|
|
6
|
+
* per component, a `Prefab` per prefab, and a `SystemDefinition` per system.
|
|
7
|
+
*
|
|
8
|
+
* ## A system definition calls through `exports`, and that is what makes it hot-reloadable
|
|
9
|
+
*
|
|
10
|
+
* `runSchedule` caches one `BoundSystem` per world and definition, and calls `definition.run(view)`.
|
|
11
|
+
* A definition holding the generated function *directly* would pin the pre-patch body in the
|
|
12
|
+
* schedule forever — and the symptom is an edit that compiles, reloads, reports success and changes
|
|
13
|
+
* nothing, which is the one failure hot reload exists to prevent.
|
|
14
|
+
*
|
|
15
|
+
* So `run` reaches the function through `module.exports` on every tick. That is one property lookup
|
|
16
|
+
* per system per tick, which is the same bargain `runtime/module.ts` documents at its own
|
|
17
|
+
* declaration and the reason that indirection exists at all.
|
|
18
|
+
*
|
|
19
|
+
* ## A host-declared component is asserted, never created
|
|
20
|
+
*
|
|
21
|
+
* `component Transform from host { … }` says the host registered one and this is its shape. The
|
|
22
|
+
* module creates nothing for it; it is checked when the module binds, and a mismatch is refused
|
|
23
|
+
* naming the field and both types. **The refusal is the whole value of the declaration** — without
|
|
24
|
+
* it the two descriptions drift, and the first thing anybody notices is a field reading zeroes.
|
|
25
|
+
*
|
|
26
|
+
* ## A system's resources are resolved here, once, and `@driftengine/entities` never learns about them
|
|
27
|
+
*
|
|
28
|
+
* `system Walk { uses graph: NavGraph … }` asks to be handed a value this host owns. Without it a
|
|
29
|
+
* handle reaches a script only as a *function* parameter, and every per-entity walk has to be
|
|
30
|
+
* driven from TypeScript. The generated function takes a second parameter: a table keyed by **type
|
|
31
|
+
* name**, so a host supplies "the `NavGraph` this world has" and never learns what any script
|
|
32
|
+
* called its binding.
|
|
33
|
+
*
|
|
34
|
+
* The table is built at registration and captured, rather than assembled per tick. A resource does
|
|
35
|
+
* not change between ticks, and an object per system per tick is the allocation this package refuses
|
|
36
|
+
* everywhere else.
|
|
37
|
+
*
|
|
38
|
+
* **A missing resource is refused at registration, not left as `undefined`.** Passing nothing would
|
|
39
|
+
* reach a capability as an absent argument and produce a wrong *result* rather than an error, which
|
|
40
|
+
* is the trap `host.ts` step four is about. Init is where a person is watching.
|
|
41
|
+
*
|
|
42
|
+
* `SystemDefinition` gains nothing for this. A resource is a host's object, and the ECS has no
|
|
43
|
+
* business knowing one exists — so the table lives in the closure that already reaches through
|
|
44
|
+
* `module.exports`.
|
|
45
|
+
*/
|
|
46
|
+
import { defineComponent, definePrefab, } from '@driftengine/entities';
|
|
47
|
+
/**
|
|
48
|
+
* Check every `from host` declaration against what the host registered.
|
|
49
|
+
*
|
|
50
|
+
* Throws on the first mismatch rather than collecting: a shape disagreement means the module was
|
|
51
|
+
* compiled against a different world than the one it is being loaded into, and the second error
|
|
52
|
+
* would be a consequence of the first.
|
|
53
|
+
*/
|
|
54
|
+
export function assertHostShapes(module, registry) {
|
|
55
|
+
for (const declared of module.info.components ?? []) {
|
|
56
|
+
if (!declared.fromHost)
|
|
57
|
+
continue;
|
|
58
|
+
const registered = registry.get(declared.name);
|
|
59
|
+
if (registered === undefined) {
|
|
60
|
+
throw new Error(`\`${module.info.module}\` declares \`component ${declared.name} from host\`, and this host ` +
|
|
61
|
+
`registered no component of that name. It has ` +
|
|
62
|
+
`${[...registry.keys()].map((k) => `\`${k}\``).join(', ')}. A \`from host\` declaration ` +
|
|
63
|
+
'asserts a shape rather than creating one, so there is nothing to fall back to.');
|
|
64
|
+
}
|
|
65
|
+
const actual = new Map(registered.schema.fields.map((field) => [field.name, field.type]));
|
|
66
|
+
for (const field of declared.schema.fields) {
|
|
67
|
+
const found = actual.get(field.name);
|
|
68
|
+
if (found === undefined) {
|
|
69
|
+
throw new Error(`\`${declared.name}.${field.name}\` is declared in \`${module.info.module}\` and the ` +
|
|
70
|
+
`registered component has no such field. It has ` +
|
|
71
|
+
`${[...actual.keys()].map((k) => `\`${k}\``).join(', ')}.`);
|
|
72
|
+
}
|
|
73
|
+
if (found !== field.type) {
|
|
74
|
+
throw new Error(`\`${declared.name}.${field.name}\` is \`${field.type}\` in \`${module.info.module}\` and ` +
|
|
75
|
+
`\`${found}\` in the component this host registered. A script reading it would get a ` +
|
|
76
|
+
'value of a type it was not compiled against.');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build every component, prefab and system a module declares.
|
|
83
|
+
*
|
|
84
|
+
* Components are registered into `registry` as they are made, so a prefab or a system later in the
|
|
85
|
+
* same module finds them — and so a second module loaded against the same registry shares them
|
|
86
|
+
* rather than making a second store for one name.
|
|
87
|
+
*/
|
|
88
|
+
export function registerEntityModule(module, registry, resources = new Map()) {
|
|
89
|
+
assertHostShapes(module, registry);
|
|
90
|
+
const components = [];
|
|
91
|
+
for (const declared of module.info.components ?? []) {
|
|
92
|
+
/* A host-declared component is already registered; asserting its shape is all this does. */
|
|
93
|
+
if (declared.fromHost)
|
|
94
|
+
continue;
|
|
95
|
+
const existing = registry.get(declared.name);
|
|
96
|
+
/* A module reloaded keeps the store it already made: replacing the type would orphan every
|
|
97
|
+
entity holding one, which is a world that silently loses its components on an edit. */
|
|
98
|
+
const type = existing ?? defineComponent(declared.schema);
|
|
99
|
+
registry.set(declared.name, type);
|
|
100
|
+
components.push(type);
|
|
101
|
+
}
|
|
102
|
+
const prefabs = (module.info.prefabs ?? []).map((declared) => definePrefab(declared.name, declared.components.map((component) => {
|
|
103
|
+
const type = registry.get(component.name);
|
|
104
|
+
if (type === undefined) {
|
|
105
|
+
throw new Error(`prefab \`${declared.name}\` names the component \`${component.name}\`, which nothing ` +
|
|
106
|
+
'has registered. The compiler refuses this, so a module reaching here was built ' +
|
|
107
|
+
'against a different set of declarations than it is being loaded with.');
|
|
108
|
+
}
|
|
109
|
+
return [type, component.values];
|
|
110
|
+
})));
|
|
111
|
+
const systems = (module.info.systems ?? []).map((declared) => {
|
|
112
|
+
const resolve = (names) => names.map((name) => {
|
|
113
|
+
const type = registry.get(name);
|
|
114
|
+
if (type === undefined) {
|
|
115
|
+
throw new Error(`system \`${declared.name}\` declares \`${name}\`, which nothing has registered.`);
|
|
116
|
+
}
|
|
117
|
+
return type;
|
|
118
|
+
});
|
|
119
|
+
/*
|
|
120
|
+
* Built once and frozen, so a system cannot acquire a resource by writing into the table it was
|
|
121
|
+
* handed — which would be state shared between ticks that nothing declares.
|
|
122
|
+
*/
|
|
123
|
+
const lent = {};
|
|
124
|
+
for (const resource of declared.uses ?? []) {
|
|
125
|
+
if (!resources.has(resource.type)) {
|
|
126
|
+
throw new Error(`system \`${declared.name}\` uses \`${resource.name}: ${resource.type}\`, and no ` +
|
|
127
|
+
`\`${resource.type}\` was supplied. Pass one in the third argument to ` +
|
|
128
|
+
'`registerEntityModule`, keyed by the type name. Refused here rather than passed as ' +
|
|
129
|
+
'nothing, because an absent argument reaches a capability as a wrong answer.');
|
|
130
|
+
}
|
|
131
|
+
lent[resource.type] = resources.get(resource.type);
|
|
132
|
+
}
|
|
133
|
+
Object.freeze(lent);
|
|
134
|
+
return {
|
|
135
|
+
name: declared.name,
|
|
136
|
+
reads: resolve(declared.reads),
|
|
137
|
+
writes: resolve(declared.writes),
|
|
138
|
+
after: declared.after,
|
|
139
|
+
everyTicks: declared.everyTicks,
|
|
140
|
+
/*
|
|
141
|
+
* Through `exports` on every tick, never captured here. See the header: a captured function
|
|
142
|
+
* is pinned in the schedule across a hot patch, and the symptom is an edit that reports
|
|
143
|
+
* success and changes nothing.
|
|
144
|
+
*/
|
|
145
|
+
run(view) {
|
|
146
|
+
const fn = module.exports[declared.name];
|
|
147
|
+
if (typeof fn !== 'function') {
|
|
148
|
+
throw new Error(`\`${declared.name}\` is not exported by \`${module.info.module}\` any more. A disposed ` +
|
|
149
|
+
'module left in a schedule fails here rather than silently doing nothing.');
|
|
150
|
+
}
|
|
151
|
+
/* The second argument is passed whether or not this system asked for anything. A generated
|
|
152
|
+
function without `uses` clauses declares one parameter and ignores it, which is what makes
|
|
153
|
+
the quiet form quiet in the compiler and costs nothing here. */
|
|
154
|
+
fn(view, lent);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
return { components, prefabs, systems };
|
|
159
|
+
}
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine as a DriftScript host: a registry describing what it provides, and the map that binds
|
|
3
|
+
* a compiled module to the real implementations.
|
|
4
|
+
*
|
|
5
|
+
* **This package is the only place the language and the engine are coupled**, and this file is the
|
|
6
|
+
* only place in it that knows about both at once. `driftscript` imports no `@driftengine/*` at all,
|
|
7
|
+
* asserted three ways; everything the engine contributes arrives through here.
|
|
8
|
+
*
|
|
9
|
+
* ---
|
|
10
|
+
*
|
|
11
|
+
* ## Adding a binding when a track lands
|
|
12
|
+
*
|
|
13
|
+
* **A capability that exists and is not bound is a capability DriftScript cannot reach**, and
|
|
14
|
+
* nothing fails when one is missed — a script author simply finds the module refused and assumes
|
|
15
|
+
* the track has not shipped. That silence is why this note exists rather than a checklist somewhere
|
|
16
|
+
* else.
|
|
17
|
+
*
|
|
18
|
+
* When a track lands, or a package grows a public function a script would want:
|
|
19
|
+
*
|
|
20
|
+
* 1. **Describe it.** A `defineCapability` entry in `bindings/`: its parameters and return as type
|
|
21
|
+
* names, its effects, whether a `@deterministic` function may call it, and a sentence of
|
|
22
|
+
* documentation that hover will show.
|
|
23
|
+
* 2. **Implement it.** One entry in the module's implementation map. A binding is a *lookup*; if it
|
|
24
|
+
* needs the engine to grow a function, that function belongs in the package that owns the
|
|
25
|
+
* subsystem.
|
|
26
|
+
* 3. **Provide it.** Add the module to `ENGINE_MODULES`, or a target cannot link it.
|
|
27
|
+
* 4. **Check the trap.** If the TypeScript signature has an optional parameter whose absence
|
|
28
|
+
* produces a *wrong result* rather than an error, make it required here and say why — a script
|
|
29
|
+
* author is further from the subsystem than a TypeScript caller and meets it first.
|
|
30
|
+
* `bindings/animation.ts` is the worked example.
|
|
31
|
+
* 5. **Correct the table.** `docs/CAPABILITIES.md` names which modules DriftScript reaches, and
|
|
32
|
+
* `driftscript`'s own linker names which do not, and refuses them by name.
|
|
33
|
+
* Those two lists must agree; a test asserts it.
|
|
34
|
+
*
|
|
35
|
+
* A binding is cheap — a table entry and a lookup — and is the difference between a subsystem
|
|
36
|
+
* a consumer can script and one they can only call from TypeScript.
|
|
37
|
+
*/
|
|
38
|
+
import type { AudioGraph, SoundRegistry } from '@driftengine/audio';
|
|
39
|
+
import { type BehaviorServices } from './bindings/behavior.ts';
|
|
40
|
+
import { type NavigationServices } from './bindings/navigation.ts';
|
|
41
|
+
import { type CapabilityRegistry, type DriftModule, type TargetManifest } from 'driftscript';
|
|
42
|
+
import { type AiServices } from './bindings/ai.ts';
|
|
43
|
+
import { type XrRuntime } from './bindings/xr.ts';
|
|
44
|
+
import type { CoreServices } from './bindings/core.ts';
|
|
45
|
+
import { type EntityServices } from './bindings/entities.ts';
|
|
46
|
+
import { type ChemistryServices } from './bindings/chemistry.ts';
|
|
47
|
+
/**
|
|
48
|
+
* What the engine can provide. Everything wired; nothing that is not.
|
|
49
|
+
*
|
|
50
|
+
* `drift/core` is deliberately absent: §11 gives its provider as `startLoop` and `LoopHooks`, which
|
|
51
|
+
* are what *drives* a script rather than what a script calls. A module with nothing to call is not
|
|
52
|
+
* registered rather than registered empty.
|
|
53
|
+
*/
|
|
54
|
+
export declare const ENGINE_MODULES: readonly string[];
|
|
55
|
+
/**
|
|
56
|
+
* A registry describing this engine's capabilities.
|
|
57
|
+
*
|
|
58
|
+
* Built rather than exported as a constant, because a registry refuses a duplicate registration —
|
|
59
|
+
* two consumers sharing one module-level instance would be one consumer's startup failing on
|
|
60
|
+
* another's import. Cheap: a few dozen object literals.
|
|
61
|
+
*/
|
|
62
|
+
export declare function engineRegistry(): CapabilityRegistry;
|
|
63
|
+
/**
|
|
64
|
+
* A target providing everything this engine has wired.
|
|
65
|
+
*
|
|
66
|
+
* A consumer that wants less passes its own list. **A consumer that wants more cannot** — the
|
|
67
|
+
* manifest may only name modules something provides, so a target claiming `drift/ecs` is refused
|
|
68
|
+
* at the manifest rather than at every call site.
|
|
69
|
+
*/
|
|
70
|
+
export declare function engineTarget(name?: string): TargetManifest;
|
|
71
|
+
export interface HostServices extends CoreServices {
|
|
72
|
+
/**
|
|
73
|
+
* The agents a script may name, and what the two bridges answer for them.
|
|
74
|
+
*
|
|
75
|
+
* `agents` alone is a supported configuration: a host with no navigation graph and no session
|
|
76
|
+
* still gets `wake`, `consider`, `intentId` and `degraded`. The bridge sub-services are what
|
|
77
|
+
* `reachable`, `navigate`, `path` and `deciding` need, and a capability whose service is absent
|
|
78
|
+
* fails at the call saying which one to provide rather than answering falsely.
|
|
79
|
+
*/
|
|
80
|
+
readonly ai?: AiServices;
|
|
81
|
+
/**
|
|
82
|
+
* The live session, if a host has one.
|
|
83
|
+
*
|
|
84
|
+
* Absent means `drift/xr` calls fail at the call naming the namespace, which is the bargain every
|
|
85
|
+
* optional service here makes. A stub would be worse than usual: a script asking where a head is
|
|
86
|
+
* and getting the origin cannot tell a headset at the world origin from no headset at all, and
|
|
87
|
+
* one of those is a scene that draws and the other is a scene nobody is in.
|
|
88
|
+
*/
|
|
89
|
+
readonly xr?: XrRuntime;
|
|
90
|
+
readonly audio?: {
|
|
91
|
+
readonly graph: AudioGraph;
|
|
92
|
+
readonly registry: SoundRegistry;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* The component types, and any prefabs, a script may name.
|
|
96
|
+
*
|
|
97
|
+
* Absent means a script's `drift/ecs` calls fail at the call with the namespace's own name in the
|
|
98
|
+
* error — the same bargain every other optional service makes, and the reason none of them is
|
|
99
|
+
* stubbed.
|
|
100
|
+
*/
|
|
101
|
+
readonly entities?: EntityServices;
|
|
102
|
+
/**
|
|
103
|
+
* The chemistry world a script reads and writes.
|
|
104
|
+
*
|
|
105
|
+
* Absent means `drift/chemistry` calls fail at the call naming the namespace — the same bargain
|
|
106
|
+
* every other optional service makes, and the reason none of them is stubbed. A stub would make a
|
|
107
|
+
* script whose fire never lights indistinguishable from one whose chemistry is not connected.
|
|
108
|
+
*/
|
|
109
|
+
readonly chemistry?: ChemistryServices;
|
|
110
|
+
/**
|
|
111
|
+
* The network scripts path over: a graph, and one search reused across every agent.
|
|
112
|
+
*
|
|
113
|
+
* Absent means `drift/navigation` calls fail at the call naming the namespace, like every other
|
|
114
|
+
* optional service. A stub would be worse here than elsewhere: an agent given a route of no nodes
|
|
115
|
+
* behaves exactly like one that arrived, so a world whose navigation was never connected would
|
|
116
|
+
* look like a world where every agent is already where it wants to be.
|
|
117
|
+
*/
|
|
118
|
+
readonly navigation?: NavigationServices;
|
|
119
|
+
/**
|
|
120
|
+
* What a behaviour tick is handed.
|
|
121
|
+
*
|
|
122
|
+
* Absent means `drift/behavior` calls fail at the call naming the namespace. A stub is
|
|
123
|
+
* particularly bad here for the reason `doing` exists: an agent whose routine never runs looks
|
|
124
|
+
* exactly like one that is standing about because it has nothing to do.
|
|
125
|
+
*/
|
|
126
|
+
readonly behavior?: BehaviorServices;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The implementation map a generated module's `__bind` receives.
|
|
130
|
+
*
|
|
131
|
+
* Keyed by logical module name, because that is what generated code looks up: `audio` in a `.drs`
|
|
132
|
+
* file is `$host['drift/audio']` in the output.
|
|
133
|
+
*
|
|
134
|
+
* **A service the consumer did not supply is absent rather than stubbed.** A module that binds it
|
|
135
|
+
* and then calls it fails at the call with the namespace's own name in the error, which is what the
|
|
136
|
+
* rule against silent no-ops asks for — a stub that quietly did nothing would make a script that
|
|
137
|
+
* plays no sound indistinguishable from one whose audio is not connected.
|
|
138
|
+
*/
|
|
139
|
+
export declare function engineImplementations(services: HostServices): Record<string, unknown>;
|
|
140
|
+
export type BindResult = {
|
|
141
|
+
readonly bound: true;
|
|
142
|
+
} | {
|
|
143
|
+
readonly bound: false;
|
|
144
|
+
readonly reason: string;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Give a loaded module its host, and refuse in words when the host cannot satisfy it.
|
|
148
|
+
*
|
|
149
|
+
* Checked against `__drift.requires` before anything is called, so a missing service is reported at
|
|
150
|
+
* bind time naming the module — rather than at the first capability call, deep in a frame, as an
|
|
151
|
+
* undefined property.
|
|
152
|
+
*/
|
|
153
|
+
export declare function bindModule(module: DriftModule, services: HostServices): BindResult;
|