@origingame/engine 0.2.0 → 0.4.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 (118) hide show
  1. package/bin/og.mjs +1 -1
  2. package/dist/web/origin-engine.d.ts +255 -5
  3. package/dist/web/origin-engine.js +1584 -125
  4. package/dist/web/origin-engine_bg.wasm +0 -0
  5. package/dist/web/origin-engine_bg.wasm.d.ts +80 -3
  6. package/dist/web-publish/origin-engine-lanes.js +1288 -82
  7. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  8. package/dist/web-publish/origin-engine.d.ts +199 -6
  9. package/dist/web-publish/origin-engine.js +1285 -79
  10. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  11. package/dist/web-publish/origin-engine_bg.wasm.d.ts +65 -4
  12. package/dist/web-publish-2d/origin-engine-lanes.js +1285 -79
  13. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  14. package/dist/web-publish-2d/origin-engine.d.ts +198 -5
  15. package/dist/web-publish-2d/origin-engine.js +1282 -76
  16. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  17. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +64 -3
  18. package/knowledge/README.md +2 -1
  19. package/knowledge/guides/three-to-og.md +393 -0
  20. package/knowledge/knowledge.json +2 -1
  21. package/knowledge/looks/catalogue.json +40 -40
  22. package/knowledge/package.json +3 -2
  23. package/knowledge/scaffold/distribution.json +14 -0
  24. package/knowledge/scaffold/origin-engine.json +2 -2
  25. package/knowledge/scaffold/template/README.md +2 -1
  26. package/knowledge/scaffold/template/assets/audio/sting.ogt +10 -0
  27. package/knowledge/scaffold/template/assets/timelines/intro.ogt +101 -0
  28. package/knowledge/scaffold/template/assets/timelines/narration.srt +11 -0
  29. package/knowledge/scaffold/template/host.d.ts +46 -0
  30. package/knowledge/scaffold/template/package.json +1 -1
  31. package/knowledge/scaffold/template/project.ogt +1 -1
  32. package/knowledge/scaffold/template/src/main.ts +1 -1
  33. package/knowledge/scaffold/template/tests/frames.ts +134 -0
  34. package/knowledge/scaffold/template/tests/intro.og.ts +65 -0
  35. package/knowledge/scaffold/template/tests/smoke.og.ts +30 -36
  36. package/knowledge/scaffold/template/tsconfig.json +1 -1
  37. package/knowledge/scripts/mount.ts +2 -1
  38. package/knowledge/scripts/validate.ts +159 -31
  39. package/knowledge/skills/og-audio/SKILL.md +2 -2
  40. package/knowledge/skills/og-build/SKILL.md +29 -7
  41. package/knowledge/skills/og-core/SKILL.md +31 -5
  42. package/knowledge/skills/og-feel/SKILL.md +2 -2
  43. package/knowledge/skills/og-game/SKILL.md +1331 -16
  44. package/knowledge/skills/og-i18n/SKILL.md +7 -7
  45. package/knowledge/skills/og-model/SKILL.md +374 -0
  46. package/knowledge/skills/og-platform-sdk/SKILL.md +2 -1
  47. package/knowledge/skills/og-taste/SKILL.md +82 -73
  48. package/knowledge/skills/og-ui/SKILL.md +80 -58
  49. package/knowledge/wiki/ai.md +20 -20
  50. package/knowledge/wiki/anim.md +392 -41
  51. package/knowledge/wiki/atmosphere.md +36 -13
  52. package/knowledge/wiki/audio.md +18 -18
  53. package/knowledge/wiki/camera.md +19 -13
  54. package/knowledge/wiki/commands.md +19 -13
  55. package/knowledge/wiki/debug.md +414 -0
  56. package/knowledge/wiki/game.md +8 -8
  57. package/knowledge/wiki/i18n.md +7 -7
  58. package/knowledge/wiki/index.md +317 -8
  59. package/knowledge/wiki/input.md +47 -47
  60. package/knowledge/wiki/load.md +238 -70
  61. package/knowledge/wiki/math.md +2607 -0
  62. package/knowledge/wiki/nav.md +34 -34
  63. package/knowledge/wiki/net.md +340 -0
  64. package/knowledge/wiki/path.md +402 -0
  65. package/knowledge/wiki/physics.md +95 -95
  66. package/knowledge/wiki/platform.md +463 -29
  67. package/knowledge/wiki/probe.md +681 -67
  68. package/knowledge/wiki/reflect.md +29 -17
  69. package/knowledge/wiki/render.md +4993 -1998
  70. package/knowledge/wiki/rng.md +14 -14
  71. package/knowledge/wiki/shapes.md +727 -0
  72. package/knowledge/wiki/stream.md +15 -15
  73. package/knowledge/wiki/system.md +8 -8
  74. package/knowledge/wiki/systems.md +2 -2
  75. package/knowledge/wiki/terrain.md +24 -20
  76. package/knowledge/wiki/tilemap.md +13 -13
  77. package/knowledge/wiki/time.md +4 -4
  78. package/knowledge/wiki/timeline.md +396 -0
  79. package/knowledge/wiki/transform.md +8 -8
  80. package/knowledge/wiki/tween.md +17 -17
  81. package/knowledge/wiki/ui.md +560 -654
  82. package/knowledge/wiki/video.md +157 -0
  83. package/knowledge/wiki/world.md +36 -36
  84. package/package.json +4 -3
  85. package/schema/api.schema.json +28151 -19640
  86. package/shell/page.ts +26 -2
  87. package/shell/platform.ts +51 -0
  88. package/shell/player.ts +15 -2
  89. package/shell/transport.ts +6 -0
  90. package/src/depth.ts +71 -0
  91. package/src/e6.ts +32 -1
  92. package/src/env.d.ts +5 -1
  93. package/src/generated/abi.ts +226 -4
  94. package/src/geom.ts +260 -0
  95. package/src/index.ts +32 -4
  96. package/src/light.ts +18 -0
  97. package/src/math.ts +622 -0
  98. package/src/node/index.ts +17 -0
  99. package/src/node/playtest-run.ts +115 -18
  100. package/src/node/polish.ts +13 -3
  101. package/src/node/render.ts +291 -0
  102. package/src/node/run.ts +86 -5
  103. package/src/p2.ts +19 -1
  104. package/src/platform-local.ts +2 -0
  105. package/src/playtest.ts +65 -4
  106. package/src/runtime.ts +100 -9
  107. package/src/targets.ts +28 -0
  108. package/src/trace.ts +169 -0
  109. package/src/ui-kit/huds.ts +34 -34
  110. package/src/ui-kit/menus.ts +12 -12
  111. package/src/ui-kit/settings.ts +11 -2
  112. package/src/ui-kit/store.ts +18 -18
  113. package/src/ui-kit/story.ts +6 -4
  114. package/src/ui-kit/util.ts +8 -8
  115. package/src/web/shell.ts +59 -1
  116. package/src/web/worker.ts +105 -4
  117. package/types/bindings.json +9368 -6619
  118. package/types/index.d.ts +16377 -11520
