pelican-engine 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/AGENTS.md +147 -0
  2. package/LICENSE +22 -0
  3. package/README.md +129 -2
  4. package/distribution.json +197 -0
  5. package/docs/AI-WORKFLOW.md +517 -0
  6. package/docs/ARCHITECTURE.md +268 -0
  7. package/docs/CAPABILITIES.md +264 -0
  8. package/docs/DECISIONS.md +107 -0
  9. package/docs/M3-USAGE.md +192 -0
  10. package/docs/PELICAN-MIGRATION.md +38 -0
  11. package/docs/RENDER-BACKENDS.md +50 -0
  12. package/docs/ROADMAP.md +239 -0
  13. package/docs/THIRD-PARTY-NOTICES.md +151 -0
  14. package/docs/api/REFERENCE.md +1435 -0
  15. package/docs/api/index.json +1139 -0
  16. package/docs/npm/DISTRIBUTION.md +130 -0
  17. package/docs/npm/GAME-AUTHOR.md +147 -0
  18. package/engine.manifest.json +15 -0
  19. package/lib/assets/audio.js +67 -0
  20. package/lib/assets/errors.js +36 -0
  21. package/lib/assets/glb.js +277 -0
  22. package/lib/assets/index.js +53 -0
  23. package/lib/assets/jpeg.js +199 -0
  24. package/lib/assets/model.js +558 -0
  25. package/lib/assets/optimize.js +95 -0
  26. package/lib/assets/texture.js +187 -0
  27. package/lib/compiler/distribution.js +17 -0
  28. package/lib/compiler/extract.js +425 -0
  29. package/lib/compiler/graph.js +366 -0
  30. package/lib/compiler/index.js +574 -0
  31. package/lib/compiler/normalize.js +1194 -0
  32. package/lib/compiler/source-map.js +25 -0
  33. package/lib/compiler/types.js +2 -0
  34. package/lib/compiler/version.js +93 -0
  35. package/lib/experiments/simulation/index.js +2 -0
  36. package/lib/experiments/simulation/math.js +41 -0
  37. package/lib/experiments/simulation/probe.js +215 -0
  38. package/lib/experiments/simulation/scenes.js +192 -0
  39. package/lib/experiments/simulation/solver.js +346 -0
  40. package/lib/experiments/simulation/types.js +1 -0
  41. package/lib/experiments/world/adapters.js +30 -0
  42. package/lib/experiments/world/geometry.js +180 -0
  43. package/lib/experiments/world/index.js +2 -0
  44. package/lib/experiments/world/probe.js +288 -0
  45. package/lib/experiments/world/types.js +1 -0
  46. package/lib/experiments/world/world.js +599 -0
  47. package/lib/navigation/index.js +157 -0
  48. package/lib/physics/index.js +909 -0
  49. package/lib/protocol/index.js +92 -0
  50. package/lib/renderer/acceleration.js +609 -0
  51. package/lib/renderer/browser.js +2015 -0
  52. package/lib/renderer/bsdf.js +260 -0
  53. package/lib/renderer/checkpoint.js +88 -0
  54. package/lib/renderer/compute-executor.js +1034 -0
  55. package/lib/renderer/compute-texture-assets.js +49 -0
  56. package/lib/renderer/compute-view-contract.js +1 -0
  57. package/lib/renderer/denoiser.js +361 -0
  58. package/lib/renderer/dielectric.js +360 -0
  59. package/lib/renderer/embedded-image-pixels.js +368 -0
  60. package/lib/renderer/emissive-lights.js +67 -0
  61. package/lib/renderer/environment-importance.js +55 -0
  62. package/lib/renderer/environment.js +346 -0
  63. package/lib/renderer/gameplay.js +464 -0
  64. package/lib/renderer/geometry.js +103 -0
  65. package/lib/renderer/graph-expressions.js +485 -0
  66. package/lib/renderer/graph-tsl.js +141 -0
  67. package/lib/renderer/graph.js +214 -0
  68. package/lib/renderer/managed-texture-sampling.js +29 -0
  69. package/lib/renderer/material-graphs.js +297 -0
  70. package/lib/renderer/medium.js +348 -0
  71. package/lib/renderer/models.js +1053 -0
  72. package/lib/renderer/motion-shader.js +40 -0
  73. package/lib/renderer/path-shader.js +196 -0
  74. package/lib/renderer/pathtrace.js +324 -0
  75. package/lib/renderer/postprocess-contract.js +1 -0
  76. package/lib/renderer/postprocess.js +494 -0
  77. package/lib/renderer/render-buffers.js +132 -0
  78. package/lib/renderer/render-textures.js +155 -0
  79. package/lib/renderer/startup.js +50 -0
  80. package/lib/renderer/surface-capture.js +199 -0
  81. package/lib/renderer/surface-gpu.js +270 -0
  82. package/lib/renderer/surface-motion.js +84 -0
  83. package/lib/renderer/surface-ray-shader.js +91 -0
  84. package/lib/renderer/surface-shader.js +88 -0
  85. package/lib/renderer/surface-tracer.js +1720 -0
  86. package/lib/renderer/surface.js +1425 -0
  87. package/lib/renderer/temporal-filter.js +512 -0
  88. package/lib/renderer/texture-pixels.js +207 -0
  89. package/lib/renderer/traversal-nodes.js +54 -0
  90. package/lib/renderer/ui.js +446 -0
  91. package/lib/renderer/vertex-bounds.js +127 -0
  92. package/lib/renderer/vertex-contract.js +1 -0
  93. package/lib/renderer/vertex-graphs.js +197 -0
  94. package/lib/renderer/volume-shader.js +50 -0
  95. package/lib/runtime/compute-contract.js +1 -0
  96. package/lib/runtime/compute-service.js +674 -0
  97. package/lib/runtime/contracts.js +1 -0
  98. package/lib/runtime/error.js +37 -0
  99. package/lib/runtime/events.js +189 -0
  100. package/lib/runtime/game-services.js +772 -0
  101. package/lib/runtime/host.js +839 -0
  102. package/lib/runtime/material-parameters.js +145 -0
  103. package/lib/runtime/plugins.js +1 -0
  104. package/lib/runtime/postprocess-contract.js +1 -0
  105. package/lib/runtime/postprocess.js +189 -0
  106. package/lib/runtime/prefabs.js +13 -0
  107. package/lib/runtime/random.js +88 -0
  108. package/lib/runtime/render-buffer-contract.js +1 -0
  109. package/lib/runtime/render-buffers.js +243 -0
  110. package/lib/runtime/render-capabilities.js +17 -0
  111. package/lib/runtime/render-settings.js +249 -0
  112. package/lib/runtime/render-texture-contract.js +1 -0
  113. package/lib/runtime/render-textures.js +271 -0
  114. package/lib/runtime/resources.js +648 -0
  115. package/lib/runtime/scheduler.js +765 -0
  116. package/lib/runtime/storage.js +169 -0
  117. package/lib/runtime/world.js +594 -0
  118. package/lib/schema/index.js +24 -0
  119. package/lib/sdk/graph-values.js +16 -0
  120. package/lib/sdk/index.d.ts +800 -0
  121. package/lib/sdk/index.js +82 -0
  122. package/lib/tools/api-index.js +113 -0
  123. package/lib/tools/browser-session.js +89 -0
  124. package/lib/tools/cli.js +513 -0
  125. package/lib/tools/compress-dist.js +59 -0
  126. package/lib/tools/control-client.js +153 -0
  127. package/lib/tools/control-server.js +633 -0
  128. package/lib/tools/diagnostic-code.js +3 -0
  129. package/lib/tools/doctor.js +496 -0
  130. package/lib/tools/npm/browser-cli.js +6 -0
  131. package/lib/tools/npm/entry.js +40 -0
  132. package/lib/tools/npm/layout.js +195 -0
  133. package/lib/tools/setup.js +153 -0
  134. package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
  135. package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
  136. package/licenses/@gltf-transform/core/LICENSE.md +21 -0
  137. package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
  138. package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
  139. package/licenses/@img/colour/LICENSE.md +82 -0
  140. package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
  141. package/licenses/@oxc-project/types/LICENSE +22 -0
  142. package/licenses/@rolldown/pluginutils/LICENSE +21 -0
  143. package/licenses/@tweenjs/tween.js/LICENSE +23 -0
  144. package/licenses/@types/ndarray/LICENSE +21 -0
  145. package/licenses/@types/node/LICENSE +21 -0
  146. package/licenses/@types/stats.js/LICENSE +21 -0
  147. package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
  148. package/licenses/@types/three/LICENSE +21 -0
  149. package/licenses/@types/webxr/LICENSE +21 -0
  150. package/licenses/@webgpu/types/LICENSE +26 -0
  151. package/licenses/THIRD-PARTY-NOTICES.md +139 -0
  152. package/licenses/cwise-compiler/LICENSE +22 -0
  153. package/licenses/detect-libc/LICENSE +201 -0
  154. package/licenses/esbuild/LICENSE.md +21 -0
  155. package/licenses/fast-png/LICENSE +21 -0
  156. package/licenses/fdir/LICENSE +7 -0
  157. package/licenses/fflate/LICENSE +21 -0
  158. package/licenses/iobuffer/LICENSE +22 -0
  159. package/licenses/iota-array/LICENSE +22 -0
  160. package/licenses/is-buffer/LICENSE +21 -0
  161. package/licenses/ktx-parse/LICENSE +21 -0
  162. package/licenses/lightningcss/LICENSE +373 -0
  163. package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
  164. package/licenses/meshoptimizer/LICENSE.md +21 -0
  165. package/licenses/nanoid/LICENSE +20 -0
  166. package/licenses/ndarray/LICENSE +22 -0
  167. package/licenses/ndarray-lanczos/LICENSE +21 -0
  168. package/licenses/ndarray-ops/LICENSE +22 -0
  169. package/licenses/ndarray-pixels/LICENSE +21 -0
  170. package/licenses/ngraph.path/LICENSE +21 -0
  171. package/licenses/picocolors/LICENSE +15 -0
  172. package/licenses/picomatch/LICENSE +21 -0
  173. package/licenses/playwright/LICENSE +202 -0
  174. package/licenses/playwright/NOTICE +5 -0
  175. package/licenses/playwright-core/LICENSE +202 -0
  176. package/licenses/playwright-core/NOTICE +5 -0
  177. package/licenses/postcss/LICENSE +20 -0
  178. package/licenses/prettier/LICENSE +7 -0
  179. package/licenses/property-graph/LICENSE +21 -0
  180. package/licenses/rolldown/LICENSE +25 -0
  181. package/licenses/semver/LICENSE +15 -0
  182. package/licenses/sharp/LICENSE +191 -0
  183. package/licenses/source-map-js/LICENSE +28 -0
  184. package/licenses/three/LICENSE +21 -0
  185. package/licenses/tinyglobby/LICENSE +21 -0
  186. package/licenses/tsx/LICENSE +21 -0
  187. package/licenses/typescript/LICENSE.txt +55 -0
  188. package/licenses/undici-types/LICENSE +21 -0
  189. package/licenses/uniq/LICENSE +22 -0
  190. package/licenses/vite/LICENSE.md +2084 -0
  191. package/package.json +49 -4
  192. package/web/index.html +34 -0
