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,765 @@
1
+ import { SYSTEM_PHASES, } from "./contracts.js";
2
+ import { createEventBus, validateEventSnapshot, } from "./events.js";
3
+ import { cloneSerializable, createRandom, validateRandomSnapshot, } from "./random.js";
4
+ import { cloneSchema, identifier, record as worldRecord, values, } from "./storage.js";
5
+ function integer(value, label, minimum = 0) {
6
+ if (!Number.isSafeInteger(value) || value < minimum)
7
+ throw new Error(`TC_SCHEDULER_ARGUMENT: ${label} must be a safe integer >= ${minimum}.`);
8
+ }
9
+ function seconds(value, label) {
10
+ if (!Number.isFinite(value) || value < 0)
11
+ throw new Error(`TC_SCHEDULER_ARGUMENT: ${label} must be finite and non-negative.`);
12
+ }
13
+ function canonical(value) {
14
+ const sort = (item) => Array.isArray(item)
15
+ ? item.map(sort)
16
+ : item && typeof item === "object"
17
+ ? Object.fromEntries(Object.entries(item)
18
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
19
+ .map(([key, val]) => [key, sort(val)]))
20
+ : item;
21
+ return JSON.stringify(sort(value));
22
+ }
23
+ function schedule(input) {
24
+ if (!Array.isArray(input))
25
+ throw new Error("TC_SCHEDULER_SYSTEM: Expected a system array.");
26
+ const systems = input.map((system) => {
27
+ if (!system ||
28
+ typeof system.id !== "string" ||
29
+ !system.id.trim() ||
30
+ typeof system.run !== "function")
31
+ throw new Error("TC_SCHEDULER_SYSTEM: Expected a named synchronous system.");
32
+ if (system.run.constructor.name === "AsyncFunction")
33
+ throw new Error("TC_SCHEDULER_ASYNC: Systems must be synchronous.");
34
+ const phase = system.phase ?? "update";
35
+ if (!SYSTEM_PHASES.includes(phase))
36
+ throw new Error(`TC_SCHEDULER_PHASE: Unknown phase ${phase}.`);
37
+ const array = (value) => {
38
+ if (value !== undefined &&
39
+ (!Array.isArray(value) ||
40
+ value.some((id) => typeof id !== "string" || !id.trim())))
41
+ throw new Error(`TC_SCHEDULER_SYSTEM: Invalid access/dependency list for ${system.id}.`);
42
+ return [...(value ?? [])];
43
+ };
44
+ if (system.structural !== undefined &&
45
+ typeof system.structural !== "boolean")
46
+ throw new Error("TC_SCHEDULER_SYSTEM: Invalid structural access.");
47
+ return {
48
+ id: system.id,
49
+ phase,
50
+ before: array(system.before),
51
+ after: array(system.after),
52
+ ...(system.reads === undefined ? {} : { reads: array(system.reads) }),
53
+ ...(system.writes === undefined ? {} : { writes: array(system.writes) }),
54
+ ...(system.structural === undefined
55
+ ? {}
56
+ : { structural: system.structural }),
57
+ run: system.run,
58
+ };
59
+ });
60
+ const byId = new Map(systems.map((system) => [system.id, system]));
61
+ if (byId.size !== systems.length)
62
+ throw new Error("TC_SCHEDULER_DUPLICATE: Duplicate system id.");
63
+ const edges = new Map(systems.map((s) => [s.id, new Set()]));
64
+ const indegree = new Map(systems.map((s) => [s.id, 0]));
65
+ const edge = (from, to) => {
66
+ const a = byId.get(from), b = byId.get(to);
67
+ if (!a || !b)
68
+ throw new Error(`TC_SCHEDULER_DEPENDENCY: Unknown dependency ${!a ? from : to}.`);
69
+ const ap = SYSTEM_PHASES.indexOf(a.phase), bp = SYSTEM_PHASES.indexOf(b.phase);
70
+ if (ap > bp)
71
+ throw new Error(`TC_SCHEDULER_PHASE_ORDER: ${from} cannot precede earlier phase ${to}.`);
72
+ if (ap === bp && !edges.get(from).has(to)) {
73
+ edges.get(from).add(to);
74
+ indegree.set(to, indegree.get(to) + 1);
75
+ }
76
+ };
77
+ for (const s of systems) {
78
+ for (const id of s.before)
79
+ edge(s.id, id);
80
+ for (const id of s.after)
81
+ edge(id, s.id);
82
+ }
83
+ const ordered = [];
84
+ for (const phase of SYSTEM_PHASES) {
85
+ const pending = systems.filter((s) => s.phase === phase);
86
+ while (pending.length) {
87
+ const index = pending.findIndex((s) => indegree.get(s.id) === 0);
88
+ if (index < 0)
89
+ throw new Error(`TC_SCHEDULER_CYCLE: Cyclic systems in phase ${phase}.`);
90
+ const [next] = pending.splice(index, 1);
91
+ ordered.push(next);
92
+ for (const id of edges.get(next.id))
93
+ indegree.set(id, indegree.get(id) - 1);
94
+ }
95
+ }
96
+ const diagnostics = [];
97
+ for (let i = 0; i < ordered.length; i++)
98
+ for (let j = i + 1; j < ordered.length; j++) {
99
+ const a = ordered[i], b = ordered[j];
100
+ const resources = [
101
+ ...new Set([
102
+ ...(a.writes ?? []).filter((key) => b.writes?.includes(key) || b.reads?.includes(key)),
103
+ ...(b.writes ?? []).filter((key) => a.reads?.includes(key)),
104
+ ...(a.writes === undefined ||
105
+ b.writes === undefined ||
106
+ a.reads === undefined ||
107
+ b.reads === undefined
108
+ ? ["$undeclared-access"]
109
+ : []),
110
+ ...(a.structural !== false || b.structural !== false
111
+ ? ["$structure"]
112
+ : []),
113
+ ]),
114
+ ].sort();
115
+ if (resources.length)
116
+ diagnostics.push({
117
+ code: "TC_SCHEDULER_CONFLICT",
118
+ first: a.id,
119
+ second: b.id,
120
+ resources,
121
+ message: `Sequential execution: ${a.id} before ${b.id}; shared access is not automatic parallelism.`,
122
+ });
123
+ }
124
+ return {
125
+ ordered,
126
+ descriptors: ordered.map(({ run: _run, ...descriptor }) => descriptor),
127
+ diagnostics,
128
+ };
129
+ }
130
+ /** One scheduler per world. step() is the already-fixed driver entry point;
131
+ * advance() is an alternative wall-clock driver, never a second fixed-step layer.
132
+ * Flush boundaries: start tick, after input events, end each phase. Failed ticks
133
+ * may have changed world/random/events: pause and require a known-good restore. */
134
+ export function createScheduler({ world, systems = [], fixedDt = 1 / 60, maxSubsteps = 8, seed = 0, }) {
135
+ if (!Number.isFinite(fixedDt) || fixedDt <= 0)
136
+ throw new Error("TC_SCHEDULER_DT: fixedDt must be positive and finite.");
137
+ integer(maxSubsteps, "maxSubsteps", 1);
138
+ const random = createRandom(seed);
139
+ let plan = schedule(systems);
140
+ let completedTick = 0, accumulator = 0, compensation = 0, paused = false, running = false;
141
+ let fault = null, delivered = [];
142
+ let recording = null, replaying = false, disposed = false;
143
+ let asyncActive = false, driving = false;
144
+ const currentTick = () => completedTick + (running ? 1 : 0);
145
+ const ensure = () => {
146
+ if (disposed)
147
+ throw new Error("TC_SCHEDULER_DISPOSED: Scheduler is disposed.");
148
+ };
149
+ const mutable = () => {
150
+ ensure();
151
+ if (asyncActive && !driving && !running)
152
+ throw new Error("TC_SCHEDULER_ASYNC_ACTIVE: An async boundary is pending.");
153
+ };
154
+ const safe = () => {
155
+ mutable();
156
+ if (running)
157
+ throw new Error("TC_SCHEDULER_SAFE_POINT: Operation requires a tick boundary.");
158
+ };
159
+ const record = (action) => {
160
+ if (recording && !running && !replaying)
161
+ recording.actions.push(cloneSerializable(action));
162
+ };
163
+ const bus = createEventBus({
164
+ currentTick,
165
+ defaultSource: () => (running ? "system" : "external"),
166
+ dispatch: (id, access, callback) => world.runSystem(`event:${id}`, access, callback),
167
+ beforeSubscribe: () => {
168
+ safe();
169
+ if (recording || replaying)
170
+ throw new Error("TC_TRACE_SUBSCRIPTION: Register listeners before recording/replay.");
171
+ },
172
+ beforeEmit: (event) => {
173
+ mutable();
174
+ if (fault)
175
+ throw new Error("TC_SCHEDULER_FAULT: Restore a known-good snapshot before emitting.");
176
+ record({ kind: "emit", event, atTick: currentTick() });
177
+ },
178
+ onDelivery: (events) => {
179
+ delivered = events;
180
+ },
181
+ });
182
+ const events = Object.assign((type) => {
183
+ ensure();
184
+ return cloneSerializable(type === undefined
185
+ ? delivered
186
+ : delivered.filter((e) => e.type === type));
187
+ }, { on: bus.on, emit: bus.emit });
188
+ const backlog = () => Math.max(0, Math.floor((accumulator + fixedDt * Number.EPSILON * 16) / fixedDt));
189
+ const addTime = (amount) => {
190
+ const adjusted = amount - compensation, sum = accumulator + adjusted;
191
+ compensation = sum - accumulator - adjusted;
192
+ accumulator = sum;
193
+ if (Math.abs(accumulator) < fixedDt * Number.EPSILON * 16) {
194
+ accumulator = 0;
195
+ compensation = 0;
196
+ }
197
+ };
198
+ const report = (executed, budgetExhausted = false, acceptedSeconds = 0, ignoredSeconds = 0) => ({
199
+ status: fault
200
+ ? "faulted"
201
+ : paused
202
+ ? "paused"
203
+ : budgetExhausted
204
+ ? "backlogged"
205
+ : "completed",
206
+ executed,
207
+ tick: completedTick,
208
+ time: completedTick * fixedDt,
209
+ backlogTicks: backlog(),
210
+ backlogSeconds: accumulator,
211
+ interpolationAlpha: Math.min(1, Math.max(0, accumulator / fixedDt)),
212
+ budgetExhausted,
213
+ acceptedSeconds,
214
+ ignoredSeconds,
215
+ fault: cloneSerializable(fault),
216
+ });
217
+ const tick = () => {
218
+ if (!Number.isSafeInteger(completedTick + 1) ||
219
+ !Number.isFinite((completedTick + 1) * fixedDt))
220
+ throw new Error("TC_SCHEDULER_LIMIT: Tick/time range exhausted.");
221
+ let stage = "tick:start";
222
+ running = true;
223
+ try {
224
+ world.flush();
225
+ stage = "events";
226
+ const delivery = bus.deliver(currentTick());
227
+ if (delivery.faults.length) {
228
+ fault = {
229
+ code: "TC_SCHEDULER_LISTENER",
230
+ tick: currentTick(),
231
+ stage,
232
+ message: "Input event listeners failed; batch completed; no rollback.",
233
+ partialState: true,
234
+ eventFaults: delivery.faults,
235
+ };
236
+ paused = true;
237
+ return false;
238
+ }
239
+ world.flush();
240
+ for (const phase of SYSTEM_PHASES) {
241
+ for (const system of plan.ordered) {
242
+ if (system.phase !== phase)
243
+ continue;
244
+ stage = `system:${system.id}`;
245
+ world.runSystem(system.id, {
246
+ ...(system.reads === undefined ? {} : { reads: system.reads }),
247
+ ...(system.writes === undefined ? {} : { writes: system.writes }),
248
+ ...(system.structural === undefined
249
+ ? {}
250
+ : { structural: system.structural }),
251
+ }, () => {
252
+ const result = system.run(fixedDt);
253
+ if (result &&
254
+ (typeof result === "object" || typeof result === "function") &&
255
+ "then" in result) {
256
+ void Promise.resolve(result).catch(() => { });
257
+ throw new Error("TC_SCHEDULER_ASYNC: Systems must complete synchronously.");
258
+ }
259
+ });
260
+ }
261
+ stage = `flush:${phase}`;
262
+ world.flush();
263
+ }
264
+ completedTick++;
265
+ return true;
266
+ }
267
+ catch (error) {
268
+ paused = true;
269
+ fault = {
270
+ code: "TC_SCHEDULER_TICK",
271
+ tick: currentTick(),
272
+ stage,
273
+ message: error instanceof Error ? error.message : String(error),
274
+ partialState: true,
275
+ eventFaults: [],
276
+ };
277
+ return false;
278
+ }
279
+ finally {
280
+ running = false;
281
+ }
282
+ };
283
+ const validateSnapshot = (value) => {
284
+ const data = cloneSerializable(value);
285
+ if (!data ||
286
+ data.format !== "pelican.scheduler" ||
287
+ data.version !== 1 ||
288
+ data.fixedDt !== fixedDt ||
289
+ data.maxSubsteps !== maxSubsteps)
290
+ throw new Error("TC_SCHEDULER_SNAPSHOT_VERSION: Unsupported snapshot/configuration.");
291
+ integer(data.tick, "snapshot tick");
292
+ seconds(data.accumulator, "snapshot accumulator");
293
+ if (!Number.isFinite(data.compensation) ||
294
+ Math.abs(data.compensation) >
295
+ Math.max(fixedDt, data.accumulator) * Number.EPSILON * 4 ||
296
+ !Number.isFinite(data.time) ||
297
+ data.time !== data.tick * fixedDt ||
298
+ !Number.isSafeInteger(Math.floor(data.accumulator / fixedDt)) ||
299
+ Math.floor(data.accumulator / fixedDt) >
300
+ Number.MAX_SAFE_INTEGER - data.tick ||
301
+ typeof data.paused !== "boolean")
302
+ throw new Error("TC_SCHEDULER_SNAPSHOT: Invalid clock state.");
303
+ if (canonical(data.systems) !== canonical(plan.descriptors) ||
304
+ canonical(data.listeners) !== canonical(bus.inspect().listeners))
305
+ throw new Error("TC_SCHEDULER_SNAPSHOT_SYSTEMS: Snapshot requires the same systems/listener contract.");
306
+ validateRandomSnapshot(data.random);
307
+ validateEventSnapshot(data.events, data.tick);
308
+ if (!Array.isArray(data.delivered))
309
+ throw new Error("TC_SCHEDULER_SNAPSHOT: Invalid delivered events.");
310
+ const history = validateEventSnapshot({
311
+ format: "pelican.events",
312
+ version: 1,
313
+ nextSequence: data.events.nextSequence,
314
+ queue: data.delivered,
315
+ }, -1);
316
+ if ((data.tick === 0 && data.delivered.length > 0) ||
317
+ history.queue.some((e) => e.tick !== data.tick) ||
318
+ data.delivered.some((e) => data.events.queue.some((q) => q.sequence === e.sequence)))
319
+ throw new Error("TC_SCHEDULER_SNAPSHOT: Inconsistent delivered event history.");
320
+ if (!data.world ||
321
+ data.world.format !== "pelican.world" ||
322
+ data.world.version !== 1 ||
323
+ !Array.isArray(data.world.components) ||
324
+ !Array.isArray(data.world.entities))
325
+ throw new Error("TC_SCHEDULER_WORLD_VERSION: Unsupported or malformed world snapshot.");
326
+ // Reuse World's field/schema validators, and validate graph identity without
327
+ // touching the live World. This also preflights BOTH trace endpoints.
328
+ const definitions = new Map();
329
+ for (const definition of data.world.components) {
330
+ worldRecord(definition, "Snapshot component");
331
+ identifier(definition.id, "Component id");
332
+ if (definition.id === "Transform" || definitions.has(definition.id))
333
+ throw new Error("TC_SCHEDULER_SNAPSHOT: Duplicate/reserved component.");
334
+ if (definition.version !== undefined)
335
+ integer(definition.version, "component version", 1);
336
+ definitions.set(definition.id, cloneSchema(definition.schema));
337
+ }
338
+ const transforms = {
339
+ position: { type: "vec3", default: [0, 0, 0] },
340
+ rotation: { type: "vec3", default: [0, 0, 0] },
341
+ scale: { type: "vec3", default: [1, 1, 1] },
342
+ };
343
+ const parents = new Map();
344
+ for (const entity of data.world.entities) {
345
+ worldRecord(entity, "Snapshot entity");
346
+ identifier(entity.id, "Entity id");
347
+ if (parents.has(entity.id))
348
+ throw new Error("TC_SCHEDULER_SNAPSHOT: Duplicate entity.");
349
+ if (entity.parent !== undefined)
350
+ identifier(entity.parent, "Parent id");
351
+ parents.set(entity.id, entity.parent);
352
+ values(transforms, entity.transform, true, true);
353
+ worldRecord(entity.components, "Snapshot components");
354
+ for (const [name, fields] of Object.entries(entity.components)) {
355
+ const schema = definitions.get(name);
356
+ if (!schema)
357
+ throw new Error(`TC_SCHEDULER_SNAPSHOT: Unknown/reserved component ${name}.`);
358
+ values(schema, fields, true, true);
359
+ }
360
+ }
361
+ const visited = new Set();
362
+ for (const id of parents.keys()) {
363
+ const path = new Set();
364
+ let current = id;
365
+ while (current !== undefined && !visited.has(current)) {
366
+ if (!parents.has(current))
367
+ throw new Error("TC_SCHEDULER_SNAPSHOT: Missing parent entity.");
368
+ if (path.has(current))
369
+ throw new Error("TC_SCHEDULER_SNAPSHOT: Parent cycle.");
370
+ path.add(current);
371
+ current = parents.get(current);
372
+ }
373
+ for (const entity of path)
374
+ visited.add(entity);
375
+ }
376
+ return data;
377
+ };
378
+ const snapshot = () => {
379
+ safe();
380
+ if (fault)
381
+ throw new Error("TC_SCHEDULER_FAULT: Failed partial tick cannot be captured as a successful checkpoint.");
382
+ if (world.inspect().pendingCommands)
383
+ throw new Error("TC_SCHEDULER_PENDING: Snapshot requires an empty structural command queue.");
384
+ return cloneSerializable({
385
+ format: "pelican.scheduler",
386
+ version: 1,
387
+ fixedDt,
388
+ maxSubsteps,
389
+ tick: completedTick,
390
+ time: completedTick * fixedDt,
391
+ accumulator,
392
+ compensation,
393
+ paused,
394
+ systems: plan.descriptors,
395
+ listeners: bus.inspect().listeners,
396
+ world: world.snapshot(),
397
+ random: random.snapshot(),
398
+ events: bus.snapshot(),
399
+ delivered,
400
+ });
401
+ };
402
+ const restore = (value) => {
403
+ safe();
404
+ if (recording)
405
+ throw new Error("TC_TRACE_RESTORE: Stop recording before restoring a checkpoint.");
406
+ const data = validateSnapshot(value);
407
+ // World.restore builds/validates a candidate before commit. All scheduler
408
+ // fields are validated before calling it; malformed input changes neither.
409
+ world.restore(data.world);
410
+ random.restore(data.random);
411
+ bus.restore(data.events, data.tick);
412
+ completedTick = data.tick;
413
+ accumulator = data.accumulator;
414
+ compensation = data.compensation;
415
+ paused = data.paused;
416
+ delivered = data.delivered;
417
+ fault = null;
418
+ };
419
+ function* stepTicks(count = 1) {
420
+ safe();
421
+ integer(count, "count");
422
+ if (count > Number.MAX_SAFE_INTEGER - completedTick ||
423
+ !Number.isFinite((completedTick + count) * fixedDt))
424
+ throw new Error("TC_SCHEDULER_LIMIT: Step count exceeds clock range.");
425
+ record({ kind: "step", count, atTick: completedTick });
426
+ let executed = 0;
427
+ while (executed < count && !paused && !fault) {
428
+ if (!tick())
429
+ break;
430
+ executed++;
431
+ yield;
432
+ ensure();
433
+ }
434
+ return report(executed);
435
+ }
436
+ function* advanceTicks(realSeconds) {
437
+ safe();
438
+ seconds(realSeconds, "realSeconds");
439
+ if (!Number.isFinite(accumulator + realSeconds) ||
440
+ Math.floor((accumulator + realSeconds) / fixedDt) >
441
+ Number.MAX_SAFE_INTEGER - completedTick)
442
+ throw new Error("TC_SCHEDULER_LIMIT: Wall-clock backlog exceeds clock range.");
443
+ record({ kind: "advance", seconds: realSeconds, atTick: completedTick });
444
+ if (paused || fault)
445
+ return report(0, false, 0, realSeconds);
446
+ addTime(realSeconds);
447
+ let executed = 0;
448
+ while (backlog() && executed < maxSubsteps && !paused && !fault) {
449
+ if (!tick())
450
+ break;
451
+ addTime(-fixedDt);
452
+ executed++;
453
+ yield;
454
+ ensure();
455
+ }
456
+ return report(executed, !fault && !paused && executed === maxSubsteps && backlog() > 0, realSeconds);
457
+ }
458
+ const drain = (iterator) => {
459
+ let next = iterator.next();
460
+ while (!next.done)
461
+ next = iterator.next();
462
+ return next.value;
463
+ };
464
+ const step = (count = 1) => drain(stepTicks(count));
465
+ const advance = (realSeconds) => drain(advanceTicks(realSeconds));
466
+ const pause = () => {
467
+ mutable();
468
+ record({ kind: "pause", atTick: currentTick() });
469
+ paused = true;
470
+ };
471
+ const resume = () => {
472
+ safe();
473
+ if (fault)
474
+ throw new Error("TC_SCHEDULER_FAULT: Cannot resume a partial failed tick; restore a known-good checkpoint.");
475
+ record({ kind: "resume", atTick: completedTick });
476
+ paused = false;
477
+ };
478
+ const nextRandom = () => {
479
+ mutable();
480
+ if (fault)
481
+ throw new Error("TC_SCHEDULER_FAULT: Restore a known-good checkpoint before drawing random values.");
482
+ const value = random.next();
483
+ record({ kind: "random", value, atTick: currentTick() });
484
+ return value;
485
+ };
486
+ const validateTrace = (value) => {
487
+ const trace = cloneSerializable(value);
488
+ if (!trace ||
489
+ trace.format !== "pelican.trace" ||
490
+ trace.version !== 1 ||
491
+ !Array.isArray(trace.actions))
492
+ throw new Error("TC_TRACE_VERSION: Unsupported or malformed recorded trace.");
493
+ validateSnapshot(trace.initial);
494
+ validateSnapshot(trace.final);
495
+ for (const action of trace.actions) {
496
+ if (!action || typeof action !== "object")
497
+ throw new Error("TC_TRACE_ACTION: Invalid recorded action.");
498
+ integer(action.atTick, "recorded atTick");
499
+ if (action.kind === "step") {
500
+ integer(action.count, "recorded count");
501
+ if (action.count > Number.MAX_SAFE_INTEGER - action.atTick ||
502
+ !Number.isFinite((action.atTick + action.count) * fixedDt))
503
+ throw new Error("TC_TRACE_ACTION: Recorded step exceeds clock range.");
504
+ }
505
+ else if (action.kind === "advance") {
506
+ seconds(action.seconds, "recorded seconds");
507
+ if (Math.floor(action.seconds / fixedDt) >
508
+ Number.MAX_SAFE_INTEGER - action.atTick)
509
+ throw new Error("TC_TRACE_ACTION: Recorded advance exceeds clock range.");
510
+ }
511
+ else if (action.kind === "random") {
512
+ if (!Number.isFinite(action.value) ||
513
+ action.value < 0 ||
514
+ action.value >= 1)
515
+ throw new Error("TC_TRACE_ACTION: Invalid recorded random value.");
516
+ }
517
+ else if (action.kind === "emit") {
518
+ const e = action.event;
519
+ if (!e ||
520
+ !Number.isSafeInteger(e.sequence) ||
521
+ e.sequence < 0 ||
522
+ !Number.isSafeInteger(e.sequence + 1))
523
+ throw new Error("TC_TRACE_ACTION: Invalid recorded event.");
524
+ validateEventSnapshot({
525
+ format: "pelican.events",
526
+ version: 1,
527
+ nextSequence: e.sequence + 1,
528
+ queue: [e],
529
+ }, action.atTick);
530
+ }
531
+ else if (action.kind !== "pause" && action.kind !== "resume")
532
+ throw new Error("TC_TRACE_ACTION: Unknown recorded action.");
533
+ }
534
+ return trace;
535
+ };
536
+ const preflightReplay = (value) => {
537
+ safe();
538
+ if (recording || replaying)
539
+ throw new Error("TC_TRACE_ACTIVE: Cannot replay during recording/replay.");
540
+ return validateTrace(value);
541
+ };
542
+ const settleTick = async (afterTick) => {
543
+ try {
544
+ await afterTick();
545
+ ensure();
546
+ }
547
+ catch (error) {
548
+ if (!disposed) {
549
+ fault = {
550
+ code: "TC_SCHEDULER_SETTLE",
551
+ tick: completedTick,
552
+ stage: "settle",
553
+ partialState: true,
554
+ eventFaults: [],
555
+ message: error instanceof Error ? error.message : String(error),
556
+ };
557
+ paused = true;
558
+ }
559
+ throw error;
560
+ }
561
+ };
562
+ const asyncBoundary = async (operation) => {
563
+ safe();
564
+ asyncActive = true;
565
+ try {
566
+ return await operation();
567
+ }
568
+ finally {
569
+ asyncActive = false;
570
+ }
571
+ };
572
+ // Only synchronous iterator execution may drive the scheduler. The permit is
573
+ // released before awaiting user/plugin code, including its synchronous prefix.
574
+ const drive = (iterator) => {
575
+ driving = true;
576
+ try {
577
+ return iterator.next();
578
+ }
579
+ finally {
580
+ driving = false;
581
+ }
582
+ };
583
+ function* replayActions(trace) {
584
+ let actionsExecuted = 0;
585
+ restore(trace.initial);
586
+ replaying = true;
587
+ try {
588
+ for (const action of trace.actions) {
589
+ if (action.atTick !== completedTick)
590
+ throw new Error("TC_REPLAY_DIVERGENCE: Recorded operation tick differs.");
591
+ switch (action.kind) {
592
+ case "step":
593
+ yield* stepTicks(action.count);
594
+ break;
595
+ case "advance":
596
+ yield* advanceTicks(action.seconds);
597
+ break;
598
+ case "pause":
599
+ pause();
600
+ break;
601
+ case "resume":
602
+ resume();
603
+ break;
604
+ case "random":
605
+ if (nextRandom() !== action.value)
606
+ throw new Error("TC_REPLAY_DIVERGENCE: Random stream differs.");
607
+ break;
608
+ case "emit": {
609
+ const actual = events.emit(action.event.type, action.event.payload, {
610
+ tick: action.event.tick,
611
+ source: action.event.source,
612
+ });
613
+ if (actual.sequence !== action.event.sequence)
614
+ throw new Error("TC_REPLAY_DIVERGENCE: Event sequence differs.");
615
+ break;
616
+ }
617
+ }
618
+ actionsExecuted++;
619
+ if (fault)
620
+ break;
621
+ }
622
+ if (!fault && canonical(snapshot()) !== canonical(trace.final))
623
+ throw new Error("TC_REPLAY_DIVERGENCE: Final state differs; use identical system code and controlled external inputs.");
624
+ }
625
+ catch (error) {
626
+ const failure = fault ?? {
627
+ code: "TC_REPLAY_DIVERGENCE",
628
+ tick: completedTick,
629
+ stage: "replay",
630
+ partialState: true,
631
+ eventFaults: [],
632
+ message: error instanceof Error ? error.message : String(error),
633
+ };
634
+ if (!disposed) {
635
+ fault = failure;
636
+ paused = true;
637
+ }
638
+ return {
639
+ status: "failed",
640
+ actionsExecuted,
641
+ fault: cloneSerializable(failure),
642
+ };
643
+ }
644
+ finally {
645
+ replaying = false;
646
+ }
647
+ return {
648
+ status: fault ? "failed" : "passed",
649
+ actionsExecuted,
650
+ fault: cloneSerializable(fault),
651
+ };
652
+ }
653
+ return {
654
+ get tick() {
655
+ return currentTick();
656
+ },
657
+ get time() {
658
+ return currentTick() * fixedDt;
659
+ },
660
+ events,
661
+ emit: bus.emit,
662
+ random: nextRandom,
663
+ step,
664
+ advance,
665
+ pause,
666
+ resume,
667
+ snapshot,
668
+ restore,
669
+ preflightReplay,
670
+ /** Await an engine boundary after an already executed synchronous tick. */
671
+ settle(afterTick) {
672
+ return asyncBoundary(() => settleTick(afterTick));
673
+ },
674
+ dispose() {
675
+ if (disposed)
676
+ return;
677
+ ensure();
678
+ if (running)
679
+ throw new Error("TC_SCHEDULER_SAFE_POINT: Operation requires a tick boundary.");
680
+ bus.dispose();
681
+ plan = schedule([]);
682
+ delivered = [];
683
+ recording = null;
684
+ paused = true;
685
+ disposed = true;
686
+ },
687
+ setSystems(next) {
688
+ safe();
689
+ if (recording || replaying)
690
+ throw new Error("TC_TRACE_SYSTEMS: Cannot replace systems during recording/replay.");
691
+ const candidate = schedule(next);
692
+ plan = candidate;
693
+ },
694
+ inspect() {
695
+ return {
696
+ tick: completedTick,
697
+ activeTick: running ? currentTick() : null,
698
+ time: completedTick * fixedDt,
699
+ fixedDt,
700
+ maxSubsteps,
701
+ paused,
702
+ running,
703
+ asyncActive,
704
+ disposed,
705
+ backlogTicks: backlog(),
706
+ backlogSeconds: accumulator,
707
+ recording: recording !== null,
708
+ replaying,
709
+ fault: cloneSerializable(fault),
710
+ systems: cloneSerializable(plan.descriptors),
711
+ diagnostics: cloneSerializable(plan.diagnostics),
712
+ events: bus.inspect(),
713
+ random: random.snapshot(),
714
+ };
715
+ },
716
+ startRecording() {
717
+ safe();
718
+ if (recording || replaying)
719
+ throw new Error("TC_TRACE_ACTIVE: Recording/replay already active.");
720
+ recording = { initial: snapshot(), actions: [] };
721
+ },
722
+ stopRecording() {
723
+ safe();
724
+ if (!recording)
725
+ throw new Error("TC_TRACE_INACTIVE: No active recording.");
726
+ const final = snapshot();
727
+ const trace = {
728
+ format: "pelican.trace",
729
+ version: 1,
730
+ initial: recording.initial,
731
+ actions: recording.actions,
732
+ final,
733
+ };
734
+ recording = null;
735
+ return cloneSerializable(trace);
736
+ },
737
+ cancelRecording() {
738
+ safe();
739
+ recording = null;
740
+ },
741
+ replay(value) {
742
+ return drain(replayActions(preflightReplay(value)));
743
+ },
744
+ async replayAsync(value, afterTick) {
745
+ const trace = preflightReplay(value);
746
+ if (typeof afterTick !== "function")
747
+ throw new Error("TC_SCHEDULER_ARGUMENT: afterTick must be a function.");
748
+ return asyncBoundary(async () => {
749
+ const iterator = replayActions(trace);
750
+ let next = drive(iterator);
751
+ while (!next.done) {
752
+ try {
753
+ await settleTick(afterTick);
754
+ }
755
+ catch (error) {
756
+ // Throw through the iterator so replay owns fault/report cleanup.
757
+ return iterator.throw(error).value;
758
+ }
759
+ next = drive(iterator);
760
+ }
761
+ return next.value;
762
+ });
763
+ },
764
+ };
765
+ }