@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/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,10 +24,19 @@
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 {
@@ -201,3 +210,40 @@ export function deferredPlatform(ready: Promise<PlatformHost>, capabilities: rea
201
210
 
202
211
  /** How long a framed page waits for a Player's hello before the browser's storage answers. */
203
212
  export const HELLO_WAIT_MS = 1500;
213
+
214
+ /** The room ops the Player answers, by the engine's op name. */
215
+ const ROOM_OPS: Record<string, string> = { create: "room.create", join: "room.join", leave: "room.leave", send: "room.send" };
216
+
217
+ /**
218
+ * The Player's rooms (docs/platform-asks.md #16), or null for a Player that has none. Ops go
219
+ * to it in order without waiting (a send never waits for a join's answer: the Player queues
220
+ * them); `push` takes the events it pushes. A refused op becomes an `error` event naming it.
221
+ */
222
+ export function playerRooms(player: PlayerLink): (RoomHost & { push(event: Record<string, unknown>): void }) | null {
223
+ if (!player.capabilities.includes("room.join")) return null;
224
+ let deliver: ((events: unknown[]) => void) | null = null;
225
+ return {
226
+ host: "player",
227
+ handle(ops, to) {
228
+ deliver = to;
229
+ for (const op of ops) {
230
+ const name = String(op["op"]);
231
+ const method = ROOM_OPS[name];
232
+ const { op: _op, ...args } = op;
233
+ if (!method || !player.capabilities.includes(method)) {
234
+ to([{ type: "error", op: name, code: "unavailable", message: `this Player answers no ${method ?? name}` }]);
235
+ continue;
236
+ }
237
+ void player.request(method, args).then(
238
+ (r) => {
239
+ if (!r.ok) deliver?.([{ type: "error", op: name, code: r.error.code, message: r.error.message }]);
240
+ },
241
+ (e: unknown) => deliver?.([{ type: "error", op: name, code: "offline", message: e instanceof Error ? e.message : String(e) }]),
242
+ );
243
+ }
244
+ },
245
+ push(event) {
246
+ deliver?.([event]);
247
+ },
248
+ };
249
+ }
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). */
@@ -156,6 +158,11 @@ export function connectPlayer(events: PlayerEvents) {
156
158
  paused = false;
157
159
  events.resume?.();
158
160
  break;
161
+ case "room": {
162
+ const event = m["event"];
163
+ if (event && typeof event === "object" && !Array.isArray(event)) events.room?.(event as Record<string, unknown>);
164
+ break;
165
+ }
159
166
  }
160
167
  };
