@origingame/engine 0.0.0-stage → 0.1.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 (158) hide show
  1. package/bin/og.mjs +25 -0
  2. package/dist/web/origin-engine.d.ts +1233 -0
  3. package/dist/web/origin-engine.js +8628 -0
  4. package/dist/web/origin-engine_bg.wasm +0 -0
  5. package/dist/web/origin-engine_bg.wasm.d.ts +366 -0
  6. package/dist/web-publish/origin-engine-lanes.js +8509 -0
  7. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  8. package/dist/web-publish/origin-engine.d.ts +1176 -0
  9. package/dist/web-publish/origin-engine.js +8313 -0
  10. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  11. package/dist/web-publish/origin-engine_bg.wasm.d.ts +347 -0
  12. package/dist/web-publish/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
  13. package/dist/web-publish-2d/origin-engine-lanes.js +8509 -0
  14. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  15. package/dist/web-publish-2d/origin-engine.d.ts +1176 -0
  16. package/dist/web-publish-2d/origin-engine.js +8313 -0
  17. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  18. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +347 -0
  19. package/dist/web-publish-2d/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
  20. package/knowledge/README.md +31 -0
  21. package/knowledge/knowledge.json +13 -0
  22. package/knowledge/looks/catalogue.json +1871 -0
  23. package/knowledge/package.json +33 -0
  24. package/knowledge/rules/platform.json +329 -0
  25. package/knowledge/scaffold/README.md +27 -0
  26. package/knowledge/scaffold/distribution.json +48 -0
  27. package/knowledge/scaffold/origin-engine.json +19 -0
  28. package/knowledge/scaffold/template/.origin/setup +14 -0
  29. package/knowledge/scaffold/template/LICENSES.md +23 -0
  30. package/knowledge/scaffold/template/README.md +37 -0
  31. package/knowledge/scaffold/template/assets/ai/guard.ogt +48 -0
  32. package/knowledge/scaffold/template/assets/audio/click.ogt +9 -0
  33. package/knowledge/scaffold/template/assets/fonts/NotoSansSC-Regular-subset.ttf.ogt +12 -0
  34. package/knowledge/scaffold/template/assets/fonts/OFL.txt +93 -0
  35. package/knowledge/scaffold/template/assets/looks/origin-game.ogt +12 -0
  36. package/knowledge/scaffold/template/assets/nav/yard.ogt +27 -0
  37. package/knowledge/scaffold/template/assets/scenes/main.ogt +34 -0
  38. package/knowledge/scaffold/template/assets/strings/en.ogt +14 -0
  39. package/knowledge/scaffold/template/assets/textures/stone_n.png.ogt +7 -0
  40. package/knowledge/scaffold/template/gitattributes +22 -0
  41. package/knowledge/scaffold/template/package.json +18 -0
  42. package/knowledge/scaffold/template/project.ogt +25 -0
  43. package/knowledge/scaffold/template/src/main.ts +246 -0
  44. package/knowledge/scaffold/template/tests/guard.og.ts +67 -0
  45. package/knowledge/scaffold/template/tests/smoke.og.ts +43 -0
  46. package/knowledge/scaffold/template/tsconfig.json +17 -0
  47. package/knowledge/scripts/lib.ts +203 -0
  48. package/knowledge/scripts/mount.ts +210 -0
  49. package/knowledge/scripts/pack.ts +24 -0
  50. package/knowledge/scripts/scaffold.ts +72 -0
  51. package/knowledge/scripts/validate.ts +749 -0
  52. package/knowledge/skills/og-ai/SKILL.md +386 -0
  53. package/knowledge/skills/og-audio/SKILL.md +191 -0
  54. package/knowledge/skills/og-build/SKILL.md +330 -0
  55. package/knowledge/skills/og-core/SKILL.md +285 -0
  56. package/knowledge/skills/og-feel/SKILL.md +385 -0
  57. package/knowledge/skills/og-game/SKILL.md +1479 -0
  58. package/knowledge/skills/og-i18n/SKILL.md +220 -0
  59. package/knowledge/skills/og-platform-sdk/SKILL.md +199 -0
  60. package/knowledge/skills/og-taste/SKILL.md +537 -0
  61. package/knowledge/skills/og-ui/SKILL.md +444 -0
  62. package/knowledge/wiki/ai.md +627 -0
  63. package/knowledge/wiki/anim.md +1187 -0
  64. package/knowledge/wiki/atmosphere.md +636 -0
  65. package/knowledge/wiki/audio.md +732 -0
  66. package/knowledge/wiki/camera.md +711 -0
  67. package/knowledge/wiki/commands.md +357 -0
  68. package/knowledge/wiki/game.md +220 -0
  69. package/knowledge/wiki/i18n.md +213 -0
  70. package/knowledge/wiki/index.md +1027 -0
  71. package/knowledge/wiki/input.md +1714 -0
  72. package/knowledge/wiki/load.md +1280 -0
  73. package/knowledge/wiki/math.md +166 -0
  74. package/knowledge/wiki/nav.md +1154 -0
  75. package/knowledge/wiki/physics.md +3142 -0
  76. package/knowledge/wiki/platform.md +857 -0
  77. package/knowledge/wiki/probe.md +1692 -0
  78. package/knowledge/wiki/reflect.md +285 -0
  79. package/knowledge/wiki/render.md +3511 -0
  80. package/knowledge/wiki/rng.md +393 -0
  81. package/knowledge/wiki/stream.md +376 -0
  82. package/knowledge/wiki/system.md +197 -0
  83. package/knowledge/wiki/systems.md +43 -0
  84. package/knowledge/wiki/terrain.md +727 -0
  85. package/knowledge/wiki/tilemap.md +422 -0
  86. package/knowledge/wiki/time.md +150 -0
  87. package/knowledge/wiki/transform.md +230 -0
  88. package/knowledge/wiki/tween.md +580 -0
  89. package/knowledge/wiki/ui.md +8000 -0
  90. package/knowledge/wiki/world.md +909 -0
  91. package/package.json +68 -4
  92. package/schema/api.schema.json +29675 -0
  93. package/shell/README.md +22 -0
  94. package/shell/brand/README.md +23 -0
  95. package/shell/brand/icon-192.png +0 -0
  96. package/shell/brand/icon-512.png +0 -0
  97. package/shell/brand/mark-on-paper.svg +6 -0
  98. package/shell/brand/origin-mark.svg +5 -0
  99. package/shell/brand/origin-motion.css +251 -0
  100. package/shell/brand/tokens.css +20 -0
  101. package/shell/brand.ts +132 -0
  102. package/shell/page.ts +715 -0
  103. package/shell/platform.ts +203 -0
  104. package/shell/player.ts +238 -0
  105. package/shell/shell.css +185 -0
  106. package/shell/sw.ts +177 -0
  107. package/shell/transport.ts +515 -0
  108. package/shell/wire.ts +369 -0
  109. package/src/e6.ts +69 -0
  110. package/src/env.d.ts +100 -0
  111. package/src/generated/abi.ts +980 -0
  112. package/src/index.ts +193 -0
  113. package/src/node/audio.ts +86 -0
  114. package/src/node/build.ts +109 -0
  115. package/src/node/device.ts +981 -0
  116. package/src/node/doctor.ts +60 -0
  117. package/src/node/index.ts +102 -0
  118. package/src/node/platform.ts +24 -0
  119. package/src/node/playtest-run.ts +249 -0
  120. package/src/node/png.ts +130 -0
  121. package/src/node/polish.ts +976 -0
  122. package/src/node/run.ts +373 -0
  123. package/src/node/web-build.ts +808 -0
  124. package/src/p2.ts +35 -0
  125. package/src/platform-local.ts +211 -0
  126. package/src/playtest.ts +131 -0
  127. package/src/runtime.ts +1509 -0
  128. package/src/systems/cameras.ts +247 -0
  129. package/src/ui-kit/bind.ts +141 -0
  130. package/src/ui-kit/huds.ts +303 -0
  131. package/src/ui-kit/index.ts +36 -0
  132. package/src/ui-kit/menus.ts +187 -0
  133. package/src/ui-kit/settings.ts +131 -0
  134. package/src/ui-kit/store.ts +222 -0
  135. package/src/ui-kit/story.ts +89 -0
  136. package/src/ui-kit/util.ts +106 -0
  137. package/src/web/a11y.ts +429 -0
  138. package/src/web/audio-worklet.ts +193 -0
  139. package/src/web/audio.ts +307 -0
  140. package/src/web/basis.ts +239 -0
  141. package/src/web/gamepad.ts +123 -0
  142. package/src/web/host.ts +166 -0
  143. package/src/web/input.ts +237 -0
  144. package/src/web/lanes.ts +21 -0
  145. package/src/web/play.ts +20 -0
  146. package/src/web/rpc-ring.ts +110 -0
  147. package/src/web/settle.ts +122 -0
  148. package/src/web/shell.ts +643 -0
  149. package/src/web/text-input.ts +248 -0
  150. package/src/web/worker.ts +1002 -0
  151. package/types/bindings.json +13654 -0
  152. package/types/index.d.ts +20090 -0
  153. package/vendor/basis/LICENSE +201 -0
  154. package/vendor/basis/NOTICE +15 -0
  155. package/vendor/basis/README.md +18 -0
  156. package/vendor/basis/basis_transcoder.js +19 -0
  157. package/vendor/basis/basis_transcoder.wasm +0 -0
  158. package/README.md +0 -3
