@origingame/engine 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/bin/og.mjs +1 -1
  2. package/dist/web/origin-engine.d.ts +219 -5
  3. package/dist/web/origin-engine.js +1344 -125
  4. package/dist/web/origin-engine_bg.wasm +0 -0
  5. package/dist/web/origin-engine_bg.wasm.d.ts +68 -3
  6. package/dist/web-publish/origin-engine-lanes.js +1007 -22
  7. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  8. package/dist/web-publish/origin-engine.d.ts +162 -5
  9. package/dist/web-publish/origin-engine.js +1004 -19
  10. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  11. package/dist/web-publish/origin-engine_bg.wasm.d.ts +52 -3
  12. package/dist/web-publish-2d/origin-engine-lanes.js +1007 -22
  13. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  14. package/dist/web-publish-2d/origin-engine.d.ts +162 -5
  15. package/dist/web-publish-2d/origin-engine.js +1004 -19
  16. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  17. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +52 -3
  18. package/knowledge/looks/catalogue.json +40 -40
  19. package/knowledge/package.json +1 -1
  20. package/knowledge/scaffold/distribution.json +14 -0
  21. package/knowledge/scaffold/origin-engine.json +2 -2
  22. package/knowledge/scaffold/template/README.md +2 -1
  23. package/knowledge/scaffold/template/assets/audio/sting.ogt +10 -0
  24. package/knowledge/scaffold/template/assets/timelines/intro.ogt +101 -0
  25. package/knowledge/scaffold/template/assets/timelines/narration.srt +11 -0
  26. package/knowledge/scaffold/template/host.d.ts +46 -0
  27. package/knowledge/scaffold/template/package.json +1 -1
  28. package/knowledge/scaffold/template/project.ogt +1 -1
  29. package/knowledge/scaffold/template/src/main.ts +1 -1
  30. package/knowledge/scaffold/template/tests/frames.ts +134 -0
  31. package/knowledge/scaffold/template/tests/intro.og.ts +65 -0
  32. package/knowledge/scaffold/template/tests/smoke.og.ts +30 -36
  33. package/knowledge/scaffold/template/tsconfig.json +1 -1
  34. package/knowledge/scripts/mount.ts +2 -1
  35. package/knowledge/scripts/validate.ts +19 -10
  36. package/knowledge/skills/og-build/SKILL.md +8 -5
  37. package/knowledge/skills/og-core/SKILL.md +11 -0
  38. package/knowledge/skills/og-game/SKILL.md +877 -8
  39. package/knowledge/skills/og-platform-sdk/SKILL.md +1 -0
  40. package/knowledge/skills/og-taste/SKILL.md +82 -73
  41. package/knowledge/skills/og-ui/SKILL.md +65 -50
  42. package/knowledge/wiki/ai.md +20 -20
  43. package/knowledge/wiki/anim.md +384 -41
  44. package/knowledge/wiki/atmosphere.md +36 -13
  45. package/knowledge/wiki/audio.md +18 -18
  46. package/knowledge/wiki/camera.md +19 -13
  47. package/knowledge/wiki/commands.md +19 -13
  48. package/knowledge/wiki/debug.md +414 -0
  49. package/knowledge/wiki/game.md +8 -8
  50. package/knowledge/wiki/i18n.md +7 -7
  51. package/knowledge/wiki/index.md +262 -7
  52. package/knowledge/wiki/input.md +47 -47
  53. package/knowledge/wiki/load.md +236 -68
  54. package/knowledge/wiki/math.md +2607 -0
  55. package/knowledge/wiki/nav.md +34 -34
  56. package/knowledge/wiki/net.md +340 -0
  57. package/knowledge/wiki/path.md +402 -0
  58. package/knowledge/wiki/physics.md +95 -95
  59. package/knowledge/wiki/platform.md +463 -29
  60. package/knowledge/wiki/probe.md +681 -67
  61. package/knowledge/wiki/reflect.md +15 -15
  62. package/knowledge/wiki/render.md +3893 -1896
  63. package/knowledge/wiki/rng.md +14 -14
  64. package/knowledge/wiki/stream.md +15 -15
  65. package/knowledge/wiki/system.md +8 -8
  66. package/knowledge/wiki/systems.md +2 -2
  67. package/knowledge/wiki/terrain.md +19 -19
  68. package/knowledge/wiki/tilemap.md +13 -13
  69. package/knowledge/wiki/time.md +4 -4
  70. package/knowledge/wiki/timeline.md +396 -0
  71. package/knowledge/wiki/transform.md +8 -8
  72. package/knowledge/wiki/tween.md +17 -17
  73. package/knowledge/wiki/ui.md +559 -629
  74. package/knowledge/wiki/video.md +157 -0
  75. package/knowledge/wiki/world.md +36 -36
  76. package/package.json +4 -3
  77. package/schema/api.schema.json +21707 -14725
  78. package/shell/page.ts +26 -2
  79. package/shell/platform.ts +46 -0
  80. package/shell/player.ts +7 -0
  81. package/src/depth.ts +71 -0
  82. package/src/e6.ts +32 -1
  83. package/src/env.d.ts +5 -1
  84. package/src/generated/abi.ts +189 -3
  85. package/src/index.ts +25 -4
  86. package/src/light.ts +18 -0
  87. package/src/math.ts +622 -0
  88. package/src/node/index.ts +17 -0
  89. package/src/node/playtest-run.ts +114 -18
  90. package/src/node/polish.ts +13 -3
  91. package/src/node/render.ts +291 -0
  92. package/src/node/run.ts +86 -5
  93. package/src/p2.ts +19 -1
  94. package/src/playtest.ts +57 -4
  95. package/src/runtime.ts +13 -3
  96. package/src/targets.ts +28 -0
  97. package/src/trace.ts +169 -0
  98. package/src/ui-kit/huds.ts +34 -34
  99. package/src/ui-kit/menus.ts +12 -12
  100. package/src/ui-kit/settings.ts +2 -2
  101. package/src/ui-kit/store.ts +18 -18
  102. package/src/ui-kit/story.ts +6 -4
  103. package/src/ui-kit/util.ts +8 -8
  104. package/src/web/shell.ts +58 -0
  105. package/src/web/worker.ts +103 -2
  106. package/types/bindings.json +9101 -6647
  107. package/types/index.d.ts +15503 -11564