package/shell/page.ts CHANGED
@@ -18,7 +18,9 @@
18
18
  // - the error card (§6): the reason, the last 200 log lines and the world
19
19
  // dump (state in a publish build), with Reload and Copy report;
20
20
  // - the probe: `globalThis.__runtime_probe__` (protocol 5, every member a
21
- // Promise, plus `lanes`, `memory()` (what the engine holds), `boot`
21
+ // Promise, plus `lanes`, `memory()` (what the engine holds), `trace()`
22
+ // and `saveTrace()` (the run's Chrome trace; `?trace` records from the
23
+ // boot), `boot`
22
24
  // (where the Worker's boot went) and
23
25
  // `error`: what stopped the engine, a panic with its tick and lane, an
24
26
  // error or a hang) and the platform's `og-probe`
@@ -34,7 +36,7 @@ import { pollGamepads } from "../src/web/gamepad.ts";
34
36
  import { mountWire } from "./wire.ts";
35
37
  import { connectPlayer } from "./player.ts";
36
38
  import { connectPage, pageGame, type PageConnection, type PageInfo } from "./transport.ts";
37
- import { deferredPlatform, HELLO_WAIT_MS, playerPlatform, storagePersistence } from "./platform.ts";
39
+ import { deferredPlatform, HELLO_WAIT_MS, playerPlatform, playerRooms, storagePersistence } from "./platform.ts";
38
40
  import { localPlatform, memoryPersistence, type PlatformHost } from "../src/platform-local.ts";