package/shell/wire.ts ADDED
@@ -0,0 +1,369 @@
1
+ // The page side of the platform's `og-probe` wire
2
+ // (origingame/origin/packages/contracts/src/probe-wire.ts, wire 1, probe
3
+ // protocol 5) over the engine's in-page probe. The engine runs in a Worker,
4
+ // so every member is answered asynchronously through the shell's request
5
+ // ring (or postMessage without isolation); the wire's shapes are the
6
+ // contract's whatever the engine answers.
7
+ //
8
+ // Rules kept exactly as the contract states them:
9
+ // - a call is answered only when `event.source` is the host window
10
+ // (`window.parent` when embedded, else `window.opener`) and `event.origin`
11
+ // is the origin the page was told: `window.__OG_STAGE_HOST__`; a page that
12
+ // was told no origin sends nothing;
13
+ // - every post names that origin; `"*"` is never used;
14
+ // - anything else (another `og`, another `v`, not a call, a foreign source or
15
+ // origin) gets no result and no error.
16
+ //
17
+ // A publish build serves the 7 player members (hello, tick, state, pick,
18
+ // entity, input, capture): `pause`, `resume`, `advance`, `bounds` and
19
+ // `patch` answer `unsupported` there; a dev (Stage) build serves them all.
20
+
21
+ export const PROBE_WIRE = "probe";
22
+ export const PROBE_WIRE_VERSION = 1;
23
+ export const PROBE_WIRE_PROTOCOL = 5;
24
+ export const STAGE_HOST_GLOBAL = "__OG_STAGE_HOST__";
25
+ export const PROBE_WIRE_RECORD_MAX_SECONDS = 15;
26
+ export const PROBE_WIRE_SLOW_MS = 100;
27
+ export const PROBE_WIRE_METHODS = ["hello", "tick", "state", "pause", "resume", "advance", "entities", "bounds", "entity", "patch", "capture", "record"] as const;
28
+ /** The probe's `advance` takes at most this many steps. */
29
+ const MAX_STEPS = 3600;
30
+ /** JSON-RPC: a member the build does not serve. */
31
+ const METHOD_NOT_FOUND = -32601;
32
+
33
+ export type WireErrorCode = "unsupported" | "refused" | "failed";
34
+
35
+ export class WireError extends Error {
36
+ readonly code: WireErrorCode;
37
+ constructor(code: WireErrorCode, message: string) {
38
+ super(message);
39
+ this.code = code;
40
+ }
41
+ }
42
+
43
+ /** One engine probe member by name (JSON-RPC params), resolving its result or rejecting with `{code}`. */
44
+ export type ProbeCall = (method: string, params?: Record<string, unknown>) => Promise<unknown>;
45
+
46
+ export interface WireOptions {
47
+ /** The window allowed to call. */
48
+ readonly host: () => Window | null | undefined;
49
+ /**
50
+ * A second caller: the page's own window, when a host in this same document (the
51
+ * Stage overlay injected into a top-level engine page) posts to it. Answered like the host.
52
+ */
53
+ readonly inPage?: () => Window | null | undefined;
54
+ /** The origin allowed to call and the only target of every post; null keeps the page silent. */
55
+ readonly hostOrigin: () => string | null;
56
+ /** The engine's probe, or null before the engine is ready. */
57
+ readonly probe: () => ProbeCall | null;
58
+ /** The render target in device pixels (what a full-size capture is). */
59
+ readonly targetSize: () => { width: number; height: number };
60
+ /** The canvas a recording is made from. */
61
+ readonly canvas: () => HTMLCanvasElement | null;
62
+ readonly frame?: (callback: () => void) => void;
63
+ readonly now?: () => number;
64
+ }
65
+
66
+ export interface WireState {
67
+ tick: number;
68
+ paused: boolean;
69
+ timings: { frameMs: number | null };
70
+ }
71
+
72
+ const finite = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
73
+ const plain = (v: unknown): Record<string, unknown> | null => (v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : null);
74
+
75
+ /** `ProbeWireState` from the engine's state head: `paused` and `timings.frameMs` are its own. */
76
+ export function wireState(head: unknown): WireState {
77
+ const h = plain(head) ?? {};
78
+ const t = plain(h["timings"]) ?? {};
79
+ const frameMs = finite(t["frameMs"]) && t["frameMs"] >= 0 ? t["frameMs"] : finite(t["step"]) && finite(t["render"]) ? t["step"] + t["render"] : null;
80
+ const tick = finite(h["tick"]) && Number.isSafeInteger(h["tick"]) && h["tick"] >= 0 ? h["tick"] : 0;
81
+ return { tick, paused: h["paused"] === true, timings: { frameMs } };
82
+ }
83
+
84
+ /** `{items}` of `bounds {id}`: the probe's order, ranges only where declared, the current value where it is a number. */
85
+ export function wireBounds(declared: unknown, values: unknown): { items: Array<Record<string, unknown>> } {
86
+ const d = plain(declared) ?? {};
87
+ const v = plain(values) ?? {};
88
+ return {
89
+ items: Object.entries(d).map(([parameter, row]) => {
90
+ const r = plain(row) ?? {};
91
+ const { label, path, min, max, step } = r;
92
+ const value = v[parameter];
93
+ return {
94
+ parameter,
95
+ ...(typeof label === "string" && label ? { label } : {}),
96
+ ...(typeof path === "string" && path ? { path } : {}),
97
+ ...(finite(min) && finite(max) && max >= min ? { min, max } : {}),
98
+ ...(finite(step) && step > 0 ? { step } : {}),
99
+ ...(finite(value) ? { value } : {}),
100
+ };
101
+ }),
102
+ };
103
+ }
104
+
105
+ /** The size a capture fits into `width`×`height` (either may be absent), never larger than the target. */
106
+ export function fitCapture(target: { width: number; height: number }, width?: number, height?: number): { width: number; height: number } {
107
+ const w = width && width > 0 ? width : 0;
108
+ const h = height && height > 0 ? height : 0;
109
+ if (!w && !h) return { width: 0, height: 0 };
110
+ const scale = Math.min(w ? w / target.width : Infinity, h ? h / target.height : Infinity, 1);
111
+ return { width: Math.max(1, Math.round(target.width * scale)), height: Math.max(1, Math.round(target.height * scale)) };
112
+ }
113
+
114
+ function blobBase64(blob: Blob): Promise<string> {
115
+ return new Promise((resolve, reject) => {
116
+ const reader = new FileReader();
117
+ reader.onerror = () => reject(new WireError("failed", "could not read the recording"));
118
+ reader.onload = () => {
119
+ const text = String(reader.result);
120
+ resolve(text.slice(text.indexOf(",") + 1));
121
+ };
122
+ reader.readAsDataURL(blob);
123
+ });
124
+ }
125
+
126
+ export function createWire(options: WireOptions) {
127
+ const now = options.now ?? (() => performance.now());
128
+ const frame = options.frame ?? ((cb: () => void) => requestAnimationFrame(cb));
129
+ let recording = false;
130
+
131
+ const need = (): ProbeCall => {
132
+ const p = options.probe();
133
+ if (!p) throw new WireError("unsupported", "the engine is not ready: no probe yet");
134
+ return p;
135
+ };
136
+ /** One engine member; a member this build does not serve is `unsupported`, any other error `failed`. */
137
+ const member = async (method: string, params: Record<string, unknown> = {}): Promise<unknown> => {
138
+ try {
139
+ return await need()(method, params);
140
+ } catch (e) {
141
+ if (e instanceof WireError) throw e;
142
+ const code = (e as { code?: unknown }).code;
143
+ if (code === METHOD_NOT_FOUND) throw new WireError("unsupported", `${method} is not served by this build (a publish build serves the player members only)`);
144
+ throw new WireError("failed", e instanceof Error ? e.message : String(e));
145
+ }
146
+ };
147
+ const text = (value: unknown, name: string): string => {
148
+ if (typeof value !== "string" || !value) throw new WireError("refused", `${name} must be a non-empty string`);
149
+ return value;
150
+ };
151
+ const state = async () => wireState(await member("state"));
152
+
153
+ function hello(): { protocol: 5 | null; substrate: "origin-engine" | null } {
154
+ return options.probe() ? { protocol: PROBE_WIRE_PROTOCOL, substrate: "origin-engine" } : { protocol: null, substrate: null };
155
+ }
156
+
157
+ async function answer(method: string, params: Record<string, unknown>, received: number): Promise<unknown> {
158
+ switch (method) {
159
+ case "hello":
160
+ return hello();
161
+ case "tick": {
162
+ const tick = await member("tick");
163
+ if (!finite(tick)) throw new WireError("failed", "the probe answered no tick");
164
+ return tick;
165
+ }
166
+ case "state":
167
+ return state();
168
+ case "pause":
169
+ case "resume": {
170
+ if ((await member(method)) !== true) throw new WireError("refused", `the probe refused ${method}`);
171
+ return state();
172
+ }
173
+ case "advance": {
174
+ const steps = params["steps"];
175
+ if (typeof steps !== "number" || !Number.isInteger(steps) || steps < 0 || steps > MAX_STEPS) throw new WireError("refused", `steps must be a whole number from 0 to ${MAX_STEPS}`);
176
+ await member("advance", { steps });
177
+ return state();
178
+ }
179
+ case "entities": {
180
+ const head = plain(await member("state"));
181
+ const list = Array.isArray(head?.["entities"]) ? (head!["entities"] as unknown[]) : [];
182
+ return {
183
+ items: list.flatMap((e) => {
184
+ const row = plain(e);
185
+ const id = row?.["id"];
186
+ if (typeof id !== "string" || !id) return [];
187
+ const label = row!["label"];
188
+ return [{ id, ...(typeof label === "string" && label ? { label } : {}) }];
189
+ }),
190
+ };
191
+ }
192
+ case "bounds": {
193
+ const id = text(params["id"], "id");
194
+ const declared = await member("bounds", { id });
195
+ if (!declared) throw new WireError("refused", `no entity ${id}`);
196
+ return wireBounds(declared, await member("entity", { id }));
197
+ }
198
+ case "entity": {
199
+ const id = text(params["id"], "id");
200
+ const values = await member("entity", { id });
201
+ if (!plain(values)) throw new WireError("refused", `no entity ${id}`);
202
+ return { id, params: values };
203
+ }
204
+ case "patch": {
205
+ const id = text(params["id"], "id");
206
+ const parameter = text(params["parameter"], "parameter");
207
+ if (!("value" in params)) throw new WireError("refused", "patch needs a value");
208
+ if ((await member("patch", { id, parameter, value: params["value"] })) !== true) throw new WireError("refused", `${parameter} of ${id} refused`);
209
+ // Applied before the next tick: the time to the first animation frame after the answer.
210
+ return new Promise((resolve) => {
211
+ let done = false;
212
+ const settle = () => {
213
+ if (done) return;
214
+ done = true;
215
+ resolve({ applied: true, roundTripMs: Math.max(0, Math.round((now() - received) * 10) / 10) });
216
+ };
217
+ frame(settle);
218
+ setTimeout(settle, PROBE_WIRE_SLOW_MS);
219
+ });
220
+ }
221
+ case "capture": {
222
+ const width = typeof params["width"] === "number" ? params["width"] : undefined;
223
+ const height = typeof params["height"] === "number" ? params["height"] : undefined;
224
+ if ((width !== undefined && !(width >= 0 && width <= 4096)) || (height !== undefined && !(height >= 0 && height <= 4096))) throw new WireError("refused", "width and height must be within 0–4096");
225
+ const size = fitCapture(options.targetSize(), width, height);
226
+ const shot = plain(await member("capture", size));
227
+ if (!shot || typeof shot["png"] !== "string" || !shot["png"]) throw new WireError("unsupported", "this page has no renderer to capture");
228
+ return { tick: finite(shot["tick"]) ? shot["tick"] : 0, width: Number(shot["width"]) || 0, height: Number(shot["height"]) || 0, png: shot["png"] };
229
+ }
230
+ case "record": {
231
+ const seconds = params["seconds"];
232
+ if (typeof seconds !== "number" || !(seconds > 0) || seconds > PROBE_WIRE_RECORD_MAX_SECONDS) throw new WireError("refused", `seconds must be within (0, ${PROBE_WIRE_RECORD_MAX_SECONDS}]`);
233
+ if (recording) throw new WireError("refused", "a recording is in progress");
234
+ const canvas = options.canvas() as (HTMLCanvasElement & { captureStream?: (fps: number) => MediaStream }) | null;
235
+ if (!canvas || !canvas.captureStream || typeof MediaRecorder === "undefined") throw new WireError("unsupported", "recording is not supported here");
236
+ recording = true;
237
+ try {
238
+ let recorder: MediaRecorder;
239
+ try {
240
+ recorder = new MediaRecorder(canvas.captureStream(30), MediaRecorder.isTypeSupported("video/webm") ? { mimeType: "video/webm" } : {});
241
+ } catch (e) {
242
+ throw new WireError("unsupported", `recording is not supported here: ${e instanceof Error ? e.message : String(e)}`);
243
+ }
244
+ const made = await new Promise<{ blob: Blob; mime: string; durationMs: number }>((resolve, reject) => {
245
+ const chunks: Blob[] = [];
246
+ const started = now();
247
+ recorder.ondataavailable = (e) => {
248
+ if (e.data.size) chunks.push(e.data);
249
+ };
250
+ recorder.onerror = () => reject(new WireError("failed", "the recorder failed"));
251
+ recorder.onstop = () => {
252
+ recorder.stream.getTracks().forEach((t) => t.stop());
253
+ const mime = recorder.mimeType || "video/webm";
254
+ resolve({ blob: new Blob(chunks, { type: mime }), mime, durationMs: Math.round(now() - started) });
255
+ };
256
+ recorder.start(1000);
257
+ setTimeout(() => recorder.state !== "inactive" && recorder.stop(), seconds * 1000);
258
+ });
259
+ return { mime: made.mime, base64: await blobBase64(made.blob), durationMs: made.durationMs };
260
+ } finally {
261
+ recording = false;
262
+ }
263
+ }
264
+ default:
265
+ throw new WireError("unsupported", `unknown method ${method}`);
266
+ }
267
+ }
268
+
269
+ /** The windows that may call: the host, and the page itself when an in-page host is allowed. */
270
+ const callers = (): Window[] => {
271
+ const out: Window[] = [];
272
+ const host = options.host();
273
+ if (host) out.push(host);
274
+ const self = options.inPage?.();
275
+ if (self && self !== host) out.push(self);
276
+ return out;
277
+ };
278
+
279
+ function post(message: Record<string, unknown>, to?: Window): void {
280
+ const origin = options.hostOrigin();
281
+ if (!origin || origin === "*") return;
282
+ for (const target of to ? [to] : callers()) {
283
+ try {
284
+ target.postMessage({ og: PROBE_WIRE, v: PROBE_WIRE_VERSION, ...message }, origin);
285
+ } catch {
286
+ /* the host went away */
287
+ }
288
+ }
289
+ }
290
+
291
+ function onMessage(event: MessageEvent): void {
292
+ const origin = options.hostOrigin();
293
+ const from = callers().find((w) => w === event.source);
294
+ if (!from || !origin || event.origin !== origin) return;
295
+ const data = plain(event.data);
296
+ if (!data || data["og"] !== PROBE_WIRE || data["v"] !== PROBE_WIRE_VERSION || data["kind"] !== "call") return;
297
+ const id = data["id"];
298
+ const params = plain(data["params"]);
299
+ if (typeof id !== "string" || typeof data["method"] !== "string" || !params) return;
300
+ const received = now();
301
+ answer(data["method"], params, received).then(
302
+ (result) => {
303
+ post({ kind: "result", id, result: result === undefined ? null : result }, from);
304
+ if (data["method"] === "hello") post({ kind: "hello", ...hello() }, from);
305
+ },
306
+ (error) => post({ kind: "error", id, code: error instanceof WireError ? error.code : "failed", message: String((error as Error)?.message ?? error) }, from),
307
+ );
308
+ }
309
+
310
+ /** Hello on load and whenever the told origin or the probe's availability changes; returns the stop function. */
311
+ function watch(intervalMs = 500): () => void {
312
+ let last: string | null = null;
313
+ const check = () => {
314
+ const origin = options.hostOrigin();
315
+ if (!callers().length || !origin) {
316
+ last = null;
317
+ return;
318
+ }
319
+ const h = hello();
320
+ const key = `${origin}|${h.protocol}|${h.substrate}`;
321
+ if (key === last) return;
322
+ last = key;
323
+ post({ kind: "hello", ...h });
324
+ };
325
+ check();
326
+ const timer = setInterval(check, intervalMs);
327
+ return () => clearInterval(timer);
328
+ }
329
+
330
+ return { onMessage, watch, hello, answer };
331
+ }
332
+
333
+ /**
334
+ * Whether the platform's Stage overlay is drawn in this very document: the page
335
+ * is not framed and was told its own origin as the Stage host. The og-probe wire
336
+ * then also answers this window, and the Player bridge (./player.ts) speaks
337
+ * Player protocol 2 to it.
338
+ */
339
+ export function stageInPage(w: Window & Record<string, unknown> = globalThis as unknown as Window & Record<string, unknown>): boolean {
340
+ try {
341
+ return w.parent === w && w[STAGE_HOST_GLOBAL] === w.location?.origin;
342
+ } catch {
343
+ return false;
344
+ }
345
+ }
346
+
347
+ /**
348
+ * The wire on this page: host = parent when embedded, else opener; origin =
349
+ * `window.__OG_STAGE_HOST__`. A top-level page told its own origin also answers
350
+ * itself: the Stage overlay drawn in this document posts to this window.
351
+ */
352
+ export function mountWire(options: Omit<WireOptions, "host" | "hostOrigin">): () => void {
353
+ const w = globalThis as unknown as Window & Record<string, unknown>;
354
+ const wire = createWire({
355
+ ...options,
356
+ host: () => (w.parent && w.parent !== w ? w.parent : (w.opener as Window | null)),
357
+ inPage: () => (stageInPage(w) ? w : null),
358
+ hostOrigin: () => {
359
+ const told = w[STAGE_HOST_GLOBAL];
360
+ return typeof told === "string" && told && told !== "*" ? told : null;
361
+ },
362
+ });
363
+ addEventListener("message", wire.onMessage);
364
+ const stop = wire.watch();
365
+ return () => {
366
+ stop();
367
+ removeEventListener("message", wire.onMessage);
368
+ };
369
+ }
package/src/e6.ts ADDED
@@ -0,0 +1,69 @@
1
+ // The E6 modules' namespaces (IDL `nav`, `ai`, `platform`, `i18n`): thin calls into the host's
2
+ // binding symbols, typed by the generated declarations through index.ts.
3
+
4
+ import type { HostAbi } from "./generated/abi.ts";
5
+ import { boundHost } from "./runtime.ts";
6
+
7
+ type W = { readonly id: number };
8
+
9
+ function abi(): HostAbi<Uint32Array, unknown> {
10
+ const h = boundHost();
11
+ if (!h) throw new Error("no engine host is loaded: start the game through @origingame/engine's web or node loader");
12
+ return h.abi;
13
+ }
14
+
15
+ const parse = <T>(text: string): T => JSON.parse(text) as T;
16
+ const json = (value: unknown): string | undefined => (value === undefined ? undefined : JSON.stringify(value));
17
+
18
+ export const nav = Object.freeze({
19
+ grid: (world: W, desc?: unknown) => parse(abi().og_nav_grid(world.id, json(desc))),
20
+ gridFromTilemap: (world: W, desc?: unknown) => parse(abi().og_nav_grid_from_tilemap(world.id, json(desc))),
21
+ setCost: (world: W, grid: number, x: number, y: number, cost: number) => parse(abi().og_nav_set_cost(world.id, grid, x, y, cost)),
22
+ cellAt: (world: W, grid: number, point: unknown) => parse(abi().og_nav_cell_at(world.id, grid, JSON.stringify(point))),
23
+ findPath: (world: W, desc?: unknown) => parse(abi().og_nav_find_path(world.id, json(desc))),
24
+ flowField: (world: W, desc?: unknown) => parse(abi().og_nav_flow_field(world.id, json(desc))),
25
+ setFlowTargets: (world: W, field: number, targets: unknown) => parse(abi().og_nav_set_flow_targets(world.id, field, JSON.stringify(targets))),
26
+ flowAt: (world: W, field: number, point: unknown) => parse(abi().og_nav_flow_at(world.id, field, JSON.stringify(point))),
27
+ load: (world: W, path: string) => parse(abi().og_nav_load(world.id, path)),
28
+ build: (world: W, desc?: unknown) => parse(abi().og_nav_build(world.id, json(desc))),
29
+ setObstacle: (world: W, mesh: number, id: number, obstacle?: unknown) => parse(abi().og_nav_set_obstacle(world.id, mesh, id, json(obstacle))),
30
+ nearest: (world: W, mesh: number, point: unknown, extent?: unknown) => parse(abi().og_nav_nearest(world.id, mesh, JSON.stringify(point), json(extent))),
31
+ pathOf: (world: W, entity: number) => parse(abi().og_nav_path_of(world.id, entity)),
32
+ });
33
+
34
+ export const ai = Object.freeze({
35
+ load: (world: W, path: string) => parse(abi().og_ai_load(world.id, path)),
36
+ parse: (world: W, text: string) => parse(abi().og_ai_parse(world.id, text)),
37
+ get: (world: W, entity: number, key: string) => parse(abi().og_ai_get(world.id, entity, key)),
38
+ set: (world: W, entity: number, key: string, value: unknown) => parse(abi().og_ai_set(world.id, entity, key, JSON.stringify(value))),
39
+ restart: (world: W, entity: number) => parse(abi().og_ai_restart(world.id, entity)),
40
+ noise: (world: W, desc?: unknown) => parse(abi().og_ai_noise(world.id, json(desc))),
41
+ describe: (world: W, entity: number) => parse(abi().og_ai_describe(world.id, entity)),
42
+ });
43
+
44
+ export const platform = Object.freeze({
45
+ storeGet: (world: W, key: string) => parse(abi().og_platform_store_get(world.id, key)),
46
+ storeSet: (world: W, desc?: unknown) => parse(abi().og_platform_store_set(world.id, json(desc))),
47
+ storeList: (world: W) => abi().og_platform_store_list(world.id),
48
+ leaderboardSubmit: (world: W, desc?: unknown) => parse(abi().og_platform_leaderboard_submit(world.id, json(desc))),
49
+ leaderboardTop: (world: W, desc?: unknown) => abi().og_platform_leaderboard_top(world.id, json(desc)),
50
+ leaderboardAround: (world: W, desc?: unknown) => abi().og_platform_leaderboard_around(world.id, json(desc)),
51
+ telemetry: (world: W, name: string, attrs?: unknown) => parse(abi().og_platform_telemetry(world.id, name, json(attrs))),
52
+ point: (world: W, desc?: unknown) => parse(abi().og_platform_point(world.id, json(desc))),
53
+ aiMark: (world: W, desc?: unknown) => parse(abi().og_platform_ai_mark(world.id, json(desc))),
54
+ loadSettings: (world: W) => abi().og_platform_load_settings(world.id),
55
+ saveSettings: (world: W, value: unknown) => parse(abi().og_platform_save_settings(world.id, JSON.stringify(value))),
56
+ saveSlot: (world: W, desc?: unknown) => parse(abi().og_platform_save_slot(world.id, json(desc))),
57
+ loadSlot: (world: W, desc?: unknown) => parse(abi().og_platform_load_slot(world.id, json(desc))),
58
+ result: (world: W, request: number) => parse(abi().og_platform_result(world.id, request)),
59
+ info: (world: W) => parse(abi().og_platform_info(world.id)),
60
+ now: (world: W) => abi().og_platform_now(world.id),
61
+ });
62
+
63
+ export const i18n = Object.freeze({
64
+ load: (world: W, path: string) => parse(abi().og_i18n_load(world.id, path)),
65
+ setLocale: (world: W, locale: string) => parse(abi().og_i18n_set_locale(world.id, locale)),
66
+ locale: (world: W) => parse(abi().og_i18n_locale(world.id)),
67
+ t: (world: W, key: string, args?: unknown) => abi().og_i18n_t(world.id, key, json(args)),
68
+ has: (world: W, key: string) => abi().og_i18n_has(world.id, key),
69
+ });
package/src/env.d.ts ADDED
@@ -0,0 +1,100 @@
1
+ // The few host globals the loaders touch, declared here so the runtime
2
+ // type-checks without @types/node or bun-types (game code never sees them).
3
+
4
+ declare module "node:module" {
5
+ export function createRequire(url: string): ((id: string) => unknown) & { resolve(id: string): string };
6
+ }
7
+
8
+ declare module "node:url" {
9
+ export function pathToFileURL(path: string): URL;
10
+ export function fileURLToPath(url: string | URL): string;
11
+ }
12
+
13
+ declare module "node:path" {
14
+ export function resolve(...parts: string[]): string;
15
+ export function dirname(path: string): string;
16
+ export function basename(path: string, suffix?: string): string;
17
+ export function join(...parts: string[]): string;
18
+ }
19
+
20
+ declare module "node:fs" {
21
+ export function existsSync(path: string): boolean;
22
+ export function readFileSync(path: string): Uint8Array;
23
+ export function readFileSync(path: string, encoding: string): string;
24
+ export function writeFileSync(path: string, data: string | Uint8Array): void;
25
+ export function rmSync(path: string, options?: { recursive?: boolean; force?: boolean }): void;
26
+ export function mkdirSync(path: string, options?: { recursive?: boolean }): void;
27
+ export function copyFileSync(from: string, to: string): void;
28
+ export function statSync(path: string): { mtimeMs: number; size: number; isDirectory(): boolean };
29
+ export function watch(path: string, listener: (event: string, file: string | null) => void): { close(): void };
30
+ }
31
+
32
+ declare module "node:os" {
33
+ export function networkInterfaces(): Record<string, Array<{ address: string; family: string | number; internal: boolean }> | undefined>;
34
+ export function tmpdir(): string;
35
+ }
36
+
37
+ declare module "node:crypto" {
38
+ export function createHash(algorithm: string): { update(data: Uint8Array | string): { digest(encoding: "hex"): string } };
39
+ }
40
+
41
+ declare module "node:zlib" {
42
+ export function inflateSync(data: Uint8Array): Uint8Array;
43
+ export function deflateSync(data: Uint8Array, options?: { level?: number }): Uint8Array;
44
+ export function brotliCompressSync(data: Uint8Array | string, options?: { params?: Record<number, number> }): Uint8Array;
45
+ export const constants: { BROTLI_PARAM_QUALITY: number; BROTLI_MAX_QUALITY: number; BROTLI_PARAM_SIZE_HINT: number };
46
+ }
47
+
48
+ declare module "bun:test" {
49
+ type Body = () => unknown | Promise<unknown>;
50
+ export function test(name: string, body: Body, timeout?: number): void;
51
+ export namespace test {
52
+ /** The test runs unless `condition` holds (then it is reported skipped). */
53
+ function skipIf(condition: boolean): (name: string, body: Body, timeout?: number) => void;
54
+ }
55
+ export function describe(name: string, body: () => void): void;
56
+ export function beforeAll(body: Body): void;
57
+ export function afterAll(body: Body): void;
58
+ interface Matchers {
59
+ toBe(value: unknown): void;
60
+ toEqual(value: unknown): void;
61
+ toBeTrue(): void;
62
+ toBeFalse(): void;
63
+ toBeGreaterThan(value: number): void;
64
+ toBeGreaterThanOrEqual(value: number): void;
65
+ toBeLessThanOrEqual(n: number): void;
66
+ toMatchObject(o: Record<string, unknown>): void;
67
+ toBeLessThan(value: number): void;
68
+ toBeCloseTo(value: number, digits?: number): void;
69
+ toBeNull(): void;
70
+ toBeDefined(): void;
71
+ toMatch(pattern: RegExp | string): void;
72
+ toContain(value: unknown): void;
73
+ toThrow(pattern?: RegExp | string): void;
74
+ not: Matchers;
75
+ }
76
+ export function expect(value: unknown): Matchers;
77
+ }
78
+
79
+ declare const process: {
80
+ readonly env: Record<string, string | undefined>;
81
+ readonly platform: string;
82
+ readonly arch: string;
83
+ readonly argv: string[];
84
+ readonly version: string;
85
+ readonly pid: number;
86
+ exit(code?: number): never;
87
+ readonly stdout: { write(text: string): boolean };
88
+ readonly stderr: { write(text: string): boolean };
89
+ };
90
+
91
+ declare const Bun: { readonly version: string } | undefined;
92
+
93
+ interface ImportMeta {
94
+ readonly dir?: string;
95
+ /** bun: whether this module is the process's entry. */
96
+ readonly main?: boolean;
97
+ }
98
+
99
+ declare function atob(data: string): string;
100
+ declare function btoa(data: string): string;