@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.
Files changed (61) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +81 -0
  4. package/dist/bindings/ai.d.ts +66 -0
  5. package/dist/bindings/ai.js +173 -0
  6. package/dist/bindings/animation.d.ts +13 -0
  7. package/dist/bindings/animation.js +177 -0
  8. package/dist/bindings/audio.d.ts +56 -0
  9. package/dist/bindings/audio.js +126 -0
  10. package/dist/bindings/behavior.d.ts +36 -0
  11. package/dist/bindings/behavior.js +101 -0
  12. package/dist/bindings/chemistry.d.ts +72 -0
  13. package/dist/bindings/chemistry.js +253 -0
  14. package/dist/bindings/core.d.ts +172 -0
  15. package/dist/bindings/core.js +814 -0
  16. package/dist/bindings/editor.d.ts +53 -0
  17. package/dist/bindings/editor.js +248 -0
  18. package/dist/bindings/entities.d.ts +73 -0
  19. package/dist/bindings/entities.js +339 -0
  20. package/dist/bindings/interface.d.ts +55 -0
  21. package/dist/bindings/interface.js +200 -0
  22. package/dist/bindings/navigation.d.ts +36 -0
  23. package/dist/bindings/navigation.js +255 -0
  24. package/dist/bindings/network.d.ts +51 -0
  25. package/dist/bindings/network.js +96 -0
  26. package/dist/bindings/render.d.ts +71 -0
  27. package/dist/bindings/render.js +143 -0
  28. package/dist/bindings/rollback.d.ts +41 -0
  29. package/dist/bindings/rollback.js +74 -0
  30. package/dist/bindings/sprites.d.ts +53 -0
  31. package/dist/bindings/sprites.js +186 -0
  32. package/dist/bindings/terrain.d.ts +54 -0
  33. package/dist/bindings/terrain.js +138 -0
  34. package/dist/bindings/xr.d.ts +56 -0
  35. package/dist/bindings/xr.js +155 -0
  36. package/dist/entityHost.d.ts +80 -0
  37. package/dist/entityHost.js +159 -0
  38. package/dist/host.d.ts +153 -0
  39. package/dist/host.js +233 -0
  40. package/dist/index.d.ts +21 -0
  41. package/dist/index.js +18 -0
  42. package/package.json +68 -0
  43. package/src/bindings/ai.ts +306 -0
  44. package/src/bindings/animation.ts +346 -0
  45. package/src/bindings/audio.ts +185 -0
  46. package/src/bindings/behavior.ts +181 -0
  47. package/src/bindings/chemistry.ts +548 -0
  48. package/src/bindings/core.ts +1604 -0
  49. package/src/bindings/editor.ts +393 -0
  50. package/src/bindings/entities.ts +550 -0
  51. package/src/bindings/interface.ts +300 -0
  52. package/src/bindings/navigation.ts +367 -0
  53. package/src/bindings/network.ts +178 -0
  54. package/src/bindings/render.ts +191 -0
  55. package/src/bindings/rollback.ts +110 -0
  56. package/src/bindings/sprites.ts +310 -0
  57. package/src/bindings/terrain.ts +180 -0
  58. package/src/bindings/xr.ts +272 -0
  59. package/src/entityHost.ts +219 -0
  60. package/src/host.ts +465 -0
  61. package/src/index.ts +41 -0
