@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,255 @@
1
+ import { NavPath, NavSearch, createNavSteer, nearestNavNode } from '@driftengine/core';
2
+ import { entityGeneration, entityIndex } from '@driftengine/entities';
3
+ import { defineCapability } from 'driftscript';
4
+ export const NAVIGATION_MODULE = 'drift/navigation';
5
+ /**
6
+ * `drift/navigation` — a graph, a route over it, and a point to steer at.
7
+ *
8
+ * **The linker has refused this module by name since the language shipped**, and the reason given
9
+ * was that nothing here paths. That was true and it was never a blocker: it was the row. A consumer
10
+ * reported what its absence costs them — twelve agents that walk straight at whatever they are
11
+ * going to and wedge against a building on the way, about once every two minutes of play — and
12
+ * noted that they already extract a road graph from their world, which is the half of the work that
13
+ * is usually the awkward half.
14
+ *
15
+ * **A graph and not a mesh**, and the header of `navGraph.ts` argues that where the decision is:
16
+ * these consumers arrive with a network already, and open ground crossed by two nodes is crossed in
17
+ * a straight line.
18
+ *
19
+ * **`navigation.read` is a deterministic effect**, which the language decided before this existed
20
+ * and this has to keep true: a route is a function of the graph and its two endpoints and of
21
+ * nothing else, ties included. `navSearch.ts` is where that is held. A `@deterministic` system may
22
+ * therefore path, which is the point — deciding where to walk is exactly the kind of thing that
23
+ * belongs inside the fixed step.
24
+ */
25
+ export const NAVIGATION_TYPES = [
26
+ {
27
+ module: NAVIGATION_MODULE,
28
+ name: 'NavGraph',
29
+ doc: 'A network of places and the ways between them.',
30
+ },
31
+ {
32
+ module: NAVIGATION_MODULE,
33
+ name: 'NavPath',
34
+ doc: 'A route being walked, and how far along it the walker is. Ask it where to steer.',
35
+ },
36
+ ];
37
+ const define = (name, params, returns, effects, doc, deterministic = true, allocates = false) => defineCapability({
38
+ module: NAVIGATION_MODULE,
39
+ name,
40
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
41
+ params: [...params],
42
+ returns,
43
+ ...(allocates ? { allocates: true } : {}),
44
+ effects: effects,
45
+ /*
46
+ * **Writing a route is deterministic too, as of DriftScript 1.12.0**, and this paragraph used
47
+ * to say the opposite. `navigation.read` was in `DETERMINISTIC_EFFECTS` and `navigation.write`
48
+ * was not, so a `@deterministic` system could steer along a route and could not compute one —
49
+ * an agent re-pathing inside the fixed step, because a bridge dropped or because it was pushed
50
+ * off the network, had to path from an ordinary system and hand the result in.
51
+ *
52
+ * **It reversed on exactly the argument this comment named**, which is the whole reason the
53
+ * condition was written down rather than the limitation merely stated: a route is a function of
54
+ * the graph and two endpoints, `navSearch.ts` breaks ties on the node index so two runs agree,
55
+ * and the `NavPath` written into is simulation state rather than a host's. The language's own
56
+ * deferral said the track that builds an effect is the one that can answer for its replay
57
+ * behaviour; this track shipped, so it answered.
58
+ *
59
+ * Every capability here is deterministic now, so the parameter keeps its default and the three
60
+ * writers stopped passing `false`.
61
+ */
62
+ deterministic,
63
+ doc,
64
+ implementation: `${NAVIGATION_MODULE}.${name}`,
65
+ });
66
+ export const NAVIGATION_CAPABILITIES = [
67
+ define('nearest', [
68
+ { name: 'graph', type: 'NavGraph' },
69
+ { name: 'x', type: 'float' },
70
+ { name: 'y', type: 'float' },
71
+ { name: 'z', type: 'float' },
72
+ ], 'i32', ['navigation.read'], 'The node nearest a place, or -1 where the graph has none. How an agent gets onto the network.'),
73
+ define('nearestWithin', [
74
+ { name: 'graph', type: 'NavGraph' },
75
+ { name: 'x', type: 'float' },
76
+ { name: 'y', type: 'float' },
77
+ { name: 'z', type: 'float' },
78
+ { name: 'maxDistance', type: 'float' },
79
+ ], 'i32', ['navigation.read'], 'The nearest node within a distance, or -1. Use it so an agent that has walked off the network is not snapped to the far side of the map.'),
80
+ /*
81
+ * **`route` takes places and not node indices**, which is the whole difference between a binding
82
+ * a script author can use and one they have to learn a data model for first. Getting onto a graph
83
+ * is `nearest` twice, and a script that had to write that would write it identically every time
84
+ * and get the out-of-reach case wrong once.
85
+ */
86
+ define('route', [
87
+ { name: 'path', type: 'NavPath' },
88
+ { name: 'graph', type: 'NavGraph' },
89
+ { name: 'fromX', type: 'float' },
90
+ { name: 'fromY', type: 'float' },
91
+ { name: 'fromZ', type: 'float' },
92
+ { name: 'toX', type: 'float' },
93
+ { name: 'toY', type: 'float' },
94
+ { name: 'toZ', type: 'float' },
95
+ ], 'bool', ['navigation.write'], 'Path from one place to another, storing the route. False where there is no way through, which is an answer and not a failure.'),
96
+ define('routeBetween', [
97
+ { name: 'path', type: 'NavPath' },
98
+ { name: 'graph', type: 'NavGraph' },
99
+ { name: 'from', type: 'i32' },
100
+ { name: 'to', type: 'i32' },
101
+ ], 'bool', ['navigation.write'], 'The same, between two nodes you already have. For a consumer with their own spatial index.'),
102
+ /*
103
+ * **A route per agent, because a script had nowhere to keep one.**
104
+ *
105
+ * Every capability above takes a `NavPath` and nothing here made one, so a consumer kept a path
106
+ * per agent in TypeScript and called a script function once per agent per step — which is the
107
+ * report DriftScript 1.8.0's `uses` clause answers half of. `uses` hands a system the *graph*, one
108
+ * per world; a route is one per walker, and a resource is one per type, so the other half has to
109
+ * arrive as a value a script can ask for by entity. This is it, and with it the loop over agents
110
+ * goes back into the `system` where it can be reloaded.
111
+ *
112
+ * **Kept per entity *index*, with the generation beside it.** A map keyed by the handle would grow
113
+ * for the life of the world — every agent that ever pathed, long after it was destroyed, with no
114
+ * moment at which anything could know to drop one. An index is bounded by the world's ceiling, and
115
+ * a reused index arriving with a different generation is a different agent, so the route is
116
+ * cleared rather than inherited. That is the failure worth naming: without the check a freshly
117
+ * spawned walker would set off along the route of whatever died in its slot.
118
+ *
119
+ * **It allocates the first time it is asked about an agent** and never again, which `allocates`
120
+ * says so a `@hot` function is told. There is no way around it — a route is a buffer — and the
121
+ * alternative, making a script pass its own, is the thing this exists to remove.
122
+ */
123
+ define('path', [{ name: 'agent', type: 'Entity' }], 'NavPath', ['navigation.read'], "The route this agent is following, kept by the host between steps. Empty until something routes it, and empty again if the agent's handle is reused.", true, true),
124
+ define('clear', [{ name: 'path', type: 'NavPath' }], 'void', ['navigation.write'], 'Forget the route. The walker is then arrived, wherever it is.'),
125
+ define('following', [{ name: 'path', type: 'NavPath' }], 'bool', ['navigation.read'], 'Whether there is a route to follow.'),
126
+ /*
127
+ * **The steer is read back through four accessors**, which is the shape `input.axisX` and
128
+ * `animation.hitX` already have and the reason is the same: the language has no way to return a
129
+ * record by value into a caller's own storage, and an opaque handle for a three-float answer
130
+ * would be a lifetime a script author has to think about to read a direction.
131
+ *
132
+ * The implementation answers all four from one computation — see `navigationImplementation`,
133
+ * where the reason it can is that the arguments say what the answer depends on.
134
+ */
135
+ define('steerX', [
136
+ { name: 'path', type: 'NavPath' },
137
+ { name: 'x', type: 'float' },
138
+ { name: 'y', type: 'float' },
139
+ { name: 'z', type: 'float' },
140
+ ], 'float', ['navigation.read'], 'Where a walker at this place should aim, along x. A point ahead on the path rather than the next node, so corners are cut smoothly instead of snapped at.'),
141
+ define('steerY', [
142
+ { name: 'path', type: 'NavPath' },
143
+ { name: 'x', type: 'float' },
144
+ { name: 'y', type: 'float' },
145
+ { name: 'z', type: 'float' },
146
+ ], 'float', ['navigation.read'], 'The same aim point, along y.'),
147
+ define('steerZ', [
148
+ { name: 'path', type: 'NavPath' },
149
+ { name: 'x', type: 'float' },
150
+ { name: 'y', type: 'float' },
151
+ { name: 'z', type: 'float' },
152
+ ], 'float', ['navigation.read'], 'The same aim point, along z.'),
153
+ define('remaining', [
154
+ { name: 'path', type: 'NavPath' },
155
+ { name: 'x', type: 'float' },
156
+ { name: 'y', type: 'float' },
157
+ { name: 'z', type: 'float' },
158
+ ], 'float', ['navigation.read'], 'How far is left, following the path rather than as the crow flies.'),
159
+ define('arrived', [
160
+ { name: 'path', type: 'NavPath' },
161
+ { name: 'x', type: 'float' },
162
+ { name: 'y', type: 'float' },
163
+ { name: 'z', type: 'float' },
164
+ ], 'bool', ['navigation.read'], 'Whether the walker is there. True with no route at all, so one check covers both.'),
165
+ ];
166
+ export function navigationImplementation(services) {
167
+ const buffer = new Uint32Array(services.maxRouteNodes ?? 256);
168
+ const steer = createNavSteer();
169
+ /*
170
+ * The last place a steer was computed for, so `steerX`, `steerY`, `steerZ`, `remaining` and
171
+ * `arrived` at one position cost one walk of the path rather than five. Correct rather than
172
+ * merely fast: the answer is a function of the path and the place, and the path cannot change
173
+ * between two reads inside a script's own statement.
174
+ */
175
+ let cachedPath = null;
176
+ let cachedX = NaN;
177
+ let cachedY = NaN;
178
+ let cachedZ = NaN;
179
+ /*
180
+ * One route per agent, by entity **index**, with the generation beside it.
181
+ *
182
+ * Two sparse arrays rather than a `Map` keyed by the handle: a map grows for the life of the
183
+ * world, one entry per agent that ever pathed, and nothing is in a position to know when to drop
184
+ * one. An index is bounded by the world's ceiling. The generation is what makes a reused index
185
+ * safe — a slot arriving with a different one is a different agent, and its route is cleared
186
+ * rather than inherited, which would otherwise send a freshly spawned walker off along the route
187
+ * of whatever died in its place.
188
+ */
189
+ const routes = [];
190
+ const owners = [];
191
+ const at = (path, x, y, z) => {
192
+ if (path !== cachedPath || x !== cachedX || y !== cachedY || z !== cachedZ) {
193
+ path.steer(x, y, z, steer);
194
+ cachedPath = path;
195
+ cachedX = x;
196
+ cachedY = y;
197
+ cachedZ = z;
198
+ }
199
+ return steer;
200
+ };
201
+ /** Any write to a path invalidates the cache, since the answer depends on the route. */
202
+ const invalidate = () => {
203
+ cachedPath = null;
204
+ };
205
+ return {
206
+ path(agent) {
207
+ const index = entityIndex(agent);
208
+ const generation = entityGeneration(agent);
209
+ let route = routes[index];
210
+ if (route === undefined) {
211
+ route = new NavPath(services.graph, services.maxRouteNodes ?? 256);
212
+ routes[index] = route;
213
+ }
214
+ else if (owners[index] !== generation) {
215
+ /* The slot was somebody else's. Clearing rather than replacing keeps the allocation this
216
+ agent is about to need and leaves nothing of the last one's route. */
217
+ route.clear();
218
+ invalidate();
219
+ }
220
+ owners[index] = generation;
221
+ return route;
222
+ },
223
+ nearest: (graph, x, y, z) => nearestNavNode(graph, x, y, z),
224
+ nearestWithin: (graph, x, y, z, maxDistance) => nearestNavNode(graph, x, y, z, maxDistance),
225
+ route(path, graph, fromX, fromY, fromZ, toX, toY, toZ) {
226
+ invalidate();
227
+ const from = nearestNavNode(graph, fromX, fromY, fromZ);
228
+ const to = nearestNavNode(graph, toX, toY, toZ);
229
+ if (from < 0 || to < 0) {
230
+ path.clear();
231
+ return false;
232
+ }
233
+ const count = services.search.find(from, to, buffer);
234
+ path.set(buffer, count);
235
+ return count > 1;
236
+ },
237
+ routeBetween(path, graph, from, to) {
238
+ invalidate();
239
+ void graph;
240
+ const count = services.search.find(from, to, buffer);
241
+ path.set(buffer, count);
242
+ return count > 1;
243
+ },
244
+ clear(path) {
245
+ invalidate();
246
+ path.clear();
247
+ },
248
+ following: (path) => path.active,
249
+ steerX: (path, x, y, z) => at(path, x, y, z).x,
250
+ steerY: (path, x, y, z) => at(path, x, y, z).y,
251
+ steerZ: (path, x, y, z) => at(path, x, y, z).z,
252
+ remaining: (path, x, y, z) => at(path, x, y, z).remainingM,
253
+ arrived: (path, x, y, z) => at(path, x, y, z).arrived,
254
+ };
255
+ }
@@ -0,0 +1,51 @@
1
+ import type { CapabilityDefinition, OpaqueType } from 'driftscript';
2
+ export declare const NETWORK_MODULE = "drift/network";
3
+ /**
4
+ * `drift/network` — what a script can see of a session, and the scalars it can publish.
5
+ *
6
+ * **The linker has refused this module by name since the language shipped**, saying it waits on
7
+ * Track J. The corpus carries `NetworkReplicatedActor.drs`, written *before* any provider existed
8
+ * so that "the networking semantics are written down before networking couples itself to the state
9
+ * and task design". Its names are honoured here — `replicate` and `authority` are both capabilities
10
+ * — and its one illustrative call is not: it reads `network.replicate(character.x, character.y)`, with no
11
+ * session, because when it was written there was no handle to take. Every capability takes the
12
+ * thing it is about, the way every other binding in this directory does.
13
+ *
14
+ * ---
15
+ *
16
+ * ## The effect split, which the language asked this track to make
17
+ *
18
+ * `capability.ts` puts `network.read` **inside** `DETERMINISTIC_EFFECTS` and leaves `network.write`
19
+ * outside, with a note that the exclusion is *"a deferral rather than a judgement: each belongs to a
20
+ * track that has not shipped, and the track that builds one is the one that can say whether its
21
+ * writes are the simulation or a consequence of it."* This is that track, so here is the answer.
22
+ *
23
+ * **`network.write` stays outside, and the rollback loop is the reason.** The tempting argument for
24
+ * admitting it is that publishing a value changes no simulation state, so a replay would publish
25
+ * the same thing and nothing would drift. That argument is exactly backwards once a rewind exists:
26
+ * **a `@deterministic` function is precisely the kind that gets re-run**, several times, over ticks
27
+ * that have already happened. A send is not idempotent — the replay would publish the same value
28
+ * again for a tick a peer already has, and a correction that replayed twelve ticks would put twelve
29
+ * duplicate messages on the wire. Deferring the effect was right and the deferral now has a reason
30
+ * behind it rather than an absence.
31
+ *
32
+ * **`network.read` is kept for the two things that genuinely do not vary**: which participant this
33
+ * is, and whether this peer is the authority. Both are fixed when the session is created, so a
34
+ * `@deterministic` system may branch on them and replay identically.
35
+ *
36
+ * **Everything else a session knows is `nondeterministic`, and that is not pedantry.** The confirmed
37
+ * watermark, the participant count and whether the session has halted all move with packet timing:
38
+ * a system that branched on "is tick 400 confirmed" would take one path live and the other on
39
+ * replay, which is the class of bug rollback netcode is famous for. `bindings/ui.ts` made the same
40
+ * call for `hovered` and `pressed`, and this file's version of that sentence is: an effect that
41
+ * type-checks can still be a lie.
42
+ *
43
+ * ## Slots rather than field names
44
+ *
45
+ * `replicate(session, slot, value)` writes into a numbered table. A name would need a string map on
46
+ * a per-tick path and a miss would need an optional to answer with, which the language does not
47
+ * have. A slot out of range does nothing on write and answers zero on read, so both are total.
48
+ */
49
+ export declare const NETWORK_TYPES: readonly OpaqueType[];
50
+ export declare const NETWORK_CAPABILITIES: readonly CapabilityDefinition[];
51
+ export declare function networkImplementation(): Record<string, unknown>;
@@ -0,0 +1,96 @@
1
+ import { defineCapability } from 'driftscript';
2
+ export const NETWORK_MODULE = 'drift/network';
3
+ /**
4
+ * `drift/network` — what a script can see of a session, and the scalars it can publish.
5
+ *
6
+ * **The linker has refused this module by name since the language shipped**, saying it waits on
7
+ * Track J. The corpus carries `NetworkReplicatedActor.drs`, written *before* any provider existed
8
+ * so that "the networking semantics are written down before networking couples itself to the state
9
+ * and task design". Its names are honoured here — `replicate` and `authority` are both capabilities
10
+ * — and its one illustrative call is not: it reads `network.replicate(character.x, character.y)`, with no
11
+ * session, because when it was written there was no handle to take. Every capability takes the
12
+ * thing it is about, the way every other binding in this directory does.
13
+ *
14
+ * ---
15
+ *
16
+ * ## The effect split, which the language asked this track to make
17
+ *
18
+ * `capability.ts` puts `network.read` **inside** `DETERMINISTIC_EFFECTS` and leaves `network.write`
19
+ * outside, with a note that the exclusion is *"a deferral rather than a judgement: each belongs to a
20
+ * track that has not shipped, and the track that builds one is the one that can say whether its
21
+ * writes are the simulation or a consequence of it."* This is that track, so here is the answer.
22
+ *
23
+ * **`network.write` stays outside, and the rollback loop is the reason.** The tempting argument for
24
+ * admitting it is that publishing a value changes no simulation state, so a replay would publish
25
+ * the same thing and nothing would drift. That argument is exactly backwards once a rewind exists:
26
+ * **a `@deterministic` function is precisely the kind that gets re-run**, several times, over ticks
27
+ * that have already happened. A send is not idempotent — the replay would publish the same value
28
+ * again for a tick a peer already has, and a correction that replayed twelve ticks would put twelve
29
+ * duplicate messages on the wire. Deferring the effect was right and the deferral now has a reason
30
+ * behind it rather than an absence.
31
+ *
32
+ * **`network.read` is kept for the two things that genuinely do not vary**: which participant this
33
+ * is, and whether this peer is the authority. Both are fixed when the session is created, so a
34
+ * `@deterministic` system may branch on them and replay identically.
35
+ *
36
+ * **Everything else a session knows is `nondeterministic`, and that is not pedantry.** The confirmed
37
+ * watermark, the participant count and whether the session has halted all move with packet timing:
38
+ * a system that branched on "is tick 400 confirmed" would take one path live and the other on
39
+ * replay, which is the class of bug rollback netcode is famous for. `bindings/ui.ts` made the same
40
+ * call for `hovered` and `pressed`, and this file's version of that sentence is: an effect that
41
+ * type-checks can still be a lie.
42
+ *
43
+ * ## Slots rather than field names
44
+ *
45
+ * `replicate(session, slot, value)` writes into a numbered table. A name would need a string map on
46
+ * a per-tick path and a miss would need an optional to answer with, which the language does not
47
+ * have. A slot out of range does nothing on write and answers zero on read, so both are total.
48
+ */
49
+ export const NETWORK_TYPES = [
50
+ {
51
+ module: NETWORK_MODULE,
52
+ name: 'Session',
53
+ doc: 'A networking session: who this peer is, what it can see of the others, and the scalars it publishes.',
54
+ },
55
+ ];
56
+ const define = (name, params, returns, effects, deterministic, doc) => defineCapability({
57
+ module: NETWORK_MODULE,
58
+ name,
59
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
60
+ params: [...params],
61
+ returns,
62
+ effects: [...effects],
63
+ deterministic,
64
+ doc,
65
+ implementation: `${NETWORK_MODULE}.${name}`,
66
+ });
67
+ const ON = [{ name: 'session', type: 'Session' }];
68
+ export const NETWORK_CAPABILITIES = [
69
+ /* The two that do not move. See the header for why only these two. */
70
+ define('self', ON, 'i32', ['network.read'], true, 'Which participant this peer is. Fixed for the life of the session, which is why a deterministic system may read it.'),
71
+ define('authority', ON, 'bool', ['network.read'], true, 'Whether this peer’s world is the authoritative one. Fixed when the session is created.'),
72
+ /* Everything that moves with packet timing. */
73
+ define('participants', ON, 'i32', ['nondeterministic'], false, 'How many participants the session holds. Nondeterministic because somebody may join or leave between two runs of the same recording.'),
74
+ define('confirmed', ON, 'i32', ['nondeterministic'], false, 'The highest tick every participant’s input has arrived for, or -1. Moves with packet timing, so a deterministic system may not branch on it.'),
75
+ define('halted', ON, 'bool', ['nondeterministic'], false, 'Whether the session has stopped, because an input arrived too late to apply or two peers computed different worlds.'),
76
+ define('haltReason', ON, 'String', ['nondeterministic'], false, 'Why it halted, as a sentence, or an empty string. Names the tick a divergence began at.'),
77
+ define('slots', ON, 'i32', ['nondeterministic'], false, 'How many replicated scalars each participant has. A consumer sizes this when the session is made.'),
78
+ define('replicated', [...ON, { name: 'participant', type: 'i32' }, { name: 'slot', type: 'i32' }], 'f32', ['nondeterministic'], false, 'Read a participant’s published scalar. An index out of range answers zero rather than failing, because a frame loop has nothing to do with a refusal.'),
79
+ /* And the one write. */
80
+ define('replicate', [...ON, { name: 'slot', type: 'i32' }, { name: 'value', type: 'f32' }], 'void', ['network.write'], false, 'Publish a value in one of this peer’s slots. Not callable from a deterministic system: a replay re-runs one and a send is not idempotent.'),
81
+ ];
82
+ export function networkImplementation() {
83
+ return {
84
+ self: (session) => session.self,
85
+ authority: (session) => session.authority,
86
+ participants: (session) => session.participants,
87
+ confirmed: (session) => session.confirmed,
88
+ halted: (session) => session.halted,
89
+ haltReason: (session) => session.haltReason,
90
+ slots: (session) => session.slots,
91
+ replicated: (session, participant, slot) => session.replicated(participant, slot),
92
+ replicate: (session, slot, value) => {
93
+ session.replicate(slot, value);
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,71 @@
1
+ import { type CapabilityDefinition, type OpaqueType } from 'driftscript';
2
+ export declare const RENDER_MODULE = "drift/render";
3
+ /**
4
+ * `drift/render` — how the finished image is presented, for the frame about to be drawn.
5
+ *
6
+ * **The linker refused this module by name from the day the language shipped until now**, and the
7
+ * roadmap slot it was waiting on was Track D. Track D completed on 2026-09-03 with refraction, so
8
+ * the wait ended five releases before anything noticed — `docs/CAPABILITIES.md` still mapped this
9
+ * module to a track that had closed, while `host.test.ts` had already been corrected to say it
10
+ * waits on a row of its own. Two documents, one right.
11
+ *
12
+ * ## What this module is, which is a smaller thing than "the renderer"
13
+ *
14
+ * `RenderQuality` has fifty fields and **none of them are here.** The engine already draws
15
+ * the line this module needs, in `renderQuality.ts`'s own words about depth of field:
16
+ *
17
+ * > A ceiling rather than the effect [...] this says how far a defocused point may spread and
18
+ * > therefore what the pass may cost, and `setDepthOfField` says where this frame's lens is focused
19
+ * > and how much of the ceiling it takes. A camera focuses on a subject; a quality profile decides
20
+ * > how expensive that is allowed to be.
21
+ *
22
+ * **A quality profile is the application's and a dial is the game's.** A profile is chosen once,
23
+ * from what the device can afford, and is clamped by `capabilityClamp` against what the adapter
24
+ * actually reports; a script that wrote to one would be overruling a decision made about hardware
25
+ * it cannot see, and two machines running the same script would draw different pictures for
26
+ * reasons the script did not choose. So this module binds the seven dials and none of the ceilings.
27
+ *
28
+ * ## Writes only, and the reason is the same one
29
+ *
30
+ * There is no `render.bloomOf` here, and nothing reads a dial back. Every one of these is **clamped
31
+ * against the ceiling the host set** — `setBloom` does nothing at all when `bloom` is 0, because
32
+ * the chain is never built — so what a read would answer is a fact about the machine wearing the
33
+ * costume of a fact about the frame. The language wrote this argument once already, for
34
+ * `network.read` and a confirmed-input watermark: a host registering a machine-dependent number
35
+ * under a deterministic effect hands a `@deterministic` system a value that differs between a run
36
+ * and its replay. A script sets a dial; what the device did with it is the device's business.
37
+ *
38
+ * ## The effect, which needed no language release
39
+ *
40
+ * **`scene.write`, and it is a description rather than a borrowing.** `capability.ts` puts
41
+ * `scene.write` outside `DETERMINISTIC_EFFECTS` and says exactly why: *"a `SceneNode` is what
42
+ * draws, and `ARCHITECTURE.md` forbids render code mutating simulation state, so moving a node is
43
+ * a change to the view."* Every capability here is a change to the view and nothing else — the
44
+ * renderers' own comments call these *"a renderer parameter rather than a scene one: it describes
45
+ * how the finished image is presented, not anything in the world, which is why nothing about the
46
+ * world has to know it exists."* Two files arriving at the same sentence from opposite ends is the
47
+ * argument.
48
+ *
49
+ * So no capability here is deterministic, and that is the property rather than a limitation: a
50
+ * `@deterministic` system may not dim the screen. Presentation belongs outside the fixed step,
51
+ * where the frame is.
52
+ */
53
+ export declare const RENDER_TYPES: readonly OpaqueType[];
54
+ export declare const RENDER_CAPABILITIES: readonly CapabilityDefinition[];
55
+ /**
56
+ * Unconditional, like `drift/terrain` and `drift/editor`: every capability takes the renderer it
57
+ * acts on, which reaches a script through `uses`, so there is nothing for an absent service to
58
+ * make fail.
59
+ *
60
+ * **`medium` takes every argument the engine defaults**, unlike `setGlobalMedium`, and for the
61
+ * reason `focus` gives about its own `scale`: a script author is further from the cost than a
62
+ * TypeScript caller, and a defaulted albedo is a number somebody has to go and look up before they
63
+ * can tell what their fog will look like. Four numbers is one line either way.
64
+ *
65
+ * * **`speedBlur` is the one name here that is not the engine's own.** The method is `setSpeedRush`,
66
+ * and a rush is a thing that happens in somebody's game rather than a description of an image
67
+ * operation — binding that spelling would have written one consumer's noun into the language's
68
+ * surface, where it could not later be taken back. A binding is a lookup and this is what a lookup
69
+ * is for.
70
+ */
71
+ export declare function renderImplementation(): Record<string, unknown>;
@@ -0,0 +1,143 @@
1
+ import { defineCapability } from 'driftscript';
2
+ export const RENDER_MODULE = 'drift/render';
3
+ /**
4
+ * `drift/render` — how the finished image is presented, for the frame about to be drawn.
5
+ *
6
+ * **The linker refused this module by name from the day the language shipped until now**, and the
7
+ * roadmap slot it was waiting on was Track D. Track D completed on 2026-09-03 with refraction, so
8
+ * the wait ended five releases before anything noticed — `docs/CAPABILITIES.md` still mapped this
9
+ * module to a track that had closed, while `host.test.ts` had already been corrected to say it
10
+ * waits on a row of its own. Two documents, one right.
11
+ *
12
+ * ## What this module is, which is a smaller thing than "the renderer"
13
+ *
14
+ * `RenderQuality` has fifty fields and **none of them are here.** The engine already draws
15
+ * the line this module needs, in `renderQuality.ts`'s own words about depth of field:
16
+ *
17
+ * > A ceiling rather than the effect [...] this says how far a defocused point may spread and
18
+ * > therefore what the pass may cost, and `setDepthOfField` says where this frame's lens is focused
19
+ * > and how much of the ceiling it takes. A camera focuses on a subject; a quality profile decides
20
+ * > how expensive that is allowed to be.
21
+ *
22
+ * **A quality profile is the application's and a dial is the game's.** A profile is chosen once,
23
+ * from what the device can afford, and is clamped by `capabilityClamp` against what the adapter
24
+ * actually reports; a script that wrote to one would be overruling a decision made about hardware
25
+ * it cannot see, and two machines running the same script would draw different pictures for
26
+ * reasons the script did not choose. So this module binds the seven dials and none of the ceilings.
27
+ *
28
+ * ## Writes only, and the reason is the same one
29
+ *
30
+ * There is no `render.bloomOf` here, and nothing reads a dial back. Every one of these is **clamped
31
+ * against the ceiling the host set** — `setBloom` does nothing at all when `bloom` is 0, because
32
+ * the chain is never built — so what a read would answer is a fact about the machine wearing the
33
+ * costume of a fact about the frame. The language wrote this argument once already, for
34
+ * `network.read` and a confirmed-input watermark: a host registering a machine-dependent number
35
+ * under a deterministic effect hands a `@deterministic` system a value that differs between a run
36
+ * and its replay. A script sets a dial; what the device did with it is the device's business.
37
+ *
38
+ * ## The effect, which needed no language release
39
+ *
40
+ * **`scene.write`, and it is a description rather than a borrowing.** `capability.ts` puts
41
+ * `scene.write` outside `DETERMINISTIC_EFFECTS` and says exactly why: *"a `SceneNode` is what
42
+ * draws, and `ARCHITECTURE.md` forbids render code mutating simulation state, so moving a node is
43
+ * a change to the view."* Every capability here is a change to the view and nothing else — the
44
+ * renderers' own comments call these *"a renderer parameter rather than a scene one: it describes
45
+ * how the finished image is presented, not anything in the world, which is why nothing about the
46
+ * world has to know it exists."* Two files arriving at the same sentence from opposite ends is the
47
+ * argument.
48
+ *
49
+ * So no capability here is deterministic, and that is the property rather than a limitation: a
50
+ * `@deterministic` system may not dim the screen. Presentation belongs outside the fixed step,
51
+ * where the frame is.
52
+ */
53
+ export const RENDER_TYPES = [
54
+ {
55
+ module: RENDER_MODULE,
56
+ name: 'Renderer',
57
+ doc: 'The renderer drawing this frame. Reaches a script through `uses`.',
58
+ },
59
+ ];
60
+ const define = (name, params, doc) => defineCapability({
61
+ module: RENDER_MODULE,
62
+ name,
63
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> void`,
64
+ params: [...params],
65
+ returns: 'void',
66
+ effects: ['scene.write'],
67
+ deterministic: false,
68
+ doc,
69
+ implementation: `${RENDER_MODULE}.${name}`,
70
+ });
71
+ const R = { name: 'renderer', type: 'Renderer' };
72
+ export const RENDER_CAPABILITIES = [
73
+ define('bloom', [R, { name: 'scale', type: 'f32' }], "How much of the frame's bloom ceiling to take, 0 to 1. Held until changed. Does nothing " +
74
+ "when the quality profile's `bloom` is 0, because the chain is never built."),
75
+ define('exposure', [R, { name: 'stops', type: 'f32' }], "How far this frame is scaled into the tone curve, above 0. Replaces the profile's " +
76
+ '`outputExposure` for this frame onward. Adaptation is yours: a game knows it walked into a ' +
77
+ 'cave, and the renderer could only find out a frame late. Ignored unless the output ' +
78
+ 'transform is `aces`, since otherwise there is no curve to be exposed into.'),
79
+ define('motionBlur', [R, { name: 'scale', type: 'f32' }], 'How much of the camera motion blur ceiling this frame takes, 0 to 1. Held until changed. ' +
80
+ 'Ramp it with whatever "fast" means here: blur that is always on stops being a speed cue ' +
81
+ 'within seconds and costs eight taps a moving pixel while doing so.'),
82
+ define('speedBlur', [R, { name: 'strength', type: 'f32' }], 'How much speed blur the frame resolves with, 0 to 1. Held until changed, and ignored when ' +
83
+ 'screen effects are off.'),
84
+ define('focus', [
85
+ R,
86
+ { name: 'distance', type: 'f32' },
87
+ { name: 'range', type: 'f32' },
88
+ { name: 'scale', type: 'f32' },
89
+ ], "Where this frame's lens is focused and how deep the sharp zone is, in metres, and how much " +
90
+ 'of the depth-of-field ceiling to take, 0 to 1. Held until changed. `scale` is required ' +
91
+ 'here though the engine defaults it to 1, because taking the whole ceiling is the expensive ' +
92
+ 'answer and a script author is further from that cost than a TypeScript caller.'),
93
+ define('medium', [
94
+ R,
95
+ { name: 'density', type: 'f32' },
96
+ { name: 'albedo', type: 'f32' },
97
+ { name: 'anisotropy', type: 'f32' },
98
+ { name: 'maxDistance', type: 'f32' },
99
+ ], 'How thick the air is: a medium filling the whole frustum, rather than a beam inside a hull. ' +
100
+ '`density` is extinction per metre and 0 is off — nothing is allocated and nothing is drawn, ' +
101
+ 'so a script that never raises it costs nothing. `albedo` is how much of what the air takes ' +
102
+ 'out comes back as light rather than heat, 0 to 1; `anisotropy` is -1 back to 1 forward, and ' +
103
+ 'is what makes haze glow toward a low sun; `maxDistance` is where the search for light to ' +
104
+ 'scatter stops, in metres, which is not where the fog stops. Held until changed. Does ' +
105
+ "nothing when the quality profile's `globalMediumSteps` is 0, because no march is built."),
106
+ define('veil', [
107
+ R,
108
+ { name: 'red', type: 'f32' },
109
+ { name: 'green', type: 'f32' },
110
+ { name: 'blue', type: 'f32' },
111
+ { name: 'alpha', type: 'f32' },
112
+ ], 'Composite a flat colour over the finished frame, for a cut dipping to white or to black. ' +
113
+ '**Once per frame: a frame that does not call this draws with no veil at all**, unlike the ' +
114
+ 'dials above, because a transition that forgets to turn itself off is worse than one that ' +
115
+ 'forgets to turn on.'),
116
+ ];
117
+ /**
118
+ * Unconditional, like `drift/terrain` and `drift/editor`: every capability takes the renderer it
119
+ * acts on, which reaches a script through `uses`, so there is nothing for an absent service to
120
+ * make fail.
121
+ *
122
+ * **`medium` takes every argument the engine defaults**, unlike `setGlobalMedium`, and for the
123
+ * reason `focus` gives about its own `scale`: a script author is further from the cost than a
124
+ * TypeScript caller, and a defaulted albedo is a number somebody has to go and look up before they
125
+ * can tell what their fog will look like. Four numbers is one line either way.
126
+ *
127
+ * * **`speedBlur` is the one name here that is not the engine's own.** The method is `setSpeedRush`,
128
+ * and a rush is a thing that happens in somebody's game rather than a description of an image
129
+ * operation — binding that spelling would have written one consumer's noun into the language's
130
+ * surface, where it could not later be taken back. A binding is a lookup and this is what a lookup
131
+ * is for.
132
+ */
133
+ export function renderImplementation() {
134
+ return {
135
+ bloom: (renderer, scale) => renderer.setBloom(scale),
136
+ exposure: (renderer, stops) => renderer.setOutputExposure(stops),
137
+ motionBlur: (renderer, scale) => renderer.setCameraMotionBlur(scale),
138
+ speedBlur: (renderer, strength) => renderer.setSpeedRush(strength),
139
+ focus: (renderer, distance, range, scale) => renderer.setDepthOfField(distance, range, scale),
140
+ medium: (renderer, density, albedo, anisotropy, maxDistance) => renderer.setGlobalMedium(density, albedo, anisotropy, maxDistance),
141
+ veil: (renderer, red, green, blue, alpha) => renderer.setFrameVeil(red, green, blue, alpha),
142
+ };
143
+ }