@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,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
|
+
}
|