@@ -0,0 +1,599 @@
1
+ import { createConsumer } from "./adapters.js";
2
+ import { affectedChunks, assertCoord, assertOrigin, buildChunk, chunkId, clone, collisionBuild, coordValid, fail, heightAt, key, localPoint, navBuild, navId, normalizePoint, renderBuild, vector, } from "./geometry.js";
3
+ export function createWorldProbe(options = {}) {
4
+ const config = {
5
+ seed: options.seed ?? 12345,
6
+ chunkSize: options.chunkSize ?? 8,
7
+ cellsPerChunk: options.cellsPerChunk ?? 4,
8
+ bounds: clone(options.bounds ?? { min: [-1, -1], max: [1, 1] }),
9
+ navMaxStep: options.navMaxStep ?? 0.75,
10
+ };
11
+ if (!Number.isSafeInteger(config.seed) ||
12
+ config.seed < 0 ||
13
+ config.seed > 0xffffffff)
14
+ fail("WORLD_CONFIG", "seed must be uint32.");
15
+ if (!Number.isFinite(config.chunkSize) ||
16
+ config.chunkSize < 1 ||
17
+ config.chunkSize > 1024 ||
18
+ !Number.isInteger(config.cellsPerChunk) ||
19
+ config.cellsPerChunk < 2 ||
20
+ config.cellsPerChunk > 16)
21
+ fail("WORLD_CONFIG", "chunkSize must be in [1,1024]; cellsPerChunk is an integer in [2,16].");
22
+ if (!config.bounds ||
23
+ !coordValid(config.bounds.min) ||
24
+ !coordValid(config.bounds.max) ||
25
+ config.bounds.min.some((v, i) => config.bounds.max[i] < v || config.bounds.max[i] - v >= 8))
26
+ fail("WORLD_CONFIG", "Use a finite map no larger than 8×8 chunks.");
27
+ if (!Number.isFinite(config.navMaxStep) ||
28
+ config.navMaxStep <= 0 ||
29
+ config.navMaxStep > 100)
30
+ fail("WORLD_CONFIG", "navMaxStep must be finite in (0,100].");
31
+ const origin = clone(options.origin ?? { chunk: [0, 0], offset: [0, 0, 0] });
32
+ assertOrigin(origin, config);
33
+ let state = {
34
+ origin,
35
+ edits: new Map(),
36
+ revisions: new Map(),
37
+ chunks: new Map(),
38
+ };
39
+ let epoch = 0, disposed = false, faulted = false;
40
+ let tail = Promise.resolve();
41
+ const pending = new Map(), generations = new Map();
42
+ const metrics = {
43
+ loadRequests: 0,
44
+ cancelledLoads: 0,
45
+ staleResults: 0,
46
+ committedTransactions: 0,
47
+ failedTransactions: 0,
48
+ discardedPreparations: 0,
49
+ prepareWallMs: 0,
50
+ commitWallMs: 0,
51
+ };
52
+ const initial = {
53
+ epoch: 0,
54
+ reason: "load",
55
+ config,
56
+ origin,
57
+ chunks: [],
58
+ };
59
+ const render = createConsumer("render", renderBuild(initial), renderBuild), physics = createConsumer("physics", collisionBuild(initial), collisionBuild), nav = createConsumer("nav", navBuild(initial), navBuild);
60
+ const adapters = [render, physics, nav, ...(options.adapters ?? [])];
61
+ if (new Set(adapters.map((a) => a.name)).size !== adapters.length ||
62
+ adapters.some((a) => !a.name || typeof a.prepare !== "function"))
63
+ fail("WORLD_ADAPTER", "Adapters require unique nonempty names and prepare().");
64
+ function alive() {
65
+ if (disposed)
66
+ fail("WORLD_DISPOSED", "Create a new world after disposal.");
67
+ if (faulted)
68
+ fail("WORLD_FAULTED", "An adapter violated its rollback contract; recreate the world.");
69
+ }
70
+ function enqueue(task) {
71
+ alive();
72
+ const operation = tail.then(() => {
73
+ alive();
74
+ return task();
75
+ });
76
+ tail = operation.catch(() => { });
77
+ return operation;
78
+ }
79
+ function nextState() {
80
+ return {
81
+ origin: clone(state.origin),
82
+ edits: new Map(state.edits),
83
+ revisions: new Map(state.revisions),
84
+ chunks: new Map(state.chunks),
85
+ };
86
+ }
87
+ function bump(c, status) {
88
+ const id = key(c), generation = (generations.get(id) ?? 0) + 1;
89
+ generations.set(id, generation);
90
+ const p = pending.get(id);
91
+ if (p) {
92
+ pending.delete(id);
93
+ p.controller.abort();
94
+ if (status === "cancelled")
95
+ metrics.cancelledLoads++;
96
+ p.resolve({
97
+ status,
98
+ id: chunkId(c),
99
+ generation: p.generation,
100
+ version: p.version,
101
+ });
102
+ }
103
+ return generation;
104
+ }
105
+ const current = (id, p) => !disposed &&
106
+ !faulted &&
107
+ pending.get(id) === p &&
108
+ generations.get(id) === p.generation &&
109
+ (state.revisions.get(id) ?? 0) === p.version;
110
+ function discard(prepared) {
111
+ for (const p of prepared.reverse()) {
112
+ try {
113
+ p.discard();
114
+ metrics.discardedPreparations++;
115
+ }
116
+ catch {
117
+ faulted = true;
118
+ }
119
+ }
120
+ }
121
+ async function publish(next, reason, valid = () => !disposed) {
122
+ if (!valid())
123
+ return false;
124
+ const transaction = {
125
+ epoch: epoch + 1,
126
+ reason,
127
+ config: clone(config),
128
+ origin: clone(next.origin),
129
+ chunks: [...next.chunks.values()]
130
+ .sort((a, b) => (a.id < b.id ? -1 : 1))
131
+ .map(clone),
132
+ };
133
+ const prepared = [];
134
+ const prepareStart = performance.now();
135
+ try {
136
+ for (const adapter of adapters) {
137
+ const p = await adapter.prepare(clone(transaction));
138
+ if (!p ||
139
+ typeof p.commit !== "function" ||
140
+ typeof p.rollback !== "function" ||
141
+ typeof p.discard !== "function")
142
+ fail("WORLD_ADAPTER_CONTRACT", `${adapter.name} returned an invalid preparation.`);
143
+ prepared.push(p);
144
+ if (!valid()) {
145
+ discard(prepared);
146
+ return false;
147
+ }
148
+ }
149
+ }
150
+ catch (error) {
151
+ discard(prepared);
152
+ metrics.failedTransactions++;
153
+ fail("WORLD_PREPARE", `No world state committed: ${String(error)}`);
154
+ }
155
+ finally {
156
+ metrics.prepareWallMs += performance.now() - prepareStart;
157
+ }
158
+ const attempted = [];
159
+ const commitStart = performance.now();
160
+ try {
161
+ // No await / observer callback in this section: built-in consumers and world publish in one JS turn.
162
+ for (const p of prepared) {
163
+ attempted.push(p);
164
+ const result = p.commit();
165
+ if (result && typeof result.then === "function")
166
+ fail("WORLD_ADAPTER_CONTRACT", "commit() must be synchronous.");
167
+ }
168
+ state = next;
169
+ epoch = transaction.epoch;
170
+ metrics.committedTransactions++;
171
+ return true;
172
+ }
173
+ catch (error) {
174
+ for (const p of attempted.reverse()) {
175
+ try {
176
+ p.rollback();
177
+ }
178
+ catch {
179
+ faulted = true;
180
+ }
181
+ }
182
+ discard(prepared.filter((p) => !attempted.includes(p)));
183
+ metrics.failedTransactions++;
184
+ fail(faulted ? "WORLD_ROLLBACK" : "WORLD_COMMIT", `World state was not published; ${faulted ? "adapter rollback failed" : "all attempted consumers rolled back"}: ${String(error)}`);
185
+ }
186
+ finally {
187
+ metrics.commitWallMs += performance.now() - commitStart;
188
+ }
189
+ }
190
+ function pointValid(p) {
191
+ if (!p ||
192
+ !coordValid(p.chunk) ||
193
+ !vector(p.local, 3) ||
194
+ p.local.some((v) => Math.abs(v) > 1e9))
195
+ fail("WORLD_POINT", "Expected an integer chunk and finite local XYZ.");
196
+ }
197
+ function snapshot() {
198
+ alive();
199
+ const ordered = (m) => [...m].sort((a, b) => (a[0] < b[0] ? -1 : 1));
200
+ return {
201
+ format: "pelican-m0-world",
202
+ version: 1,
203
+ config: clone(config),
204
+ origin: clone(state.origin),
205
+ edits: ordered(state.edits).map(([k, height]) => ({
206
+ vertex: k.split(":").map(Number),
207
+ height,
208
+ })),
209
+ revisions: ordered(state.revisions).map(([k, version]) => ({
210
+ coord: k.split(":").map(Number),
211
+ version,
212
+ })),
213
+ resident: ordered(state.chunks).map(([, c]) => clone(c)),
214
+ };
215
+ }
216
+ function parseSnapshot(input) {
217
+ try {
218
+ if (!input || input.format !== "pelican-m0-world" || input.version !== 1)
219
+ throw new Error("format / version mismatch");
220
+ const c = input.config;
221
+ if (!c ||
222
+ c.seed !== config.seed ||
223
+ c.chunkSize !== config.chunkSize ||
224
+ c.cellsPerChunk !== config.cellsPerChunk ||
225
+ c.navMaxStep !== config.navMaxStep ||
226
+ JSON.stringify(c.bounds) !== JSON.stringify(config.bounds))
227
+ throw new Error("configuration mismatch");
228
+ assertOrigin(input.origin, config);
229
+ if (!Array.isArray(input.edits) ||
230
+ !Array.isArray(input.revisions) ||
231
+ !Array.isArray(input.resident))
232
+ throw new Error("missing arrays");
233
+ const limit = (config.bounds.max[0] - config.bounds.min[0] + 1) *
234
+ (config.bounds.max[1] - config.bounds.min[1] + 1);
235
+ if (input.edits.length > limit * (config.cellsPerChunk + 1) ** 2 ||
236
+ input.revisions.length > limit ||
237
+ input.resident.length > limit)
238
+ throw new Error("snapshot exceeds finite map");
239
+ const next = {
240
+ origin: clone(input.origin),
241
+ edits: new Map(),
242
+ revisions: new Map(),
243
+ chunks: new Map(),
244
+ };
245
+ for (const e of input.edits) {
246
+ if (!e ||
247
+ !vector(e.vertex, 2) ||
248
+ !e.vertex.every(Number.isSafeInteger) ||
249
+ !Number.isFinite(e.height) ||
250
+ Math.abs(e.height) > 100 ||
251
+ !affectedChunks(e.vertex, config).length ||
252
+ next.edits.has(key(e.vertex)))
253
+ throw new Error("invalid / duplicate persistent vertex");
254
+ next.edits.set(key(e.vertex), e.height);
255
+ }
256
+ for (const r of input.revisions) {
257
+ assertCoord(r.coord, config);
258
+ if (!Number.isSafeInteger(r.version) ||
259
+ r.version < 1 ||
260
+ next.revisions.has(key(r.coord)))
261
+ throw new Error("invalid / duplicate revision");
262
+ next.revisions.set(key(r.coord), r.version);
263
+ }
264
+ for (const e of input.edits)
265
+ for (const coord of affectedChunks(e.vertex, config))
266
+ if (!next.revisions.has(key(coord)))
267
+ throw new Error("edited chunk lacks revision");
268
+ for (const c of input.resident) {
269
+ assertCoord(c.coord, config);
270
+ const id = key(c.coord), expected = buildChunk(c.coord, next.revisions.get(id) ?? 0, config, next.edits);
271
+ if (next.chunks.has(id) ||
272
+ c.id !== expected.id ||
273
+ c.version !== expected.version ||
274
+ !Array.isArray(c.heights) ||
275
+ c.heights.length !== expected.heights.length ||
276
+ c.heights.some((h, i) => h !== expected.heights[i]))
277
+ throw new Error("resident mesh / height data disagrees with persistent state");
278
+ next.chunks.set(id, expected);
279
+ }
280
+ return next;
281
+ }
282
+ catch (error) {
283
+ fail("WORLD_SNAPSHOT", `Snapshot rejected before commit: ${String(error)}`);
284
+ }
285
+ }
286
+ return {
287
+ loadChunk(coord) {
288
+ alive();
289
+ assertCoord(coord, config);
290
+ coord = [...coord];
291
+ metrics.loadRequests++;
292
+ const id = key(coord), generation = bump(coord, "stale"), version = state.revisions.get(id) ?? 0;
293
+ if (state.chunks.has(id))
294
+ return Promise.resolve({
295
+ status: "already-loaded",
296
+ id: chunkId(coord),
297
+ generation,
298
+ version,
299
+ });
300
+ let p;
301
+ const promise = new Promise((resolve, reject) => {
302
+ p = {
303
+ generation,
304
+ version,
305
+ controller: new AbortController(),
306
+ resolve,
307
+ reject,
308
+ };
309
+ pending.set(id, p);
310
+ });
311
+ void Promise.resolve().then(async () => {
312
+ try {
313
+ if (options.beforeBuild)
314
+ await options.beforeBuild({ coord: [...coord], generation, version }, p.controller.signal);
315
+ if (!current(id, p)) {
316
+ metrics.staleResults++;
317
+ return;
318
+ }
319
+ await enqueue(async () => {
320
+ if (!current(id, p)) {
321
+ metrics.staleResults++;
322
+ return;
323
+ }
324
+ const next = nextState();
325
+ next.chunks.set(id, buildChunk(coord, version, config, next.edits));
326
+ const committed = await publish(next, "load", () => current(id, p));
327
+ if (committed) {
328
+ pending.delete(id);
329
+ p.resolve({
330
+ status: "loaded",
331
+ id: chunkId(coord),
332
+ generation,
333
+ version,
334
+ });
335
+ }
336
+ else {
337
+ metrics.staleResults++;
338
+ if (pending.get(id) === p) {
339
+ pending.delete(id);
340
+ p.reject(new Error("WORLD_FAULTED: load preparation was not publishable."));
341
+ }
342
+ }
343
+ });
344
+ }
345
+ catch (error) {
346
+ if (pending.get(id) === p) {
347
+ pending.delete(id);
348
+ p.reject(error);
349
+ }
350
+ else
351
+ metrics.staleResults++;
352
+ }
353
+ });
354
+ return promise;
355
+ },
356
+ cancelLoad(coord) {
357
+ alive();
358
+ assertCoord(coord, config);
359
+ if (!pending.has(key(coord)))
360
+ return false;
361
+ bump(coord, "cancelled");
362
+ return true;
363
+ },
364
+ async unloadChunk(coord) {
365
+ alive();
366
+ assertCoord(coord, config);
367
+ coord = [...coord];
368
+ const id = key(coord), generation = bump(coord, "cancelled");
369
+ return enqueue(async () => {
370
+ if (generations.get(id) !== generation)
371
+ return { status: "stale", id: chunkId(coord) };
372
+ if (!state.chunks.has(id))
373
+ return { status: "absent", id: chunkId(coord) };
374
+ const next = nextState();
375
+ next.chunks.delete(id);
376
+ const committed = await publish(next, "unload", () => !disposed && generations.get(id) === generation);
377
+ return {
378
+ status: committed ? "unloaded" : "stale",
379
+ id: chunkId(coord),
380
+ };
381
+ });
382
+ },
383
+ async applyEdits(edits) {
384
+ alive();
385
+ if (!Array.isArray(edits) || edits.length > 4096)
386
+ fail("WORLD_EDIT", "Supply at most 4096 height edits.");
387
+ const validated = edits.map((e) => {
388
+ if (!e)
389
+ fail("WORLD_EDIT", "Missing height edit.");
390
+ assertCoord(e.chunk, config);
391
+ if (!Number.isInteger(e.x) ||
392
+ !Number.isInteger(e.z) ||
393
+ e.x < 0 ||
394
+ e.z < 0 ||
395
+ e.x > config.cellsPerChunk ||
396
+ e.z > config.cellsPerChunk ||
397
+ !Number.isFinite(e.height) ||
398
+ Math.abs(e.height) > 100)
399
+ fail("WORLD_EDIT", "Vertex indices must address the chunk grid; height must be finite in [-100,100].");
400
+ return {
401
+ vertex: [
402
+ e.chunk[0] * config.cellsPerChunk + e.x,
403
+ e.chunk[1] * config.cellsPerChunk + e.z,
404
+ ],
405
+ height: e.height,
406
+ };
407
+ });
408
+ await enqueue(async () => {
409
+ const next = nextState(), affected = new Map();
410
+ for (const e of validated) {
411
+ next.edits.set(key(e.vertex), e.height);
412
+ for (const c of affectedChunks(e.vertex, config))
413
+ affected.set(key(c), c);
414
+ }
415
+ if (!affected.size)
416
+ return;
417
+ for (const [id, c] of affected) {
418
+ const version = (next.revisions.get(id) ?? 0) + 1;
419
+ next.revisions.set(id, version);
420
+ if (next.chunks.has(id))
421
+ next.chunks.set(id, buildChunk(c, version, config, next.edits));
422
+ }
423
+ if (await publish(next, "edit"))
424
+ for (const c of affected.values())
425
+ bump(c, "stale");
426
+ });
427
+ },
428
+ async setOrigin(nextOrigin) {
429
+ alive();
430
+ assertOrigin(nextOrigin, config);
431
+ const o = clone(nextOrigin);
432
+ await enqueue(async () => {
433
+ const next = nextState();
434
+ next.origin = o;
435
+ await publish(next, "origin");
436
+ });
437
+ },
438
+ snapshot,
439
+ async restore(input) {
440
+ alive();
441
+ const next = parseSnapshot(input);
442
+ await enqueue(async () => {
443
+ if (await publish(next, "restore"))
444
+ for (const [id] of pending)
445
+ bump(id.split(":").map(Number), "stale");
446
+ });
447
+ },
448
+ inspect() {
449
+ const r = render.read(), p = physics.read(), n = nav.read(), mismatches = [];
450
+ if (!disposed) {
451
+ if (r.epoch !== epoch || p.epoch !== epoch || n.epoch !== epoch)
452
+ mismatches.push("epoch");
453
+ const expected = [...state.chunks.values()]
454
+ .map((c) => ({ id: c.id, version: c.version }))
455
+ .sort((a, b) => (a.id < b.id ? -1 : 1));
456
+ for (const [name, list] of [
457
+ ["render", r.chunks],
458
+ ["physics", p.chunks],
459
+ ["nav", n.versions],
460
+ ]) {
461
+ const actual = list
462
+ .map((c) => ({ id: c.id, version: c.version }))
463
+ .sort((a, b) => (a.id < b.id ? -1 : 1));
464
+ if (JSON.stringify(actual) !== JSON.stringify(expected))
465
+ mismatches.push(name);
466
+ }
467
+ }
468
+ return {
469
+ disposed,
470
+ faulted,
471
+ epoch,
472
+ origin: clone(state.origin),
473
+ resident: [...state.chunks.values()].map((c) => ({
474
+ id: c.id,
475
+ version: c.version,
476
+ })),
477
+ pending: [...pending].map(([id, p]) => ({
478
+ id: `chunk:${id}`,
479
+ generation: p.generation,
480
+ })),
481
+ persistentEdits: state.edits.size,
482
+ persistentRevisions: state.revisions.size,
483
+ consumers: {
484
+ renderEpoch: r.epoch,
485
+ physicsEpoch: p.epoch,
486
+ navEpoch: n.epoch,
487
+ consistent: !faulted && !mismatches.length,
488
+ mismatches,
489
+ },
490
+ metrics: clone(metrics),
491
+ resources: {
492
+ residentHeightValues: [...state.chunks.values()].reduce((sum, c) => sum + c.heights.length, 0),
493
+ renderScalars: r.chunks.reduce((sum, c) => sum + c.positions.length + c.indices.length, 0),
494
+ collisionHeightValues: p.chunks.reduce((sum, c) => sum + c.heights.length, 0),
495
+ navNodes: n.nodes.length,
496
+ navEdges: n.nodes.reduce((sum, node) => sum + node.neighbors.length, 0),
497
+ },
498
+ };
499
+ },
500
+ getRenderState() {
501
+ alive();
502
+ return clone(render.read());
503
+ },
504
+ getCollisionState() {
505
+ alive();
506
+ return clone(physics.read());
507
+ },
508
+ getNavigationState() {
509
+ alive();
510
+ return clone(nav.read());
511
+ },
512
+ sampleHeight(point) {
513
+ alive();
514
+ pointValid(point);
515
+ const normalized = normalizePoint(point, config);
516
+ const c = physics
517
+ .read()
518
+ .chunks.find((c) => key(c.coord) === key(normalized.chunk));
519
+ if (!c)
520
+ return null;
521
+ return {
522
+ height: heightAt(c.heights, c.cells, normalized.local[0] / c.cellSize, normalized.local[2] / c.cellSize),
523
+ chunkId: c.id,
524
+ version: c.version,
525
+ };
526
+ },
527
+ findPath(start, goal) {
528
+ alive();
529
+ for (const a of [start, goal])
530
+ if (!a ||
531
+ !coordValid(a.chunk) ||
532
+ !vector(a.cell, 2) ||
533
+ a.cell.some((v) => !Number.isInteger(v) || v < 0 || v >= config.cellsPerChunk))
534
+ fail("WORLD_NAV", "Expected a chunk and an integer cell inside its grid.");
535
+ const n = nav.read(), nodes = new Map(n.nodes.map((node) => [node.id, node])), source = navId(start), target = navId(goal);
536
+ if (!nodes.has(source) || !nodes.has(target))
537
+ return { epoch, found: false, visited: 0, nodes: [] };
538
+ const queue = [source], parents = new Map([[source, null]]);
539
+ let head = 0;
540
+ while (head < queue.length) {
541
+ const id = queue[head++];
542
+ if (id === target)
543
+ break;
544
+ for (const neighbor of nodes.get(id).neighbors)
545
+ if (!parents.has(neighbor)) {
546
+ parents.set(neighbor, id);
547
+ queue.push(neighbor);
548
+ }
549
+ }
550
+ if (!parents.has(target))
551
+ return { epoch, found: false, visited: head, nodes: [] };
552
+ const path = [];
553
+ for (let id = target; id !== null; id = parents.get(id))
554
+ path.push(id);
555
+ return {
556
+ epoch,
557
+ found: true,
558
+ visited: head,
559
+ nodes: clone(path.reverse().map((id) => nodes.get(id))),
560
+ };
561
+ },
562
+ toLocal(point) {
563
+ alive();
564
+ pointValid(point);
565
+ return localPoint(point, state.origin, config);
566
+ },
567
+ fromLocal(point) {
568
+ alive();
569
+ if (!vector(point, 3) || point.some((v) => Math.abs(v) > 1e9))
570
+ fail("WORLD_POINT", "Expected finite origin-local XYZ.");
571
+ const p = normalizePoint({
572
+ chunk: [...state.origin.chunk],
573
+ local: point.map((v, i) => v + state.origin.offset[i]),
574
+ }, config);
575
+ if (!coordValid(p.chunk))
576
+ fail("WORLD_POINT", "Converted coordinate exceeds prototype range.");
577
+ return p;
578
+ },
579
+ dispose() {
580
+ if (disposed)
581
+ return;
582
+ disposed = true;
583
+ for (const [id] of pending)
584
+ bump(id.split(":").map(Number), "cancelled");
585
+ for (const adapter of adapters) {
586
+ try {
587
+ adapter.dispose?.();
588
+ }
589
+ catch {
590
+ faulted = true;
591
+ }
592
+ }
593
+ state.chunks.clear();
594
+ state.edits.clear();
595
+ state.revisions.clear();
596
+ generations.clear();
597
+ },
598
+ };
599
+ }