39
41
 
40
42
  /** The page's configuration (`<script type="application/json" id="og-config">`, written by og build). */
@@ -62,12 +64,14 @@ export interface ShellConfig {
62
64
  const PROBE_GLOBAL = "__runtime_probe__";
63
65
  const TIMINGS_GLOBAL = "__og_timings__";
64
66
  const REPLAY_SEED_PARAM = "replay-seed";
67
+ const TRACE_PARAM = "trace";
65
68
  const LOG_LINES = 200;
66
69
  const INSTALL_DISMISSED = "og-install-dismissed";
67
70
 
68
71
  const MEMBERS = [
69
72
  "hello", "tick", "state", "advance", "pause", "resume", "input", "accepted", "pick", "entity",
70
73
  "bounds", "patch", "run", "capture", "sweep", "reload", "snapshot", "restore", "dump", "commands",
74
+ "describe", "expect", "expectations",
71
75
  ] as const;
72
76
 
73
77
  /** Positional protocol-5 arguments to JSON-RPC params, as og-probe reads them. */
@@ -91,6 +95,8 @@ export function probeParams(member: string, args: unknown[]): Record<string, unk
91
95
  return { width: args[0] ?? 0, height: args[1] ?? 0 };
92
96
  case "commands":
93
97
  return { script: args[0] };
98
+ case "expect":
99
+ return { what: args[0] };
94
100
  case "sweep":
95
101
  case "dump":
96
102
  return (args[0] as Record<string, unknown>) ?? {};
@@ -178,6 +184,9 @@ export interface PlayHandle {
178
184
  export async function play(): Promise<PlayHandle> {
179
185
  const config = readConfig();
180
186
  const query = new URLSearchParams(location.search);
187
+ // A Play link to a room (`?room=<id>`, docs/platform-asks.md #16): the engine joins it at load
188
+ // once a host carries rooms (the Player's room.*), og.RoomJoined answering as for roomJoin.
189
+ const roomLink = query.get("room") ?? undefined;
181
190
  const t0 = performance.now();
182
191
  const log = new LogTail();
183
192
  const playerEvents: import("./player.ts").PlayerEvents = {};
@@ -446,6 +455,8 @@ export async function play(): Promise<PlayHandle> {
446
455
  // Behind the gate, the first screen: one tick builds the game's UI (a harness keeps tick 0).
447
456
  prime: !harness,
448
457
  ...(seed === undefined ? {} : { seed }),
458
+ // `?trace`: the run's trace from the boot on (the probe's `trace()` and `saveTrace()` read it).
459
+ ...(query.has(TRACE_PARAM) ? { trace: true } : {}),
449
460
  profile: config.profile,
450
461
  ...(config.project === undefined ? {} : { project: config.project }),
451
462
  dprCap: config.dprCap,
@@ -494,8 +505,14 @@ export async function play(): Promise<PlayHandle> {
494
505
  // `boot`: where the Worker's boot went (src/web/worker.ts BootPhases), null before the first frame.
495
506
  Object.defineProperty(probe, "boot", { get: () => shell.boot, enumerable: true });
496
507
  for (const m of MEMBERS) probe[m] = (...args: unknown[]) => shell.probe(m, probeParams(m, args));
508
+ // `saveScene`: the wire's save_scene; a page writes no file, so it answers the text.
509
+ probe["saveScene"] = () => shell.probe("save_scene", { write: false });
497
510
  // `memory`: what the engine holds (ProbeMemory), from the Worker in every build (a publish probe has no `memory`).
498
511
  probe["memory"] = () => shell.call("memory");
512
+ // `trace`: the run's Chrome trace, as `og run --trace` writes it ("take", "start", "stop");
513
+ // `saveTrace`: the same, downloaded as a file (`?trace` records from the boot).
514
+ probe["trace"] = (action?: "take" | "start" | "stop") => shell.trace(action);
515
+ probe["saveTrace"] = (name?: string) => shell.saveTrace(name);
499
516
  (globalThis as Record<string, unknown>)[PROBE_GLOBAL] = Object.freeze(probe);
500
517
  // The played frames' display intervals and engine work, ms (`og run --device` reads them; `true` resets).
501
518
  (globalThis as Record<string, unknown>)[TIMINGS_GLOBAL] = (reset?: boolean) => shell.call("timings", reset === true);
@@ -576,6 +593,10 @@ export async function play(): Promise<PlayHandle> {
576
593
  helloForPlatform();
577
594
  // What it answers may have changed with this hello.
578
595
  shell.setPlatform(playerPlatform(player, platformStore));
596
+ // Rooms ride the Player's postMessage link (pushed events): a framed Player's, never the HTTP page's.
597
+ const rooms = playerRooms(player);
598
+ shell.setRooms(rooms, roomLink);
599
+ playerEvents.room = (e) => rooms?.push(e);
579
600
  pausedByPlayer = m.paused;
580
601
  if (m.started && phase === "gate") start();
581
602
  };
@@ -595,6 +616,9 @@ export async function play(): Promise<PlayHandle> {
595
616
  pausedByPlayer = player.paused;
596
617
  // A framed page (or the Stage overlay's own document) waits briefly for a Player's hello before drawing its own gate.
597
618
  if (linked && !byPlayer) await new Promise((r) => setTimeout(r, 300));
619
+ // A Play link to a room on a page no Player carries rooms for: the game hears why it is in none
620
+ // (og.RoomJoined, ok false); a Player's hello later joins it.
621
+ if (roomLink && !byPlayer) shell.setRooms(null, roomLink);
598
622
 
599
623
  // ── ready: reveal, then the gate ─────────────────────────────────────
600
624
  setProgress(1);
package/shell/platform.ts CHANGED
@@ -24,16 +24,26 @@
24
24
  // telemetry an `event` named game.custom.<name>
25
25
  // point point.award { reason, amount } → { granted, balance }
26
26
  // ai.mark ai.mark { kind, label, model? } → { marked }
27
+ // rooms room.create { name?, capacity? }, room.join { room, name? },
28
+ // room.leave {}, room.send { to?, channel?, data? | bytes? }
29
+ // (docs/platform-asks.md #16): a result only acknowledges
30
+ // (a failed one becomes the engine's `error` event); every
31
+ // room event arrives as a pushed `room` message in the
32
+ // engine's own shape (joined, peer_joined, peer_left,
33
+ // message, left), in order. A Player without room.join
34
+ // carries no rooms: roomInfo's host is `none`.
27
35
 
28
36
  import { answerLocally, fail, localPlatform, type LocalDocument, type LocalPersistence, type PlatformAnswer, type PlatformCall, type PlatformHost } from "../src/platform-local.ts";
29
37
  export { storagePersistence } from "../src/platform-local.ts";
30
38
  import type { PlayerResult } from "./player.ts";
39
+ import type { RoomHost } from "../src/web/shell.ts";
31
40
 
32
41
  /** The part of the Player link this needs (connectPlayer's answer). */
33
42
  export interface PlayerLink {
34
43
  readonly framed: boolean;
35
44
  readonly hosted: boolean;
36
45
  readonly capabilities: readonly string[];
46
+ readonly time?: number | undefined;
37
47
  request(method: string, args?: Record<string, unknown>): Promise<PlayerResult>;
38
48
  event(name: string, attrs?: Record<string, string | number | boolean | null>): void;
39
49
  }
@@ -160,6 +170,7 @@ export function playerPlatform(player: PlayerLink, local: LocalPersistence): Pla
160
170
  return {
161
171
  host: "player",
162
172
  capabilities,
173
+ time: player.time,
163
174
  handle(calls, answer) {
164
175
  // In order: a store write through the save reads it first, so calls never interleave.
165
176
  for (const call of calls) {
@@ -192,6 +203,9 @@ export function deferredPlatform(ready: Promise<PlatformHost>, capabilities: rea
192
203
  return {
193
204
  host: "player",
194
205
  capabilities,
206
+ get time() {
207
+ return real?.time;
208
+ },
195
209
  handle(calls, answer) {
196
210
  if (real) real.handle(calls, answer);
197
211
  else held.push([calls, answer]);
@@ -201,3 +215,40 @@ export function deferredPlatform(ready: Promise<PlatformHost>, capabilities: rea
201
215
 
202
216
  /** How long a framed page waits for a Player's hello before the browser's storage answers. */
203
217
  export const HELLO_WAIT_MS = 1500;
218
+
219
+ /** The room ops the Player answers, by the engine's op name. */
220
+ const ROOM_OPS: Record<string, string> = { create: "room.create", join: "room.join", leave: "room.leave", send: "room.send" };
221
+
222
+ /**
223
+ * The Player's rooms (docs/platform-asks.md #16), or null for a Player that has none. Ops go
224
+ * to it in order without waiting (a send never waits for a join's answer: the Player queues
225
+ * them); `push` takes the events it pushes. A refused op becomes an `error` event naming it.
226
+ */
227
+ export function playerRooms(player: PlayerLink): (RoomHost & { push(event: Record<string, unknown>): void }) | null {
228
+ if (!player.capabilities.includes("room.join")) return null;
229
+ let deliver: ((events: unknown[]) => void) | null = null;
230
+ return {
231
+ host: "player",
232
+ handle(ops, to) {
233
+ deliver = to;
234
+ for (const op of ops) {
235
+ const name = String(op["op"]);
236
+ const method = ROOM_OPS[name];
237
+ const { op: _op, ...args } = op;
238
+ if (!method || !player.capabilities.includes(method)) {
239
+ to([{ type: "error", op: name, code: "unavailable", message: `this Player answers no ${method ?? name}` }]);
240
+ continue;
241
+ }
242
+ void player.request(method, args).then(
243
+ (r) => {
244
+ if (!r.ok) deliver?.([{ type: "error", op: name, code: r.error.code, message: r.error.message }]);
245
+ },
246
+ (e: unknown) => deliver?.([{ type: "error", op: name, code: "offline", message: e instanceof Error ? e.message : String(e) }]),
247
+ );
248
+ }
249
+ },
250
+ push(event) {
251
+ deliver?.([event]);
252
+ },
253
+ };
254
+ }
package/shell/player.ts CHANGED
@@ -39,6 +39,8 @@ export interface PlayerEvents {
39
39
  gesture?(): void;
40
40
  pause?(): void;
41
41
  resume?(): void;
42
+ /** A room event the Player pushed (`kind: "room"`, docs/platform-asks.md #16): the engine's own shape. */
43
+ room?(event: Record<string, unknown>): void;
42
44
  }
43
45
 
44
46
  /** A short fingerprint of an error message: `[a-z0-9-]{1,64}` (FNV-1a of the first line, hex). */
@@ -57,9 +59,9 @@ export type PlayerResult =
57
59
  | { readonly ok: true; readonly data: unknown; readonly time?: number }
58
60
  | { readonly ok: false; readonly error: { readonly code: string; readonly message: string; readonly retryable: boolean; readonly status?: number; readonly revision?: number }; readonly time?: number };
59
61
 
60
- /** A message's `time` field, when it is a finite number. */
62
+ /** A message's `time` field, when it is finite UTC milliseconds since the epoch. */
61
63
  export const timeOf = (m: Record<string, unknown>): { time?: number } =>
62
- typeof m["time"] === "number" && Number.isFinite(m["time"]) ? { time: m["time"] } : {};
64
+ typeof m["time"] === "number" && Number.isFinite(m["time"]) && m["time"] >= 0 ? { time: m["time"] } : {};
63
65
 
64
66
  /** How long a call waits for its `result` before it fails with `timeout`. */
65
67
  export const CALL_TIMEOUT_MS = 15_000;
@@ -91,6 +93,7 @@ export function connectPlayer(events: PlayerEvents) {
91
93
  let started = false;
92
94
  let paused = false;
93
95
  let capabilities: readonly string[] = [];
96
+ let time: number | undefined;
94
97
  let nextCall = 1;
95
98
  const calls = new Map<string, (r: PlayerResult) => void>();
96
99
 
@@ -114,6 +117,7 @@ export function connectPlayer(events: PlayerEvents) {
114
117
  heard = true;
115
118
  if (m["started"] === true) started = true;
116
119
  paused = m["paused"] === true;
120
+ time = timeOf(m).time;
117
121
  capabilities = Array.isArray(m["capabilities"]) ? (m["capabilities"] as unknown[]).filter((c): c is string => typeof c === "string") : [];
118
122
  events.hello?.({ started: m["started"] === true, paused, capabilities });
119
123
  break;
@@ -156,6 +160,11 @@ export function connectPlayer(events: PlayerEvents) {
156
160
  paused = false;
157
161
  events.resume?.();
158
162
  break;
163
+ case "room": {
164
+ const event = m["event"];
165
+ if (event && typeof event === "object" && !Array.isArray(event)) events.room?.(event as Record<string, unknown>);
166
+ break;
167
+ }
159
168
  }
160
169
  };
161
170
  if (peer) addEventListener("message", onMessage);
@@ -189,6 +198,10 @@ export function connectPlayer(events: PlayerEvents) {
189
198
  get capabilities(): readonly string[] {
190
199
  return capabilities;
191
200
  },
201
+ /** The most recent hello's clock, before any calls have been answered. */
202
+ get time(): number | undefined {
203
+ return time;
204
+ },
192
205
  loading(value: number | null, label?: string) {
193
206
  if (readySent) return;
194
207
  if (value !== null) {
@@ -139,6 +139,7 @@ export interface PageConnection {
139
139
  readonly started: boolean;
140
140
  readonly paused: boolean;
141
141
  readonly capabilities: readonly string[];
142
+ readonly time: number | undefined;
142
143
  readonly page: PageInfo | null;
143
144
  /** Resolves once the hello is answered (true) or failed (false). */
144
145
  readonly hello: Promise<boolean>;
@@ -249,6 +250,7 @@ export function connectPage(game: PageGame, env: TransportEnv, events: PlayerEve
249
250
  // ── hello ──
250
251
  let hosted = false;
251
252
  let capabilities: readonly string[] = [];
253
+ let time: number | undefined;
252
254
  let page: PageInfo | null = null;
253
255
  const str = (v: unknown) => (typeof v === "string" && v ? v : null);
254
256
  const hello = serial(async () => {
@@ -256,6 +258,7 @@ export function connectPage(game: PageGame, env: TransportEnv, events: PlayerEve
256
258
  const m = r.data;
257
259
  if (r.status !== 200 || !m || m["kind"] !== "hello") return false;
258
260
  hosted = true;
261
+ time = timeOf(m).time;
259
262
  capabilities = Array.isArray(m["capabilities"]) ? (m["capabilities"] as unknown[]).filter((c): c is string => typeof c === "string") : [];
260
263
  const p = (m["page"] ?? null) as Record<string, unknown> | null;
261
264
  page = p && typeof p === "object" ? { title: str(p["title"]), cover: str(p["cover"]), backdrop: str(p["backdrop"]), back: str(p["back"]), portal: str(p["portal"]) } : null;
@@ -446,6 +449,9 @@ export function connectPage(game: PageGame, env: TransportEnv, events: PlayerEve
446
449
  get capabilities() {
447
450
  return capabilities;
448
451
  },
452
+ get time() {
453
+ return time;
454
+ },
449
455
  get page() {
450
456
  return page;
451
457
  },
package/src/depth.ts ADDED
@@ -0,0 +1,71 @@
1
+ // The depth wave's additions (IDL `render` geometry, lines, points and
2
+ // raycast; `debug`): thin calls into the host's binding symbols, typed by
3
+ // the generated declarations through index.ts.
4
+
5
+ import type { HostAbi } from "./generated/abi.ts";
6
+ import { boundHost } from "./runtime.ts";
7
+
8
+ type W = { readonly id: number };
9
+
10
+ function abi(): HostAbi<Uint32Array, unknown> {
11
+ const h = boundHost();
12
+ if (!h) throw new Error("no engine host is loaded: start the game through @origingame/engine's web or node loader");
13
+ return h.abi;
14
+ }
15
+
16
+ const parse = <T>(text: string): T => JSON.parse(text) as T;
17
+ const json = (value: unknown): string | undefined => (value === undefined ? undefined : JSON.stringify(value));
18
+
19
+ // A typed array crosses the wire as a plain array (JSON.stringify writes
20
+ // a Float32Array as an object keyed by index).
21
+ const ATTRIBUTES = ["positions", "normals", "uvs", "colors", "tangents", "indices"] as const;
22
+ function attributes(desc: unknown): string | undefined {
23
+ if (desc === undefined || desc === null || typeof desc !== "object") return json(desc);
24
+ const d: Record<string, unknown> = { ...(desc as Record<string, unknown>) };
25
+ for (const k of ATTRIBUTES) {
26
+ const v = d[k];
27
+ if (ArrayBuffer.isView(v)) d[k] = Array.from(v as unknown as ArrayLike<number>);
28
+ }
29
+ return JSON.stringify(d);
30
+ }
31
+
32
+ export const depthRender = Object.freeze({
33
+ bufferGeometry: (world: W, desc?: unknown) => parse(abi().og_render_buffer_geometry(world.id, attributes(desc))),
34
+ updateGeometry: (world: W, geometry: number, desc?: unknown) =>
35
+ parse(abi().og_render_update_geometry(world.id, geometry, attributes(desc))),
36
+ geometryInfo: (world: W, geometry: number) => parse(abi().og_render_geometry_info(world.id, geometry)),
37
+ cylinderGeometry: (world: W, desc?: unknown) => parse(abi().og_render_cylinder_geometry(world.id, json(desc))),
38
+ coneGeometry: (world: W, desc?: unknown) => parse(abi().og_render_cone_geometry(world.id, json(desc))),
39
+ capsuleGeometry: (world: W, desc?: unknown) => parse(abi().og_render_capsule_geometry(world.id, json(desc))),
40
+ torusGeometry: (world: W, desc?: unknown) => parse(abi().og_render_torus_geometry(world.id, json(desc))),
41
+ ringGeometry: (world: W, desc?: unknown) => parse(abi().og_render_ring_geometry(world.id, json(desc))),
42
+ circleGeometry: (world: W, desc?: unknown) => parse(abi().og_render_circle_geometry(world.id, json(desc))),
43
+ tubeGeometry: (world: W, desc?: unknown) => parse(abi().og_render_tube_geometry(world.id, json(desc))),
44
+ lines: (world: W, desc?: unknown) => parse(abi().og_render_lines(world.id, json(desc))),
45
+ points: (world: W, desc?: unknown) => parse(abi().og_render_points(world.id, json(desc))),
46
+ raycast: (world: W, ray: unknown, desc?: unknown) => parse(abi().og_render_raycast(world.id, JSON.stringify(ray), json(desc))),
47
+ });
48
+
49
+ export const debug = Object.freeze({
50
+ line: (world: W, from: unknown, to: unknown, desc?: unknown) =>
51
+ abi().og_debug_line(world.id, JSON.stringify(from), JSON.stringify(to), json(desc)),
52
+ box: (world: W, center: unknown, size: unknown, desc?: unknown) =>
53
+ abi().og_debug_box(world.id, JSON.stringify(center), JSON.stringify(size), json(desc)),
54
+ sphere: (world: W, center: unknown, radius: number, desc?: unknown) =>
55
+ abi().og_debug_sphere(world.id, JSON.stringify(center), radius, json(desc)),
56
+ arrow: (world: W, from: unknown, to: unknown, desc?: unknown) =>
57
+ abi().og_debug_arrow(world.id, JSON.stringify(from), JSON.stringify(to), json(desc)),
58
+ axes: (world: W, position: unknown, size: number, desc?: unknown) =>
59
+ abi().og_debug_axes(world.id, JSON.stringify(position), size, json(desc)),
60
+ text: (world: W, position: unknown, text: string, desc?: unknown) =>
61
+ abi().og_debug_text(world.id, JSON.stringify(position), text, json(desc)),
62
+ clear: (world: W) => abi().og_debug_clear(world.id),
63
+ setView: (world: W, view: string) => abi().og_debug_set_view(world.id, JSON.stringify(view)),
64
+ // A publish build answers every debug member with a refusal: the view is none there.
65
+ view: (world: W) => {
66
+ const v = parse<unknown>(abi().og_debug_view(world.id));
67
+ return typeof v === "string" ? v : "none";
68
+ },
69
+ setEnabled: (world: W, enabled: boolean) => abi().og_debug_set_enabled(world.id, enabled),
70
+ enabled: (world: W) => abi().og_debug_enabled(world.id),
71
+ });
package/src/e6.ts CHANGED
@@ -1,4 +1,4 @@
1
- // The E6 modules' namespaces (IDL `nav`, `ai`, `platform`, `i18n`): thin calls into the host's
1
+ // The E6 modules' namespaces (IDL `nav`, `ai`, `platform`, `i18n`) and the depth wave's `net`: thin calls into the host's
2
2
  // binding symbols, typed by the generated declarations through index.ts.
3
3
 
4
4
  import type { HostAbi } from "./generated/abi.ts";
@@ -58,8 +58,39 @@ export const platform = Object.freeze({
58
58
  result: (world: W, request: number) => parse(abi().og_platform_result(world.id, request)),
59
59
  info: (world: W) => parse(abi().og_platform_info(world.id)),
60
60
  now: (world: W) => abi().og_platform_now(world.id),
61
+ roomCreate: (world: W, desc?: unknown) => parse(abi().og_platform_room_create(world.id, json(desc))),
62
+ roomJoin: (world: W, room: string) => parse(abi().og_platform_room_join(world.id, room)),
63
+ roomLeave: (world: W) => parse(abi().og_platform_room_leave(world.id)),
64
+ // Bytes cross as { "$og.bytes": base64 } (og-runtime net.rs BYTES_KEY).
65
+ roomSend: (world: W, data: unknown, to?: number) =>
66
+ parse(abi().og_platform_room_send(world.id, JSON.stringify(data instanceof Uint8Array ? { "$og.bytes": toBase64(data) } : (data ?? null)), to)),
67
+ roomPeers: (world: W) => parse(abi().og_platform_room_peers(world.id)),
68
+ roomInfo: (world: W) => parse(abi().og_platform_room_info(world.id)),
69
+ roomMessage: (world: W, message: number) => {
70
+ const m = parse<{ bytes: string | null } | null>(abi().og_platform_room_message(world.id, message));
71
+ return m && { ...m, bytes: typeof m.bytes === "string" ? fromBase64(m.bytes) : null };
72
+ },
61
73
  });
62
74
 
75
+ export const net = Object.freeze({
76
+ submit: (world: W, input?: unknown) => parse(abi().og_net_submit(world.id, json(input))),
77
+ status: (world: W) => parse(abi().og_net_status(world.id)),
78
+ hash: (world: W) => parse<string>(abi().og_net_hash(world.id)),
79
+ });
80
+
81
+ function toBase64(bytes: Uint8Array): string {
82
+ let bin = "";
83
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!);
84
+ return btoa(bin);
85
+ }
86
+
87
+ function fromBase64(text: string): Uint8Array {
88
+ const bin = atob(text);
89
+ const out = new Uint8Array(bin.length);
90
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
91
+ return out;
92
+ }
93
+
63
94
  export const i18n = Object.freeze({
64
95
  load: (world: W, path: string) => parse(abi().og_i18n_load(world.id, path)),
65
96
  setLocale: (world: W, locale: string) => parse(abi().og_i18n_set_locale(world.id, locale)),
package/src/env.d.ts CHANGED
@@ -22,6 +22,7 @@ declare module "node:fs" {
22
22
  export function readFileSync(path: string): Uint8Array;
23
23
  export function readFileSync(path: string, encoding: string): string;
24
24
  export function writeFileSync(path: string, data: string | Uint8Array): void;
25
+ export function writeSync(fd: number, data: Uint8Array, offset?: number, length?: number): number;
25
26
  export function rmSync(path: string, options?: { recursive?: boolean; force?: boolean }): void;
26
27
  export function mkdirSync(path: string, options?: { recursive?: boolean }): void;
27
28
  export function copyFileSync(from: string, to: string): void;
@@ -51,6 +52,8 @@ declare module "bun:test" {
51
52
  export namespace test {
52
53
  /** The test runs unless `condition` holds (then it is reported skipped). */
53
54
  function skipIf(condition: boolean): (name: string, body: Body, timeout?: number) => void;
55
+ /** One test per row, the row spread into the body (`%s` in the name is the row's first value). */
56
+ function each<T extends readonly unknown[]>(rows: readonly T[]): (name: string, body: (...row: [...T]) => unknown, timeout?: number) => void;
54
57
  }
55
58
  export function describe(name: string, body: () => void): void;
56
59
  export function beforeAll(body: Body): void;
@@ -73,7 +76,8 @@ declare module "bun:test" {
73
76
  toThrow(pattern?: RegExp | string): void;
74
77
  not: Matchers;
75
78
  }
76
- export function expect(value: unknown): Matchers;
79
+ /** `message` names the assertion in its failure (bun's custom failure message). */
80
+ export function expect(value: unknown, message?: string): Matchers;
77
81
  }
78
82
 
79
83
  declare const process: {