161
168
  if (peer) addEventListener("message", onMessage);
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: {
@@ -41,6 +41,12 @@ export interface HostAbi<A, V> {
41
41
  og_platform_answer(engine: number, request: number, answer: string): boolean;
42
42
  /** Say what answers this engine's platform calls: JSON `{ host, capabilities }` (`player`, `local` or `none`; the methods it answers), as og.platform.info reports. */
43
43
  og_platform_set_host(engine: number, info: string): void;
44
+ /** The room ops (IDL `platform` rooms, og.net's lockstep) made since the last call, oldest first, as JSON `[op]`: `{ op: "create", name, capacity }`, `{ op: "join", room, name }`, `{ op: "leave" }`, `{ op: "send", to?, channel?, data? | bytes? }` (bytes base64; a channel starting `og.` is the engine's own). The host carries each in order (docs/design.md §2.8). */
45
+ og_room_take(engine: number): string;
46
+ /** One room event from the host, JSON: `{ type: "joined", room, peer, peers: [{ peer, name }] }`, `{ type: "peer_joined", peer, name }`, `{ type: "peer_left", peer }`, `{ type: "message", from, channel?, data? | bytes? }`, `{ type: "left", reason }` or `{ type: "error", op, code, message }`. Lockstep's messages act between ticks at once; the game's events are sent at the start of the next tick. False for a malformed event. */
47
+ og_room_deliver(engine: number, event: string): boolean;
48
+ /** Say what carries this engine's rooms: JSON `{ host, join? }` (`relay`, `player` or `none`), as og.platform.roomInfo reports; with `none` roomCreate and roomJoin are refused. `join` is the room a Play link named (`?room=<id>`): the engine joins it once, itself, and og.RoomJoined answers. */
49
+ og_room_set_host(engine: number, info: string): void;
44
50
  /** `defineComponent` */
45
51
  og_reflect_define_component(name: string, fields: string): string;
46
52
  /** `defineResource` */
@@ -227,6 +233,40 @@ export interface HostAbi<A, V> {
227
233
  og_render_capabilities(world: number): string;
228
234
  /** `og.render.text` */
229
235
  og_render_text(world: number, desc: string | undefined): string;
236
+ /** `og.render.bufferGeometry` */
237
+ og_render_buffer_geometry(world: number, desc: string | undefined): string;
238
+ /** `og.render.updateGeometry` */
239
+ og_render_update_geometry(world: number, geometry: number, desc: string | undefined): string;
240
+ /** `og.render.geometryInfo` */
241
+ og_render_geometry_info(world: number, geometry: number): string;
242
+ /** `og.render.cylinderGeometry` */
243
+ og_render_cylinder_geometry(world: number, desc: string | undefined): string;
244
+ /** `og.render.coneGeometry` */
245
+ og_render_cone_geometry(world: number, desc: string | undefined): string;
246
+ /** `og.render.capsuleGeometry` */
247
+ og_render_capsule_geometry(world: number, desc: string | undefined): string;
248
+ /** `og.render.torusGeometry` */
249
+ og_render_torus_geometry(world: number, desc: string | undefined): string;
250
+ /** `og.render.ringGeometry` */
251
+ og_render_ring_geometry(world: number, desc: string | undefined): string;
252
+ /** `og.render.circleGeometry` */
253
+ og_render_circle_geometry(world: number, desc: string | undefined): string;
254
+ /** `og.render.tubeGeometry` */
255
+ og_render_tube_geometry(world: number, desc: string | undefined): string;
256
+ /** `og.render.lines` */
257
+ og_render_lines(world: number, desc: string | undefined): string;
258
+ /** `og.render.points` */
259
+ og_render_points(world: number, desc: string | undefined): string;
260
+ /** `og.render.raycast` */
261
+ og_render_raycast(world: number, ray: string, desc: string | undefined): string;
262
+ /** `og.render.renderTarget` */
263
+ og_render_render_target(world: number, desc: string | undefined): string;
264
+ /** `og.render.setPostParam` */
265
+ og_render_set_post_param(world: number, post: number, name: string, value: string): string;
266
+ /** `og.render.postErrors` */
267
+ og_render_post_errors(world: number): string;
268
+ /** `og.render.bakedLighting` */
269
+ og_render_baked_lighting(world: number): string;
230
270
  /** `og.load.bundle` */
231
271
  og_load_bundle(world: number, path: string): string;
232
272
  /** `og.load.bundleInfo` */
@@ -251,12 +291,14 @@ export interface HostAbi<A, V> {
251
291
  og_load_emitter(world: number, path: string): string;
252
292
  /** `og.load.shader` */
253
293
  og_load_shader(world: number, path: string): string;
294
+ /** `og.load.post` */
295
+ og_load_post(world: number, path: string): string;
254
296
  /** `og.load.texture` */
255
297
  og_load_texture(world: number, path: string): string;
256
298
  /** `og.load.textureArray` */
257
299
  og_load_texture_array(world: number, path: string): string;
258
- /** `og.load.skin` */
259
- og_load_skin(world: number, path: string): string;
300
+ /** `og.load.style` */
301
+ og_load_style(world: number, path: string): string;
260
302
  /** `og.load.look` */
261
303
  og_load_look(world: number, path: string): string;
262
304
  /** `og.load.lookInfo` */
@@ -279,6 +321,14 @@ export interface HostAbi<A, V> {
279
321
  og_load_flipbook(world: number, path: string): string;
280
322
  /** `og.load.tilemap` */
281
323
  og_load_tilemap(world: number, path: string): string;
324
+ /** `og.load.video` */
325
+ og_load_video(world: number, path: string): string;
326
+ /** `og.load.videoInfo` */
327
+ og_load_video_info(world: number, video: number): string;
328
+ /** `og.load.timeline` */
329
+ og_load_timeline(world: number, path: string): string;
330
+ /** `og.load.rig2d` */
331
+ og_load_rig2d(world: number, path: string): string;
282
332
  /** `og.physics.raycast` */
283
333
  og_physics_raycast(world: number, desc: string | undefined): string;
284
334
  /** `og.physics.shapeCast` */
@@ -323,6 +373,10 @@ export interface HostAbi<A, V> {
323
373
  og_anim_humanoid(world: number, desc: string | undefined): string;
324
374
  /** `og.anim.retarget` */
325
375
  og_anim_retarget(world: number, desc: string | undefined): string;
376
+ /** `og.anim.rig2dInfo` */
377
+ og_anim_rig2d_info(world: number, rig: number): string;
378
+ /** `og.anim.skeleton2dBoxes` */
379
+ og_anim_skeleton2d_boxes(world: number, entity: number): string;
326
380
  /** `og.tween.to` */
327
381
  og_tween_to(world: number, desc: string | undefined): string;
328
382
  /** `og.tween.sequence` */
@@ -421,6 +475,20 @@ export interface HostAbi<A, V> {
421
475
  og_platform_info(world: number): string;
422
476
  /** `og.platform.now` */
423
477
  og_platform_now(world: number): number;
478
+ /** `og.platform.roomCreate` */
479
+ og_platform_room_create(world: number, desc: string | undefined): string;
480
+ /** `og.platform.roomJoin` */
481
+ og_platform_room_join(world: number, room: string): string;
482
+ /** `og.platform.roomLeave` */
483
+ og_platform_room_leave(world: number): string;
484
+ /** `og.platform.roomSend` */
485
+ og_platform_room_send(world: number, data: string, to: number | undefined): string;
486
+ /** `og.platform.roomPeers` */
487
+ og_platform_room_peers(world: number): string;
488
+ /** `og.platform.roomInfo` */
489
+ og_platform_room_info(world: number): string;
490
+ /** `og.platform.roomMessage` */
491
+ og_platform_room_message(world: number, message: number): string;
424
492
  /** `og.i18n.load` */
425
493
  og_i18n_load(world: number, path: string): string;
426
494
  /** `og.i18n.setLocale` */
@@ -629,6 +697,14 @@ export interface HostAbi<A, V> {
629
697
  og_engine_probe_listen(engine_probe: number, seconds: number | undefined): string;
630
698
  /** `EngineProbe.memory` */
631
699
  og_engine_probe_memory(engine_probe: number): string;
700
+ /** `EngineProbe.describe` */
701
+ og_engine_probe_describe(engine_probe: number): string;
702
+ /** `EngineProbe.expect` */
703
+ og_engine_probe_expect(engine_probe: number, what: string): string;
704
+ /** `EngineProbe.expectations` */
705
+ og_engine_probe_expectations(engine_probe: number): string;
706
+ /** `EngineProbe.saveScene` */
707
+ og_engine_probe_save_scene(engine_probe: number, write: boolean | undefined, spawned: boolean | undefined): string;
632
708
  /** `og.terrain.load` */
633
709
  og_terrain_load(world: number, path: string): string;
634
710
  /** `og.terrain.sample` */
@@ -651,6 +727,54 @@ export interface HostAbi<A, V> {
651
727
  og_stream_budget(world: number): string;
652
728
  /** `og.stream.setBudget` */
653
729
  og_stream_set_budget(world: number, bytes: number): string;
730
+ /** `og.debug.line` */
731
+ og_debug_line(world: number, from: string, to: string, desc: string | undefined): void;
732
+ /** `og.debug.box` */
733
+ og_debug_box(world: number, center: string, size: string, desc: string | undefined): void;
734
+ /** `og.debug.sphere` */
735
+ og_debug_sphere(world: number, center: string, radius: number, desc: string | undefined): void;
736
+ /** `og.debug.arrow` */
737
+ og_debug_arrow(world: number, from: string, to: string, desc: string | undefined): void;
738
+ /** `og.debug.axes` */
739
+ og_debug_axes(world: number, position: string, size: number, desc: string | undefined): void;
740
+ /** `og.debug.text` */
741
+ og_debug_text(world: number, position: string, text: string, desc: string | undefined): void;
742
+ /** `og.debug.clear` */
743
+ og_debug_clear(world: number): void;
744
+ /** `og.debug.setView` */
745
+ og_debug_set_view(world: number, view: string): void;
746
+ /** `og.debug.view` */
747
+ og_debug_view(world: number): string;
748
+ /** `og.debug.setEnabled` */
749
+ og_debug_set_enabled(world: number, enabled: boolean): void;
750
+ /** `og.debug.enabled` */
751
+ og_debug_enabled(world: number): boolean;
752
+ /** `og.path.create` */
753
+ og_path_create(world: number, desc: string | undefined): string;
754
+ /** `og.path.find` */
755
+ og_path_find(world: number, name: string): string;
756
+ /** `og.path.sample` */
757
+ og_path_sample(world: number, path: number, t: number): string;
758
+ /** `og.path.length` */
759
+ og_path_length(world: number, path: number): string;
760
+ /** `og.path.nearest` */
761
+ og_path_nearest(world: number, path: number, point: string): string;
762
+ /** `og.timeline.info` */
763
+ og_timeline_info(world: number, asset: number): string;
764
+ /** `og.timeline.play` */
765
+ og_timeline_play(world: number, entity: number): string;
766
+ /** `og.timeline.pause` */
767
+ og_timeline_pause(world: number, entity: number): string;
768
+ /** `og.timeline.seek` */
769
+ og_timeline_seek(world: number, entity: number, time: number): string;
770
+ /** `og.timeline.stop` */
771
+ og_timeline_stop(world: number, entity: number): string;
772
+ /** `og.net.submit` */
773
+ og_net_submit(world: number, input: string | undefined): string;
774
+ /** `og.net.status` */
775
+ og_net_status(world: number): string;
776
+ /** `og.net.hash` */
777
+ og_net_hash(world: number): string;
654
778
  }
655
779
 
656
780
  /** Every binding symbol, in binding-table order, then the lifecycle members. */
@@ -748,6 +872,23 @@ export const SYMBOLS: readonly string[] = [
748
872
  "og_render_look",
749
873
  "og_render_capabilities",
750
874
  "og_render_text",
875
+ "og_render_buffer_geometry",
876
+ "og_render_update_geometry",
877
+ "og_render_geometry_info",
878
+ "og_render_cylinder_geometry",
879
+ "og_render_cone_geometry",
880
+ "og_render_capsule_geometry",
881
+ "og_render_torus_geometry",
882
+ "og_render_ring_geometry",
883
+ "og_render_circle_geometry",
884
+ "og_render_tube_geometry",
885
+ "og_render_lines",
886
+ "og_render_points",
887
+ "og_render_raycast",
888
+ "og_render_render_target",
889
+ "og_render_set_post_param",
890
+ "og_render_post_errors",
891
+ "og_render_baked_lighting",
751
892
  "og_load_bundle",
752
893
  "og_load_bundle_info",
753
894
  "og_load_default_bundle",
@@ -760,9 +901,10 @@ export const SYMBOLS: readonly string[] = [
760
901
  "og_load_environment",
761
902
  "og_load_emitter",
762
903
  "og_load_shader",
904
+ "og_load_post",
763
905
  "og_load_texture",
764
906
  "og_load_texture_array",
765
- "og_load_skin",
907
+ "og_load_style",
766
908
  "og_load_look",
767
909
  "og_load_look_info",
768
910
  "og_load_palette",
@@ -774,6 +916,10 @@ export const SYMBOLS: readonly string[] = [
774
916
  "og_load_rig",
775
917
  "og_load_flipbook",
776
918
  "og_load_tilemap",
919
+ "og_load_video",
920
+ "og_load_video_info",
921
+ "og_load_timeline",
922
+ "og_load_rig2d",
777
923
  "og_physics_raycast",
778
924
  "og_physics_shape_cast",
779
925
  "og_physics_overlap",
@@ -796,6 +942,8 @@ export const SYMBOLS: readonly string[] = [
796
942
  "og_anim_graph",
797
943
  "og_anim_humanoid",
798
944
  "og_anim_retarget",
945
+ "og_anim_rig2d_info",
946
+ "og_anim_skeleton2d_boxes",
799
947
  "og_tween_to",
800
948
  "og_tween_sequence",
801
949
  "og_tween_parallel",
@@ -845,6 +993,13 @@ export const SYMBOLS: readonly string[] = [
845
993
  "og_platform_result",
846
994
  "og_platform_info",
847
995
  "og_platform_now",
996
+ "og_platform_room_create",
997
+ "og_platform_room_join",
998
+ "og_platform_room_leave",
999
+ "og_platform_room_send",
1000
+ "og_platform_room_peers",
1001
+ "og_platform_room_info",
1002
+ "og_platform_room_message",
848
1003
  "og_i18n_load",
849
1004
  "og_i18n_set_locale",
850
1005
  "og_i18n_locale",
@@ -949,6 +1104,10 @@ export const SYMBOLS: readonly string[] = [
949
1104
  "og_engine_probe_commands",
950
1105
  "og_engine_probe_listen",
951
1106
  "og_engine_probe_memory",
1107
+ "og_engine_probe_describe",
1108
+ "og_engine_probe_expect",
1109
+ "og_engine_probe_expectations",
1110
+ "og_engine_probe_save_scene",
952
1111
  "og_terrain_load",
953
1112
  "og_terrain_sample",
954
1113
  "og_terrain_bounds",
@@ -960,6 +1119,30 @@ export const SYMBOLS: readonly string[] = [
960
1119
  "og_stream_release",
961
1120
  "og_stream_budget",
962
1121
  "og_stream_set_budget",
1122
+ "og_debug_line",
1123
+ "og_debug_box",
1124
+ "og_debug_sphere",
1125
+ "og_debug_arrow",
1126
+ "og_debug_axes",
1127
+ "og_debug_text",
1128
+ "og_debug_clear",
1129
+ "og_debug_set_view",
1130
+ "og_debug_view",
1131
+ "og_debug_set_enabled",
1132
+ "og_debug_enabled",
1133
+ "og_path_create",
1134
+ "og_path_find",
1135
+ "og_path_sample",
1136
+ "og_path_length",
1137
+ "og_path_nearest",
1138
+ "og_timeline_info",
1139
+ "og_timeline_play",
1140
+ "og_timeline_pause",
1141
+ "og_timeline_seek",
1142
+ "og_timeline_stop",
1143
+ "og_net_submit",
1144
+ "og_net_status",
1145
+ "og_net_hash",
963
1146
  "og_host_set_dispatcher",
964
1147
  "og_engine_create",
965
1148
  "og_engine_destroy",
@@ -977,4 +1160,7 @@ export const SYMBOLS: readonly string[] = [
977
1160
  "og_platform_take",
978
1161
  "og_platform_answer",
979
1162
  "og_platform_set_host",
1163
+ "og_room_take",
1164
+ "og_room_deliver",
1165
+ "og_room_set_host",
980
1166
  ];