@@ -0,0 +1,272 @@
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
+
39
+ import { type CapabilityDefinition, type OpaqueType, defineCapability } from 'driftscript';
40
+ import { JOINT_INDEX } from '@driftengine/xr';
41
+ import type { ControllerState, HandSkeleton, Handedness, XrSupport } from '@driftengine/xr';
42
+
43
+ export const XR_MODULE = 'drift/xr';
44
+
45
+ export const XR_TYPES: readonly OpaqueType[] = [];
46
+
47
+ const define = (
48
+ name: string,
49
+ params: readonly { name: string; type: string }[],
50
+ returns: string,
51
+ effects: CapabilityDefinition['effects'],
52
+ doc: string,
53
+ implementation: string,
54
+ ): CapabilityDefinition =>
55
+ defineCapability({
56
+ module: XR_MODULE,
57
+ name,
58
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
59
+ params: [...params],
60
+ returns,
61
+ effects,
62
+ /*
63
+ * Never deterministic, on every capability here. See the header: a head moves because a person
64
+ * moved, and a `@deterministic` system reading one would take a different branch on replay.
65
+ */
66
+ deterministic: false,
67
+ doc,
68
+ implementation,
69
+ });
70
+
71
+ /** `left`, `right`, or anything else for the source with no handedness. */
72
+ const HAND = { name: 'hand', type: 'String' };
73
+
74
+ export const XR_CAPABILITIES: readonly CapabilityDefinition[] = [
75
+ define(
76
+ 'presenting',
77
+ [],
78
+ 'bool',
79
+ ['scene.read'],
80
+ 'Whether a session is running. False before one starts and after the user takes the headset off.',
81
+ 'XrRuntime.presenting',
82
+ ),
83
+ define(
84
+ 'supported',
85
+ [],
86
+ 'bool',
87
+ ['scene.read'],
88
+ 'Whether this device could present at all. False on a machine with no headset attached.',
89
+ 'XrRuntime.supported',
90
+ ),
91
+ /*
92
+ * The head, as three reads rather than one call returning a triple.
93
+ *
94
+ * The language has no tuple and no out parameter, and a `data` record would be a value the host
95
+ * allocates per call inside a frame loop. Three floats is the shape every other position on this
96
+ * surface takes, and `drift/navigation`'s `steerX`/`steerY`/`steerZ` set the precedent.
97
+ */
98
+ /*
99
+ * `f32` and not `float`. The language refuses a width-polymorphic return with no `float`
100
+ * parameter to fix it against, which `drift/navigation.steerX` never meets because it takes the
101
+ * position it steers from. These take a hand or nothing, so the width is stated: every position
102
+ * this engine stores is a `Float32Array`, and answering `f64` would widen a number that was
103
+ * never that precise.
104
+ */
105
+ define(
106
+ 'headX',
107
+ [],
108
+ 'f32',
109
+ ['input.read'],
110
+ 'Where the viewer’s head is, in metres. Zero outside a session.',
111
+ 'XrRuntime.headX',
112
+ ),
113
+ define(
114
+ 'headY',
115
+ [],
116
+ 'f32',
117
+ ['input.read'],
118
+ 'Where the viewer’s head is, in metres. Zero outside a session.',
119
+ 'XrRuntime.headY',
120
+ ),
121
+ define(
122
+ 'headZ',
123
+ [],
124
+ 'f32',
125
+ ['input.read'],
126
+ 'Where the viewer’s head is, in metres. Zero outside a session.',
127
+ 'XrRuntime.headZ',
128
+ ),
129
+ define(
130
+ 'trigger',
131
+ [HAND],
132
+ 'f32',
133
+ ['input.read'],
134
+ 'How far a hand’s trigger is pulled, 0 to 1. Zero for a hand that is not there.',
135
+ 'XrRuntime.trigger',
136
+ ),
137
+ define(
138
+ 'squeeze',
139
+ [HAND],
140
+ 'f32',
141
+ ['input.read'],
142
+ 'How far a hand’s grip is squeezed, 0 to 1. Zero for a hand that is not there.',
143
+ 'XrRuntime.squeeze',
144
+ ),
145
+ define(
146
+ 'holding',
147
+ [HAND],
148
+ 'bool',
149
+ ['input.read'],
150
+ 'Whether a hand is tracked this frame. False while a controller is set down or out of view.',
151
+ 'XrRuntime.holding',
152
+ ),
153
+ /*
154
+ * A joint by name and not by index.
155
+ *
156
+ * `HAND_JOINTS` fixes an order and a script could be handed it, and should not be: an index is a
157
+ * number a script would have to keep its own copy of and watch drift against a specification.
158
+ * A name is checked at the call and answers false for one nobody has heard of.
159
+ */
160
+ define(
161
+ 'jointX',
162
+ [HAND, { name: 'joint', type: 'String' }],
163
+ 'f32',
164
+ ['input.read'],
165
+ 'Where a named hand joint is, in metres. Zero for a joint that is not tracked.',
166
+ 'XrRuntime.jointX',
167
+ ),
168
+ define(
169
+ 'jointY',
170
+ [HAND, { name: 'joint', type: 'String' }],
171
+ 'f32',
172
+ ['input.read'],
173
+ 'Where a named hand joint is, in metres. Zero for a joint that is not tracked.',
174
+ 'XrRuntime.jointY',
175
+ ),
176
+ define(
177
+ 'jointZ',
178
+ [HAND, { name: 'joint', type: 'String' }],
179
+ 'f32',
180
+ ['input.read'],
181
+ 'Where a named hand joint is, in metres. Zero for a joint that is not tracked.',
182
+ 'XrRuntime.jointZ',
183
+ ),
184
+ define(
185
+ 'pinching',
186
+ [HAND],
187
+ 'bool',
188
+ ['input.read'],
189
+ 'Whether a hand’s thumb and index tips are close enough to count as a pinch.',
190
+ 'XrRuntime.pinching',
191
+ ),
192
+ ];
193
+
194
+ /**
195
+ * What the host keeps up to date, and what a script reads through.
196
+ *
197
+ * The consumer's, exactly as `AgentRegistry` and `SoundRegistry` are. This package owns neither the
198
+ * session nor the frame, and which of them is current is not something a binding can know.
199
+ */
200
+ export interface XrRuntime {
201
+ readonly support: XrSupport | null;
202
+ /** Null outside a session. */
203
+ readonly headPosition: Float32Array | null;
204
+ controller(hand: Handedness): ControllerState | null;
205
+ hand(hand: Handedness): HandSkeleton | null;
206
+ }
207
+
208
+ /** How close two fingertips count as a pinch, in metres. */
209
+ const PINCH_METRES = 0.025;
210
+
211
+ function handOf(value: string): Handedness {
212
+ return value === 'left' ? 'left' : value === 'right' ? 'right' : 'none';
213
+ }
214
+
215
+ export function xrImplementation(runtime: XrRuntime): Readonly<Record<string, unknown>> {
216
+ const at = new Float32Array(3);
217
+ const other = new Float32Array(3);
218
+
219
+ const head = (axis: number): number => runtime.headPosition?.[axis] ?? 0;
220
+
221
+ const joint = (hand: string, name: string, axis: number): number => {
222
+ const skeleton = runtime.hand(handOf(hand));
223
+ if (skeleton === null) return 0;
224
+ /* The flat arrays the package already fills are read directly. `jointPosition` would answer the
225
+ same question through a `Float32Array` this would then have to unpack, three times per joint
226
+ per frame. */
227
+ const index = jointIndexOf(name);
228
+ if (index < 0 || skeleton.tracked[index] !== 1) return 0;
229
+ return skeleton.matrices[index * 16 + 12 + axis] ?? 0;
230
+ };
231
+
232
+ return {
233
+ presenting: (): boolean => runtime.headPosition !== null,
234
+ supported: (): boolean =>
235
+ runtime.support?.immersiveVr === true || runtime.support?.immersiveAr === true,
236
+ headX: (): number => head(0),
237
+ headY: (): number => head(1),
238
+ headZ: (): number => head(2),
239
+ trigger: (hand: string): number => runtime.controller(handOf(hand))?.buttons[0] ?? 0,
240
+ squeeze: (hand: string): number => runtime.controller(handOf(hand))?.buttons[1] ?? 0,
241
+ holding: (hand: string): boolean => runtime.controller(handOf(hand))?.tracked ?? false,
242
+ jointX: (hand: string, name: string): number => joint(hand, name, 0),
243
+ jointY: (hand: string, name: string): number => joint(hand, name, 1),
244
+ jointZ: (hand: string, name: string): number => joint(hand, name, 2),
245
+ pinching: (hand: string): boolean => {
246
+ const skeleton = runtime.hand(handOf(hand));
247
+ if (skeleton === null) return false;
248
+ const thumb = jointIndexOf('thumb-tip');
249
+ const index = jointIndexOf('index-finger-tip');
250
+ if (skeleton.tracked[thumb] !== 1 || skeleton.tracked[index] !== 1) return false;
251
+ for (let i = 0; i < 3; i++) {
252
+ at[i] = skeleton.matrices[thumb * 16 + 12 + i] ?? 0;
253
+ other[i] = skeleton.matrices[index * 16 + 12 + i] ?? 0;
254
+ }
255
+ const dx = (at[0] as number) - (other[0] as number);
256
+ const dy = (at[1] as number) - (other[1] as number);
257
+ const dz = (at[2] as number) - (other[2] as number);
258
+ return Math.sqrt(dx * dx + dy * dy + dz * dz) <= PINCH_METRES;
259
+ },
260
+ };
261
+ }
262
+
263
+ /**
264
+ * A joint's index by name.
265
+ *
266
+ * **Read from `@driftengine/xr` rather than written again here.** That package owns the order, an
267
+ * index is written into recordings, and a second copy of the list would be a second answer to what
268
+ * a stored hand means the first time somebody added a joint to one of them.
269
+ */
270
+ function jointIndexOf(name: string): number {
271
+ return JOINT_INDEX[name] ?? -1;
272
+ }
@@ -0,0 +1,219 @@
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 {
47
+ type ComponentType,
48
+ type Prefab,
49
+ type SystemDefinition,
50
+ type SystemView,
51
+ defineComponent,
52
+ definePrefab,
53
+ } from '@driftengine/entities';
54
+ import type { DriftModule } from 'driftscript';
55
+
56
+ /** Components a host already registered, by the name a script writes. */
57
+ export type ComponentRegistry = Map<string, ComponentType>;
58
+
59
+ /**
60
+ * Values a host lends to systems, by the **type name** a script writes in a `uses` clause.
61
+ *
62
+ * By type rather than by binding name, which is the language's own rule: a resource is one per type,
63
+ * so two systems asking for a `NavGraph` are handed the same object whatever each file calls it, and
64
+ * a script renaming its binding changes nothing here.
65
+ */
66
+ export type ResourceRegistry = ReadonlyMap<string, unknown>;
67
+
68
+ export interface RegisteredModule {
69
+ /** Component types this module declared, in declaration order. Host-asserted ones are absent. */
70
+ readonly components: readonly ComponentType[];
71
+ readonly prefabs: readonly Prefab[];
72
+ /** Ready for `buildSchedule`, in declaration order. */
73
+ readonly systems: readonly SystemDefinition[];
74
+ }
75
+
76
+ /**
77
+ * Check every `from host` declaration against what the host registered.
78
+ *
79
+ * Throws on the first mismatch rather than collecting: a shape disagreement means the module was
80
+ * compiled against a different world than the one it is being loaded into, and the second error
81
+ * would be a consequence of the first.
82
+ */
83
+ export function assertHostShapes(module: DriftModule, registry: ComponentRegistry): void {
84
+ for (const declared of module.info.components ?? []) {
85
+ if (!declared.fromHost) continue;
86
+
87
+ const registered = registry.get(declared.name);
88
+ if (registered === undefined) {
89
+ throw new Error(
90
+ `\`${module.info.module}\` declares \`component ${declared.name} from host\`, and this host ` +
91
+ `registered no component of that name. It has ` +
92
+ `${[...registry.keys()].map((k) => `\`${k}\``).join(', ')}. A \`from host\` declaration ` +
93
+ 'asserts a shape rather than creating one, so there is nothing to fall back to.',
94
+ );
95
+ }
96
+
97
+ const actual = new Map(registered.schema.fields.map((field) => [field.name, field.type]));
98
+ for (const field of declared.schema.fields) {
99
+ const found = actual.get(field.name);
100
+ if (found === undefined) {
101
+ throw new Error(
102
+ `\`${declared.name}.${field.name}\` is declared in \`${module.info.module}\` and the ` +
103
+ `registered component has no such field. It has ` +
104
+ `${[...actual.keys()].map((k) => `\`${k}\``).join(', ')}.`,
105
+ );
106
+ }
107
+ if (found !== field.type) {
108
+ throw new Error(
109
+ `\`${declared.name}.${field.name}\` is \`${field.type}\` in \`${module.info.module}\` and ` +
110
+ `\`${found}\` in the component this host registered. A script reading it would get a ` +
111
+ 'value of a type it was not compiled against.',
112
+ );
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Build every component, prefab and system a module declares.
120
+ *
121
+ * Components are registered into `registry` as they are made, so a prefab or a system later in the
122
+ * same module finds them — and so a second module loaded against the same registry shares them
123
+ * rather than making a second store for one name.
124
+ */
125
+ export function registerEntityModule(
126
+ module: DriftModule,
127
+ registry: ComponentRegistry,
128
+ resources: ResourceRegistry = new Map(),
129
+ ): RegisteredModule {
130
+ assertHostShapes(module, registry);
131
+
132
+ const components: ComponentType[] = [];
133
+ for (const declared of module.info.components ?? []) {
134
+ /* A host-declared component is already registered; asserting its shape is all this does. */
135
+ if (declared.fromHost) continue;
136
+ const existing = registry.get(declared.name);
137
+ /* A module reloaded keeps the store it already made: replacing the type would orphan every
138
+ entity holding one, which is a world that silently loses its components on an edit. */
139
+ const type = existing ?? defineComponent(declared.schema);
140
+ registry.set(declared.name, type);
141
+ components.push(type);
142
+ }
143
+
144
+ const prefabs = (module.info.prefabs ?? []).map((declared) =>
145
+ definePrefab(
146
+ declared.name,
147
+ declared.components.map((component) => {
148
+ const type = registry.get(component.name);
149
+ if (type === undefined) {
150
+ throw new Error(
151
+ `prefab \`${declared.name}\` names the component \`${component.name}\`, which nothing ` +
152
+ 'has registered. The compiler refuses this, so a module reaching here was built ' +
153
+ 'against a different set of declarations than it is being loaded with.',
154
+ );
155
+ }
156
+ return [type, component.values] as const;
157
+ }),
158
+ ),
159
+ );
160
+
161
+ const systems = (module.info.systems ?? []).map((declared): SystemDefinition => {
162
+ const resolve = (names: readonly string[]): ComponentType[] =>
163
+ names.map((name) => {
164
+ const type = registry.get(name);
165
+ if (type === undefined) {
166
+ throw new Error(
167
+ `system \`${declared.name}\` declares \`${name}\`, which nothing has registered.`,
168
+ );
169
+ }
170
+ return type;
171
+ });
172
+
173
+ /*
174
+ * Built once and frozen, so a system cannot acquire a resource by writing into the table it was
175
+ * handed — which would be state shared between ticks that nothing declares.
176
+ */
177
+ const lent: Record<string, unknown> = {};
178
+ for (const resource of declared.uses ?? []) {
179
+ if (!resources.has(resource.type)) {
180
+ throw new Error(
181
+ `system \`${declared.name}\` uses \`${resource.name}: ${resource.type}\`, and no ` +
182
+ `\`${resource.type}\` was supplied. Pass one in the third argument to ` +
183
+ '`registerEntityModule`, keyed by the type name. Refused here rather than passed as ' +
184
+ 'nothing, because an absent argument reaches a capability as a wrong answer.',
185
+ );
186
+ }
187
+ lent[resource.type] = resources.get(resource.type);
188
+ }
189
+ Object.freeze(lent);
190
+
191
+ return {
192
+ name: declared.name,
193
+ reads: resolve(declared.reads),
194
+ writes: resolve(declared.writes),
195
+ after: declared.after,
196
+ everyTicks: declared.everyTicks,
197
+ /*
198
+ * Through `exports` on every tick, never captured here. See the header: a captured function
199
+ * is pinned in the schedule across a hot patch, and the symptom is an edit that reports
200
+ * success and changes nothing.
201
+ */
202
+ run(view: SystemView): void {
203
+ const fn = module.exports[declared.name];
204
+ if (typeof fn !== 'function') {
205
+ throw new Error(
206
+ `\`${declared.name}\` is not exported by \`${module.info.module}\` any more. A disposed ` +
207
+ 'module left in a schedule fails here rather than silently doing nothing.',
208
+ );
209
+ }
210
+ /* The second argument is passed whether or not this system asked for anything. A generated
211
+ function without `uses` clauses declares one parameter and ignores it, which is what makes
212
+ the quiet form quiet in the compiler and costs nothing here. */
213
+ (fn as (v: SystemView, r: Record<string, unknown>) => void)(view, lent);
214
+ },
215
+ };
216
+ });
217
+
218
+ return { components, prefabs, systems };
219
+ }