package/src/web/shell.ts CHANGED
@@ -30,6 +30,7 @@ import { laneChoice } from "./lanes.ts";
30
30
  import { createRpcRings, readFrames, waitForFrames, writeFrame, type RpcRings } from "./rpc-ring.ts";
31
31
  import { localPlatform, storagePersistence, type PlatformCall, type PlatformHost } from "../platform-local.ts";
32
32
  import type { BootPhases } from "./worker.ts";
33
+ import type { TraceDocument } from "../trace.ts";
33
34
 
34
35
  export interface ShellOptions {
35
36
  /** The play surface. Its pixel size is set from its CSS size and the device pixel ratio. */
@@ -78,6 +79,8 @@ export interface ShellOptions {
78
79
  readonly locale?: string;
79
80
  /** What answers og.platform's calls; default: the browser's own storage (`setPlatform` changes it, a Player bridge for instance). */
80
81
  readonly platform?: PlatformHost;
82
+ /** Record the run's trace from the boot on, the boot scene's loads included (`trace()` reads it; `trace("start")` begins one later). */
83
+ readonly trace?: boolean;
81
84
  }
82
85
 
83
86
  /** The pixel size of a canvas laid out at `cssWidth`×`cssHeight` on this display, with the DPR cap. */
@@ -93,6 +96,17 @@ export const PAGE_PREFERENCES = [
93
96
  ["reducedTransparency", "(prefers-reduced-transparency: reduce)"],
94
97
  ] as const;
95
98
 
99
+ /**
100
+ * What carries og.platform's rooms on the web (docs/design.md §2.8): the engine's room ops
101
+ * (`{ op: "create" | "join" | "leave" | "send", ... }`, og-net's protocol) in, its room events
102
+ * (`joined`, `peer_joined`, `peer_left`, `message`, `left`, `error`) out, in order.
103
+ */
104
+ export interface RoomHost {
105
+ /** og.platform.roomInfo's `host`: `player` for the Player's rooms. */
106
+ readonly host: string;
107
+ handle(ops: readonly Record<string, unknown>[], deliver: (events: unknown[]) => void): void;
108
+ }
109
+
96
110
  /** The page's motion, contrast and transparency preferences and the platform's editing keys, as the viewport carries them to og-ui. */
97
111
  export function pagePreferences(): { reducedMotion: boolean; highContrast: boolean; reducedTransparency: boolean; macKeys: boolean } {
98
112
  const query = (q: string) => globalThis.matchMedia?.(q).matches ?? false;
@@ -203,6 +217,7 @@ export class Shell {
203
217
  /** When the deadline was last looked at: a late look means the page itself was blocked. */
204
218
  #watched = 0;
205
219
  #watchdog: ReturnType<typeof setInterval> | undefined;
220
+ #rooms: RoomHost | null = null;
206
221
  #platform: PlatformHost = localPlatform(storagePersistence(`og.platform:${globalThis.location?.pathname ?? "/"}`));
207
222
 
208
223
  private constructor(worker: Worker, ready: Ready, rpc: RpcRings | undefined) {
@@ -250,6 +265,14 @@ export class Shell {
250
265
  this.#answer(m.frame);
251
266
  return;
252
267
  }
268
+ if (m.type === "room") {
269
+ const ops = (m as unknown as { ops: Record<string, unknown>[] }).ops ?? [];
270
+ const deliver = (events: unknown[]) => worker.postMessage({ type: "room-events", events });
271
+ if (this.#rooms) this.#rooms.handle(ops, deliver);
272
+ // No room host: what asked for a room hears so (the engine refuses first while its host is none).
273
+ else deliver(ops.filter((o) => o["op"] === "create" || o["op"] === "join").map((o) => ({ type: "error", op: o["op"], code: "unavailable", message: "this page carries no rooms" })));
274
+ return;
275
+ }
253
276
  if (m.type === "platform") {
254
277
  const calls = (m as unknown as { calls: PlatformCall[] }).calls ?? [];
255
278
  this.#platform.handle(calls, (request, answer) => worker.postMessage({ type: "platform-answer", request, answer }));
@@ -347,6 +370,7 @@ export class Shell {
347
370
  locale: options.locale ?? globalThis.navigator?.language,
348
371
  ...(options.expect ? { expect: options.expect } : {}),
349
372
  ...(options.prime ? { prime: true } : {}),
373
+ ...(options.trace ? { trace: true } : {}),
350
374
  viewport: { width: canvas.clientWidth || canvas.width / dpr, height: canvas.clientHeight || canvas.height / dpr, dpr, coarsePointer: globalThis.matchMedia?.("(pointer: coarse)").matches ?? false, ...pagePreferences() },
351
375
  },
352
376
  [offscreen],
@@ -416,6 +440,27 @@ export class Shell {
416
440
  }
417
441
 
418
442
  /** Call the engine in the Worker: `advance`, `command`, `hash`, `frame`, or `probe` with a member name. */
443
+ /**
444
+ * The run's trace in the Chrome trace format, as `og run --trace` writes it natively (src/trace.ts):
445
+ * buffered in the engine Worker, taken here. `take` (the default) answers the document so far (null
446
+ * when no trace is recorded), `start` begins one (`ShellOptions.trace` starts one at the boot),
447
+ * `stop` ends it.
448
+ */
449
+ trace(action: "take" | "start" | "stop" = "take"): Promise<TraceDocument | null | boolean> {
450
+ return this.call("trace", action) as Promise<TraceDocument | null | boolean>;
451
+ }
452
+
453
+ /** Saves the trace so far as a JSON file the browser downloads (`name`, default `trace.json`): open it in Perfetto. False when none is recorded. */
454
+ async saveTrace(name = "trace.json"): Promise<boolean> {
455
+ const doc = await this.trace("take");
456
+ if (!doc || typeof doc !== "object") return false;
457
+ const url = URL.createObjectURL(new Blob([JSON.stringify(doc)], { type: "application/json" }));
458
+ const a = Object.assign(document.createElement("a"), { href: url, download: name });
459
+ a.click();
460
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
461
+ return true;
462
+ }
463
+
419
464
  call(method: string, ...args: unknown[]): Promise<unknown> {
420
465
  if (this.#stopped) return Promise.reject(this.#stopped);
421
466
  const id = this.#next++;
@@ -425,6 +470,19 @@ export class Shell {
425
470
  });
426
471
  }
427
472
 
473
+ /**
474
+ * What carries og.platform's rooms from now on (og.platform.roomInfo's `host`); null for
475
+ * none, where roomCreate and roomJoin are refused. A host already carrying a room keeps it
476
+ * until its `left`: the engine is told the new host only for its next room. `link` is the room
477
+ * a Play link named (`?room=<id>`, the play page passes it): the engine joins it once, itself,
478
+ * as soon as a host carries rooms (og.RoomJoined answers, as for roomJoin; with none it says
479
+ * this page carries no rooms), and og.platform.roomInfo's `link` names it.
480
+ */
481
+ setRooms(host: RoomHost | null, link?: string): void {
482
+ this.#rooms = host;
483
+ this.worker.postMessage({ type: "room-host", host: host ? host.host : "none", ...(link ? { join: link } : {}) });
484
+ }
485
+
428
486
  /** What answers og.platform's calls from now on (og.platform.info reports its host and capabilities). */
429
487
  setPlatform(host: PlatformHost): void {
430
488
  this.#platform = host;
package/src/web/worker.ts CHANGED
@@ -44,7 +44,7 @@
44
44
  // Worker's animation frames while the clock played, so isolated pages never
45
45
  // answered.
46
46
 
47
- import { createEngine, systemClock, type Engine, type GameToken } from "../runtime.ts";
47
+ import { boundHost, createEngine, systemClock, type Engine, type GameToken } from "../runtime.ts";
48
48
  import type { PlatformAnswer, PlatformHost } from "../platform-local.ts";
49
49
  import { loadWebHost, type WebGlue } from "./host.ts";
50
50
  import { drainRing, stampAt, type InjectedInput } from "./input.ts";
@@ -53,6 +53,7 @@ import { readFrames, waitForFrames, writeFrame, type RpcRings } from "./rpc-ring
53
53
  import { loadBasisTranscoder, transcodeKtx2, TEXTURE_FORMATS, type BasisTranscoder, type TextureFormat } from "./basis.ts";
54
54
  import type { HostError } from "./shell.ts";
55
55
  import { PlatformCounter, settle, SETTLE_FIRST_MS, SETTLE_MS } from "./settle.ts";
56
+ import { Trace, unixMs } from "../trace.ts";
56
57
 
57
58
  interface Start {
58
59
  type: "start";
@@ -92,6 +93,8 @@ interface Start {
92
93
  * game's first screen (its UI is built by ticks), then the ticks that apply the platform's answers to it (settle.ts).
93
94
  */
94
95
  prime?: boolean;
96
+ /** Record the run's trace from the boot on (src/trace.ts; the shell's `trace()` reads it). */
97
+ trace?: boolean;
95
98
  }
96
99
 
97
100
  type Viewport = { width: number; height: number; dpr: number; safeArea?: Record<string, number>; covered?: Record<string, number>; coarsePointer?: boolean; reducedMotion?: boolean; highContrast?: boolean; reducedTransparency?: boolean; macKeys?: boolean };
@@ -113,9 +116,26 @@ type Message =
113
116
  | { type: "resize"; width: number; height: number; viewport: Viewport }
114
117
  | { type: "platform-answer"; request: number; answer: PlatformAnswer }
115
118
  | { type: "platform-host"; host: string; capabilities: string[] }
119
+ | { type: "room-events"; events: unknown[] }
120
+ | { type: "room-host"; host: string; join?: string }
116
121
  | AudioMessage
117
122
  | AudioLatencyMessage;
118
123
 
124
+ /**
125
+ * Rooms (docs/design.md §2.8): the engine's room ops (og_room_take) go to the
126
+ * page as `room` messages, which the shell's room host carries (the Player's
127
+ * room.*, docs/platform-asks.md #16); the page's `room-events` come back
128
+ * through og_room_deliver, in order, and `room-host` names what carries them.
129
+ * Ops are taken after every frame and after events are delivered, so
130
+ * lockstep's inputs leave on the frame that made them.
131
+ */
132
+ function pumpRoom(engine: Engine): void {
133
+ const host = boundHost();
134
+ if (!host) return;
135
+ const ops = JSON.parse(host.abi.og_room_take(engine.id)) as unknown[];
136
+ if (ops.length > 0) self.postMessage({ type: "room", ops });
137
+ }
138
+
119
139
  /** og.platform's host in the Worker: the calls go to the page (counted), whose answers come back as `platform-answer`. */
120
140
  function toPage(host: string, capabilities: readonly string[], counter: PlatformCounter): PlatformHost {
121
141
  return {
@@ -159,12 +179,21 @@ export interface WebExtras {
159
179
  og_web_memory?(engine: number): string;
160
180
  /** Make one pipeline no frame used yet (og-render's Renderer::warm); false when none is left. */
161
181
  og_web_warm?(engine: number): boolean;
182
+ /** `og render`'s every-frame capture (development builds): the display frame drawn last as RGBA8 rows at the canvas's size; empty until its readback lands. */
183
+ og_web_render_frame?(engine: number): Uint8Array;
162
184
  /** The first Rust panic recorded (JSON: kind, message, location, tick, lane), or `null`. */
163
185
  og_web_panic?(): string;
164
186
  /** Test hook: the next item at a lanes site panics (in a lane when the call splits). */
165
187
  og_web_lanes_fault?(site: string): boolean;
188
+ /** The trace (crates/og-runtime/src/trace.rs): on or off for an engine (0xffffffff: the loads before one exists). */
189
+ og_web_trace?(engine: number, on: boolean): void;
190
+ /** The trace's spans since the last take: `{ now, events }`, Chrome trace events on the engine's clock. */
191
+ og_web_trace_take?(engine: number, last: boolean): string;
166
192
  }
167
193
 
194
+ /** The trace's key for loads made before the engine exists (og-runtime's `BEFORE_ENGINE`). */
195
+ const TRACE_BEFORE_ENGINE = 0xffffffff;
196
+
168
197
  /** Where the runtime asks for a bundle's files: `<root>/cooked/<file>` (crates/og-runtime/src/assets.rs). */
169
198
  export function cookedDir(root: string): string {
170
199
  return `${root.replace(/\/+$/, "")}/cooked`;
@@ -514,6 +543,9 @@ export function startWorker(game: GameToken): void {
514
543
  phaseAt = now;
515
544
  };
516
545
  const firstFrameMessage = () => ({ type: "frame", first: true, boot: { ...boot, at: Math.round(performance.now()) } });
546
+ /** What the trace's document records beside its events: the renderer's tier and the lanes. */
547
+ let lastTier = "none";
548
+ let lanesState: unknown = null;
517
549
  let glueRef: (WebGlue & WebExtras & WebAudioGlue) | null = null;
518
550
  let audio: WorkerAudio | null = null;
519
551
  /** Captures waiting for their readback (requested on tick N, answered on N + 1). */
@@ -592,7 +624,21 @@ export function startWorker(game: GameToken): void {
592
624
  };
593
625
 
594
626
  /** What a frame's answer tells the shell: the UI's focused text control (text-input.ts), a copy, and the accessibility tree when it changed (a11y.ts). */
627
+ /** The run's trace while one is recorded (src/trace.ts): taken after every frame, so the engine holds a frame's spans at most. */
628
+ let trace: Trace | null = null;
629
+ const takeTrace = (last = false) => {
630
+ if (!trace || !engine || !glueRef?.og_web_trace_take) return;
631
+ trace.append(glueRef.og_web_trace_take(engine.id, last), unixMs());
632
+ };
633
+ const traceOn = (on: boolean) => {
634
+ if (!engine || !glueRef?.og_web_trace) return false;
635
+ if (on && !trace) trace = new Trace({ producer: "og web trace", process: "og web" });
636
+ if (!on) trace = null;
637
+ glueRef.og_web_trace(engine.id, on);
638
+ return true;
639
+ };
595
640
  const relay = (answered: ReturnType<Engine["frame"]>) => {
641
+ takeTrace();
596
642
  if (!answered.ok) return;
597
643
  const target = JSON.stringify(answered.text ?? null);
598
644
  if (target !== textTarget) {
@@ -676,6 +722,7 @@ export function startWorker(game: GameToken): void {
676
722
  }
677
723
  behind = answered.behind ?? 0;
678
724
  relay(answered);
725
+ pumpRoom(engine);
679
726
  if (typeof answered.tick === "number") lastTick = answered.tick;
680
727
  if (!firstFrame && answered.rendered !== false) {
681
728
  firstFrame = true;
@@ -777,11 +824,25 @@ export function startWorker(game: GameToken): void {
777
824
  };
778
825
 
779
826
  const calls: Record<string, (e: Engine, args: unknown[]) => unknown> = {
780
- advance: (e, [n]) => e.advance(Number(n)),
827
+ // A harness's ticks carry the room as the page's own frames do: lockstep's inputs leave with them.
828
+ advance: (e, [n]) => {
829
+ const answered = e.advance(Number(n));
830
+ pumpRoom(e);
831
+ return answered;
832
+ },
781
833
  command: (e, [action, args]) => e.command(String(action), (args as string[]) ?? []),
782
834
  hash: (e) => e.hash(),
783
835
  /** What the engine holds (ProbeMemory: the wasm linear memory, the counted heap, the world, streaming, audio), in every build. */
784
836
  memory: (e) => JSON.parse(glueRef?.og_web_memory?.(e.id) ?? "null"),
837
+ /**
838
+ * The display frame drawn last as RGBA8 rows (the probe's Capture::frame,
839
+ * `og render`'s every-frame capture), or null until its readback lands: the
840
+ * first call turns the keeping of frames on; ask again before the next frame.
841
+ */
842
+ renderFrame: (e) => {
843
+ const rgba = glueRef?.og_web_render_frame?.(e.id);
844
+ return rgba && rgba.length > 0 ? rgba : null;
845
+ },
785
846
  /** What the lanes did so far (crates/og-runtime/src/lanes.rs `report`). */
786
847
  lanes: () => JSON.parse(glueRef?.og_web_lanes?.() ?? "null"),
787
848
  /** Test hook: the next item at lanes site `site` panics (og_web_lanes_fault); false when the module has no such hook or site. */
@@ -789,8 +850,14 @@ export function startWorker(game: GameToken): void {
789
850
  frame: (e, [ms]) => {
790
851
  const answered = e.frame(Number(ms));
791
852
  relay(answered);
853
+ pumpRoom(e);
792
854
  return answered;
793
855
  },
856
+ /** og.net on this page: og.net.status (lockstep running, its start tick, the seats, the first divergent tick) and the shared hash, as `og run --json` reports them natively (`lockstep`, `netHash`). */
857
+ net: (e) => {
858
+ const abi = boundHost()!.abi;
859
+ return { status: JSON.parse(abi.og_net_status(e.id)), hash: JSON.parse(abi.og_net_hash(e.id)), room: JSON.parse(abi.og_platform_room_info(e.id)) };
860
+ },
794
861
  // The frames played since the last reset (at most FRAME_RECORDS, oldest first); `true` resets.
795
862
  timings: (_, [reset]) => {
796
863
  const n = Math.min(recorded, FRAME_RECORDS);
@@ -799,6 +866,22 @@ export function startWorker(game: GameToken): void {
799
866
  if (reset) recorded = 0;
800
867
  return answer;
801
868
  },
869
+ /**
870
+ * The run's trace (src/trace.ts): `start` begins one (dropping any), `stop` ends it, `take` (the
871
+ * default) answers the document so far, a Chrome trace as `og run --trace` writes it; null when
872
+ * none is recorded or the module cannot trace.
873
+ */
874
+ trace: (_, [action]) => {
875
+ const a = String(action ?? "take");
876
+ if (a === "start") {
877
+ traceOn(false);
878
+ return traceOn(true);
879
+ }
880
+ if (a === "stop") return traceOn(false);
881
+ if (!trace) return null;
882
+ takeTrace(true);
883
+ return trace.document({ tier: lastTier, lanes: lanesState });
884
+ },
802
885
  probe: (e, [member, ...rest]) => {
803
886
  const f = (e.probe as unknown as Record<string, unknown>)[String(member)];
804
887
  if (typeof f !== "function") throw new TypeError(`probe has no member '${String(member)}'`);
@@ -870,6 +953,8 @@ export function startWorker(game: GameToken): void {
870
953
  installFetcher(glue, fetched.manifest ?? "", data.basis, () => (rendering && engine ? engine.id : null));
871
954
  }
872
955
  phase("bundle");
956
+ // A trace from the boot on records the boot scene's loads too.
957
+ if (data.trace) glue.og_web_trace?.(TRACE_BEFORE_ENGINE, true);
873
958
  const created = createEngine(game, {
874
959
  ...(data.seed === undefined ? {} : { seed: data.seed }),
875
960
  ...(data.root === undefined ? {} : { root: data.root }),
@@ -881,6 +966,10 @@ export function startWorker(game: GameToken): void {
881
966
  });
882
967
  if (!created.ok) throw new Error(created.reason);
883
968
  engine = created.value;
969
+ if (data.trace) {
970
+ traceOn(true);
971
+ glue.og_web_trace?.(TRACE_BEFORE_ENGINE, false);
972
+ }
884
973
  // og.platform: calls go to the page, which answers them (the Player, or the browser's storage).
885
974
  // The time is the browser's until the Player's answers carry its own (docs/platform-asks.md).
886
975
  engine.setClock(systemClock("browser"));
@@ -896,6 +985,8 @@ export function startWorker(game: GameToken): void {
896
985
  rendering = true;
897
986
  }
898
987
  phase("attach");
988
+ lastTier = tier;
989
+ lanesState = lanes;
899
990
  // The textures go to the renderer's encodings, so after it is attached (the game starts later).
900
991
  const loaded = fetched ? await transcodeBundle(glue, rendering ? engine.id : null, fetched, data.basis, progress, data.expect ? received : undefined) : null;
901
992
  phase("transcode");
@@ -983,6 +1074,16 @@ export function startWorker(game: GameToken): void {
983
1074
  platformCounter.answered();
984
1075
  }
985
1076
  if (data.type === "platform-host") engine.attachPlatform(toPage(data.host, data.capabilities, platformCounter));
1077
+ if (data.type === "room-host") {
1078
+ boundHost()?.abi.og_room_set_host(engine.id, JSON.stringify({ host: data.host, ...(data.join ? { join: data.join } : {}) }));
1079
+ // A Play link's join leaves now, not with the next frame (the page may be behind its gate).
1080
+ pumpRoom(engine);
1081
+ }
1082
+ if (data.type === "room-events") {
1083
+ const abi = boundHost()?.abi;
1084
+ for (const event of data.events) abi?.og_room_deliver(engine.id, JSON.stringify(event));
1085
+ pumpRoom(engine);
1086
+ }
986
1087
  if (data.type === "play") {
987
1088
  playing = data.on;
988
1089
  last = 0;