@origingame/engine 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/og.mjs +25 -0
- package/dist/web/origin-engine.d.ts +1233 -0
- package/dist/web/origin-engine.js +8628 -0
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +366 -0
- package/dist/web-publish/origin-engine-lanes.js +8509 -0
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +1176 -0
- package/dist/web-publish/origin-engine.js +8313 -0
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +347 -0
- package/dist/web-publish/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
- package/dist/web-publish-2d/origin-engine-lanes.js +8509 -0
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +1176 -0
- package/dist/web-publish-2d/origin-engine.js +8313 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +347 -0
- package/dist/web-publish-2d/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
- package/knowledge/README.md +31 -0
- package/knowledge/knowledge.json +13 -0
- package/knowledge/looks/catalogue.json +1871 -0
- package/knowledge/package.json +33 -0
- package/knowledge/rules/platform.json +329 -0
- package/knowledge/scaffold/README.md +27 -0
- package/knowledge/scaffold/distribution.json +48 -0
- package/knowledge/scaffold/origin-engine.json +19 -0
- package/knowledge/scaffold/template/.origin/setup +14 -0
- package/knowledge/scaffold/template/LICENSES.md +23 -0
- package/knowledge/scaffold/template/README.md +37 -0
- package/knowledge/scaffold/template/assets/ai/guard.ogt +48 -0
- package/knowledge/scaffold/template/assets/audio/click.ogt +9 -0
- package/knowledge/scaffold/template/assets/fonts/NotoSansSC-Regular-subset.ttf.ogt +12 -0
- package/knowledge/scaffold/template/assets/fonts/OFL.txt +93 -0
- package/knowledge/scaffold/template/assets/looks/origin-game.ogt +12 -0
- package/knowledge/scaffold/template/assets/nav/yard.ogt +27 -0
- package/knowledge/scaffold/template/assets/scenes/main.ogt +34 -0
- package/knowledge/scaffold/template/assets/strings/en.ogt +14 -0
- package/knowledge/scaffold/template/assets/textures/stone_n.png.ogt +7 -0
- package/knowledge/scaffold/template/gitattributes +22 -0
- package/knowledge/scaffold/template/gitignore +8 -0
- package/knowledge/scaffold/template/package.json +18 -0
- package/knowledge/scaffold/template/project.ogt +25 -0
- package/knowledge/scaffold/template/src/main.ts +246 -0
- package/knowledge/scaffold/template/tests/guard.og.ts +67 -0
- package/knowledge/scaffold/template/tests/smoke.og.ts +43 -0
- package/knowledge/scaffold/template/tsconfig.json +17 -0
- package/knowledge/scripts/lib.ts +207 -0
- package/knowledge/scripts/mount.ts +210 -0
- package/knowledge/scripts/pack.ts +25 -0
- package/knowledge/scripts/scaffold.ts +72 -0
- package/knowledge/scripts/validate.ts +749 -0
- package/knowledge/skills/og-ai/SKILL.md +386 -0
- package/knowledge/skills/og-audio/SKILL.md +191 -0
- package/knowledge/skills/og-build/SKILL.md +330 -0
- package/knowledge/skills/og-core/SKILL.md +285 -0
- package/knowledge/skills/og-feel/SKILL.md +385 -0
- package/knowledge/skills/og-game/SKILL.md +1479 -0
- package/knowledge/skills/og-i18n/SKILL.md +220 -0
- package/knowledge/skills/og-platform-sdk/SKILL.md +199 -0
- package/knowledge/skills/og-taste/SKILL.md +537 -0
- package/knowledge/skills/og-ui/SKILL.md +444 -0
- package/knowledge/wiki/ai.md +627 -0
- package/knowledge/wiki/anim.md +1187 -0
- package/knowledge/wiki/atmosphere.md +636 -0
- package/knowledge/wiki/audio.md +732 -0
- package/knowledge/wiki/camera.md +711 -0
- package/knowledge/wiki/commands.md +357 -0
- package/knowledge/wiki/game.md +220 -0
- package/knowledge/wiki/i18n.md +213 -0
- package/knowledge/wiki/index.md +1027 -0
- package/knowledge/wiki/input.md +1714 -0
- package/knowledge/wiki/load.md +1280 -0
- package/knowledge/wiki/math.md +166 -0
- package/knowledge/wiki/nav.md +1154 -0
- package/knowledge/wiki/physics.md +3142 -0
- package/knowledge/wiki/platform.md +857 -0
- package/knowledge/wiki/probe.md +1692 -0
- package/knowledge/wiki/reflect.md +285 -0
- package/knowledge/wiki/render.md +3511 -0
- package/knowledge/wiki/rng.md +393 -0
- package/knowledge/wiki/stream.md +376 -0
- package/knowledge/wiki/system.md +197 -0
- package/knowledge/wiki/systems.md +43 -0
- package/knowledge/wiki/terrain.md +727 -0
- package/knowledge/wiki/tilemap.md +422 -0
- package/knowledge/wiki/time.md +150 -0
- package/knowledge/wiki/transform.md +230 -0
- package/knowledge/wiki/tween.md +580 -0
- package/knowledge/wiki/ui.md +8000 -0
- package/knowledge/wiki/world.md +909 -0
- package/package.json +68 -4
- package/schema/api.schema.json +29675 -0
- package/shell/README.md +22 -0
- package/shell/brand/README.md +23 -0
- package/shell/brand/icon-192.png +0 -0
- package/shell/brand/icon-512.png +0 -0
- package/shell/brand/mark-on-paper.svg +6 -0
- package/shell/brand/origin-mark.svg +5 -0
- package/shell/brand/origin-motion.css +251 -0
- package/shell/brand/tokens.css +20 -0
- package/shell/brand.ts +132 -0
- package/shell/page.ts +715 -0
- package/shell/platform.ts +203 -0
- package/shell/player.ts +238 -0
- package/shell/shell.css +185 -0
- package/shell/sw.ts +177 -0
- package/shell/transport.ts +515 -0
- package/shell/wire.ts +369 -0
- package/src/e6.ts +69 -0
- package/src/env.d.ts +100 -0
- package/src/generated/abi.ts +980 -0
- package/src/index.ts +193 -0
- package/src/node/audio.ts +86 -0
- package/src/node/build.ts +109 -0
- package/src/node/device.ts +981 -0
- package/src/node/doctor.ts +60 -0
- package/src/node/index.ts +102 -0
- package/src/node/platform.ts +24 -0
- package/src/node/playtest-run.ts +249 -0
- package/src/node/png.ts +130 -0
- package/src/node/polish.ts +976 -0
- package/src/node/run.ts +373 -0
- package/src/node/web-build.ts +808 -0
- package/src/p2.ts +35 -0
- package/src/platform-local.ts +211 -0
- package/src/playtest.ts +131 -0
- package/src/runtime.ts +1509 -0
- package/src/systems/cameras.ts +247 -0
- package/src/ui-kit/bind.ts +141 -0
- package/src/ui-kit/huds.ts +303 -0
- package/src/ui-kit/index.ts +36 -0
- package/src/ui-kit/menus.ts +187 -0
- package/src/ui-kit/settings.ts +131 -0
- package/src/ui-kit/store.ts +222 -0
- package/src/ui-kit/story.ts +89 -0
- package/src/ui-kit/util.ts +106 -0
- package/src/web/a11y.ts +429 -0
- package/src/web/audio-worklet.ts +193 -0
- package/src/web/audio.ts +307 -0
- package/src/web/basis.ts +239 -0
- package/src/web/gamepad.ts +123 -0
- package/src/web/host.ts +166 -0
- package/src/web/input.ts +237 -0
- package/src/web/lanes.ts +21 -0
- package/src/web/play.ts +20 -0
- package/src/web/rpc-ring.ts +110 -0
- package/src/web/settle.ts +122 -0
- package/src/web/shell.ts +643 -0
- package/src/web/text-input.ts +248 -0
- package/src/web/worker.ts +1002 -0
- package/types/bindings.json +13654 -0
- package/types/index.d.ts +20090 -0
- package/vendor/basis/LICENSE +201 -0
- package/vendor/basis/NOTICE +15 -0
- package/vendor/basis/README.md +18 -0
- package/vendor/basis/basis_transcoder.js +19 -0
- package/vendor/basis/basis_transcoder.wasm +0 -0
- package/README.md +0 -3
package/src/node/run.ts
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// `og run` on the native host: load a game bundle under bun (or node) with
|
|
2
|
+
// the napi addon, open the project's cooked bundle, mount the probe, and
|
|
3
|
+
// drive the tick.
|
|
4
|
+
//
|
|
5
|
+
// bun src/node/run.ts --entry <game.js|.ts> [--root <project>] [--ticks N] [--seed S]
|
|
6
|
+
// [--setup action[:arg...]]... [--commands "t:action[:args];..."] [--out <dir>]
|
|
7
|
+
// [--port P] [--size WxH] [--realtime] [--watch <bundle.js>] [--no-render]
|
|
8
|
+
// [--audio null|device|<out.wav>] [--platform-store <file.json>] [--clock <UTC ms>] [--locale <tag>] [--json]
|
|
9
|
+
//
|
|
10
|
+
// - Without --commands, --port or --watch: advance --ticks N at once and print
|
|
11
|
+
// the world hash (M2's hello world).
|
|
12
|
+
// - With --commands: the script is scheduled through og-probe at tick 0 and
|
|
13
|
+
// the run takes one tick per frame until `quit`, --ticks, or the script's
|
|
14
|
+
// last step has run. Outputs (`screenshot`, `dump_*`, `help`) are written
|
|
15
|
+
// under --out at the probe's `Output::file_name()`.
|
|
16
|
+
// - With --port: the probe answers JSON-RPC 2.0 at ws://127.0.0.1:<port>/probe
|
|
17
|
+
// between frames (`og probe`); the run keeps going until `quit` or a signal.
|
|
18
|
+
// - With --watch: a change to the bundle re-imports it and hot-reloads it
|
|
19
|
+
// through the probe's `reload` (snapshot, swap, restore), printing one JSON
|
|
20
|
+
// line per reload with the latency from the file write to the first frame
|
|
21
|
+
// of the new bundle.
|
|
22
|
+
// - Without a renderer (no usable adapter, as in the platform's engine and
|
|
23
|
+
// probe sandboxes, or OG_RENDERER=none) the run is the same run: the play
|
|
24
|
+
// surface keeps --size, a `screenshot` writes no PNG and is listed in
|
|
25
|
+
// --json's `unchecked` ("no renderer, frame unchecked"), the tier is
|
|
26
|
+
// `none`, and the exit status does not change.
|
|
27
|
+
|
|
28
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
29
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
30
|
+
import { createHash } from "node:crypto";
|
|
31
|
+
import { pathToFileURL } from "node:url";
|
|
32
|
+
import { loadNativeHost, native } from "./index.ts";
|
|
33
|
+
import { openAudio, type AudioSink } from "./audio.ts";
|
|
34
|
+
import { nativePlatform } from "./platform.ts";
|
|
35
|
+
import { beginBundle, createEngine, fixedClock, type Engine, type GameToken } from "../runtime.ts";
|
|
36
|
+
|
|
37
|
+
const argv = process.argv.slice(2);
|
|
38
|
+
let entry = "";
|
|
39
|
+
let root: string | undefined;
|
|
40
|
+
let ticks: number | undefined;
|
|
41
|
+
let seed: number | undefined;
|
|
42
|
+
let commands: string | undefined;
|
|
43
|
+
let out = "og-out";
|
|
44
|
+
let port: number | undefined;
|
|
45
|
+
let size = { width: 1280, height: 720 };
|
|
46
|
+
let realtime = false;
|
|
47
|
+
let watchFile: string | undefined;
|
|
48
|
+
let render = true;
|
|
49
|
+
let asJson = false;
|
|
50
|
+
let audioOut = "null";
|
|
51
|
+
let platformStore: string | undefined;
|
|
52
|
+
let locale: string | undefined;
|
|
53
|
+
let clock: number | undefined;
|
|
54
|
+
const setup: string[] = [];
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const a = argv[i]!;
|
|
57
|
+
const next = () => {
|
|
58
|
+
const v = argv[++i];
|
|
59
|
+
if (v === undefined) fail(`${a} needs a value`);
|
|
60
|
+
return v!;
|
|
61
|
+
};
|
|
62
|
+
if (a === "--entry") entry = next();
|
|
63
|
+
else if (a === "--root") root = resolve(next());
|
|
64
|
+
else if (a === "--ticks") ticks = Number(next());
|
|
65
|
+
else if (a === "--seed") seed = Number(next());
|
|
66
|
+
else if (a === "--setup") setup.push(next());
|
|
67
|
+
else if (a === "--commands") commands = next();
|
|
68
|
+
else if (a === "--out") out = next();
|
|
69
|
+
else if (a === "--port") port = Number(next());
|
|
70
|
+
else if (a === "--size") {
|
|
71
|
+
const m = /^(\d+)x(\d+)$/.exec(next());
|
|
72
|
+
if (!m) fail("--size takes WIDTHxHEIGHT, such as 1280x720");
|
|
73
|
+
size = { width: Number(m![1]), height: Number(m![2]) };
|
|
74
|
+
} else if (a === "--realtime") realtime = true;
|
|
75
|
+
else if (a === "--watch") watchFile = resolve(next());
|
|
76
|
+
else if (a === "--no-render") render = false;
|
|
77
|
+
else if (a === "--json") asJson = true;
|
|
78
|
+
else if (a === "--audio") audioOut = next();
|
|
79
|
+
else if (a === "--platform-store") platformStore = resolve(next());
|
|
80
|
+
else if (a === "--locale") locale = next();
|
|
81
|
+
else if (a === "--clock") clock = Number(next());
|
|
82
|
+
else if (a === "--headless") {
|
|
83
|
+
// Native runs are always headless; accepted for `og run --headless`.
|
|
84
|
+
} else fail(`unknown argument '${a}'`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fail(reason: string): never {
|
|
88
|
+
process.stderr.write(`og run: ${reason}\n`);
|
|
89
|
+
process.exit(2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!entry) fail("no game entry: pass --entry <file>");
|
|
93
|
+
if (ticks !== undefined && (!Number.isInteger(ticks) || ticks < 0)) fail("--ticks takes a whole number of fixed steps");
|
|
94
|
+
if (seed !== undefined && (!Number.isInteger(seed) || seed < 0)) fail("--seed takes a whole number");
|
|
95
|
+
if (clock !== undefined && !(Number.isFinite(clock) && clock > 0)) fail("--clock takes UTC milliseconds since 1970, such as 1767225600000");
|
|
96
|
+
if (port !== undefined && (!Number.isInteger(port) || port < 0 || port > 65535)) fail("--port takes 0 (a free port) to 65535");
|
|
97
|
+
|
|
98
|
+
loadNativeHost();
|
|
99
|
+
const extras = native();
|
|
100
|
+
let version = 0;
|
|
101
|
+
/**
|
|
102
|
+
* Import a bundle as a fresh module. A re-import goes through a copy under
|
|
103
|
+
* its own name: bun keys file:// modules by path and ignores a query, so
|
|
104
|
+
* `game.js?v=2` would answer the module already evaluated, declaring nothing.
|
|
105
|
+
*/
|
|
106
|
+
async function importGame(file: string): Promise<GameToken> {
|
|
107
|
+
let path = resolve(file);
|
|
108
|
+
if (version > 0) {
|
|
109
|
+
const copy = path.replace(/(\.[cm]?[jt]s)?$/, (ext) => `.reload-${version}${ext || ".js"}`);
|
|
110
|
+
copyFileSync(path, copy);
|
|
111
|
+
path = copy;
|
|
112
|
+
}
|
|
113
|
+
const url = pathToFileURL(path).href;
|
|
114
|
+
version++;
|
|
115
|
+
const mod = (await import(url)) as { default?: GameToken };
|
|
116
|
+
if (path !== resolve(file)) rmSync(path, { force: true });
|
|
117
|
+
if (!mod.default) fail(`${file} has no default export: export default og.game({ ... })`);
|
|
118
|
+
return mod.default!;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** project.ogt's text, which the engine reads tick, seed, scenes and scene from (CreateOptions.project). */
|
|
122
|
+
function projectText(dir: string | undefined): string | undefined {
|
|
123
|
+
if (!dir || !existsSync(join(dir, "project.ogt"))) return undefined;
|
|
124
|
+
return readFileSync(join(dir, "project.ogt"), "utf8");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** project.ogt's `profile` ("2d" or "3d"), which chooses the physics profile. */
|
|
128
|
+
function projectProfile(text: string | undefined): "2d" | "3d" | undefined {
|
|
129
|
+
const m = text === undefined ? null : /^\s*profile\s+"(2d|3d)"/m.exec(text);
|
|
130
|
+
return m ? (m[1] as "2d" | "3d") : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const game = await importGame(entry);
|
|
134
|
+
const project = projectText(root);
|
|
135
|
+
const profile = projectProfile(project);
|
|
136
|
+
const probing = commands !== undefined || port !== undefined || watchFile !== undefined;
|
|
137
|
+
// With a renderer, GameDesc.start runs once it is attached (as the web Worker
|
|
138
|
+
// does), so og.render.capabilities reports the play surface's size there.
|
|
139
|
+
const deferStart = probing && render;
|
|
140
|
+
const created = createEngine(game, { ...(seed === undefined ? {} : { seed }), ...(root ? { root } : {}), ...(project === undefined ? {} : { project }), ...(profile ? { profile } : {}), ...(locale ? { locale } : {}), ...(deferStart ? { deferStart } : {}) });
|
|
141
|
+
if (!created.ok) fail(`${entry}: ${created.reason}`);
|
|
142
|
+
const engine: Engine = created.value;
|
|
143
|
+
// og.platform: the local store, in memory unless --platform-store names a file;
|
|
144
|
+
// its clock the system's, or with --clock one that starts there and moves with the ticks.
|
|
145
|
+
if (clock !== undefined) engine.setClock(fixedClock(engine, clock));
|
|
146
|
+
engine.attachPlatform(nativePlatform(platformStore));
|
|
147
|
+
// One sink per engine, pumped after every step (crates/og-audio/README.md
|
|
148
|
+
// §Host contract): null keeps the stream drained, a .wav path captures it.
|
|
149
|
+
const audioMode = audioOut === "device" ? "device" : audioOut === "null" ? "null" : "capture";
|
|
150
|
+
const opened = openAudio(engine.id, audioMode);
|
|
151
|
+
if (!opened.ok) fail(`--audio ${audioOut}: ${opened.reason}`);
|
|
152
|
+
const sink: AudioSink = opened.value;
|
|
153
|
+
const outputs: string[] = [];
|
|
154
|
+
const warnings: string[] = [];
|
|
155
|
+
// The screenshots taken without a renderer: the probe's FRAME_UNCHECKED notes.
|
|
156
|
+
const unchecked: string[] = [];
|
|
157
|
+
const UNCHECKED = /^[\d.]+s screenshot: no renderer, frame unchecked \((.+)\)$/;
|
|
158
|
+
|
|
159
|
+
function decode(base64: string): Uint8Array {
|
|
160
|
+
const bin = atob(base64);
|
|
161
|
+
const bytes = new Uint8Array(bin.length);
|
|
162
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
163
|
+
return bytes;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function drain(): void {
|
|
167
|
+
for (const o of engine.takeOutputs()) {
|
|
168
|
+
// A script names its output relative to --out; one that would land
|
|
169
|
+
// outside it (an absolute path or `..`) is refused, never written.
|
|
170
|
+
const path = resolve(out, o.file);
|
|
171
|
+
const inside = relative(resolve(out), path);
|
|
172
|
+
if (inside === "" || inside === ".." || inside.startsWith(".." + sep) || isAbsolute(inside)) {
|
|
173
|
+
const line = `WARN tick ${o.tick} ${o.action}: output path '${o.file}' leaves --out ${out}; not written`;
|
|
174
|
+
warnings.push(line);
|
|
175
|
+
process.stderr.write(line + "\n");
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
179
|
+
writeFileSync(path, o.kind === "png" || o.kind === "wav" ? decode(o.data) : o.data);
|
|
180
|
+
outputs.push(path);
|
|
181
|
+
if (!asJson) process.stdout.write(`tick ${o.tick} ${o.action}: wrote ${path}\n`);
|
|
182
|
+
}
|
|
183
|
+
for (const l of engine.takeLog()) {
|
|
184
|
+
const line = `${l.warning ? "WARN" : "INFO"} tick ${l.tick}: ${l.text}`;
|
|
185
|
+
if (l.warning) warnings.push(line);
|
|
186
|
+
else {
|
|
187
|
+
const frame = UNCHECKED.exec(l.text);
|
|
188
|
+
if (frame) unchecked.push(frame[1]!);
|
|
189
|
+
}
|
|
190
|
+
process.stderr.write(line + "\n");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let tier = "none";
|
|
195
|
+
if (deferStart) {
|
|
196
|
+
const attached = JSON.parse(extras.og_node_attach_offscreen(engine.id, size.width, size.height)) as { ok: boolean; value?: string; reason?: string };
|
|
197
|
+
if (attached.ok) tier = attached.value!;
|
|
198
|
+
else process.stderr.write(`INFO no renderer (${attached.reason}): the run is the same, a screenshot writes no frame and reports it unchecked, capture answers null\n`);
|
|
199
|
+
// The first tick boundary: GameDesc.start, with the surface in place.
|
|
200
|
+
const started = engine.advance(0);
|
|
201
|
+
if (!started.ok) fail(started.reason);
|
|
202
|
+
}
|
|
203
|
+
drain();
|
|
204
|
+
|
|
205
|
+
for (const step of setup) {
|
|
206
|
+
const [action, ...args] = step.split(":");
|
|
207
|
+
const r = engine.command(action!, args);
|
|
208
|
+
if (!r.ok) fail(`--setup ${step}: ${r.reason}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let listening: number | undefined;
|
|
212
|
+
if (port !== undefined) {
|
|
213
|
+
const r = JSON.parse(extras.og_node_probe_listen(engine.id, port)) as { ok: boolean; value?: number; reason?: string };
|
|
214
|
+
if (!r.ok) fail(r.reason!);
|
|
215
|
+
listening = r.value!;
|
|
216
|
+
process.stdout.write(`probe: ws://127.0.0.1:${listening}/probe\n`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let untilTick = -1;
|
|
220
|
+
if (commands !== undefined) {
|
|
221
|
+
const report = engine.probe.commands(commands) as { scheduled: number; untilTick: number; warnings: string[] } | null;
|
|
222
|
+
drain();
|
|
223
|
+
if (!report) fail(`--commands: the script is malformed (the reason is above)`);
|
|
224
|
+
for (const w of report!.warnings) process.stderr.write(`WARN ${w}\n`);
|
|
225
|
+
untilTick = report!.untilTick;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const rate = engine.rate;
|
|
229
|
+
const tickMs = 1000 / rate;
|
|
230
|
+
// --json's frame times: each frame's tick and render milliseconds (the
|
|
231
|
+
// first five left out, as og polish does), and each system's time over the
|
|
232
|
+
// run from the world's dump (World::set_clock).
|
|
233
|
+
type SystemTimes = { name: string; totalMs?: number; runs?: number }[];
|
|
234
|
+
const systemTimes = (): SystemTimes => (engine.probe.dump({ what: "world" }) as { systems?: SystemTimes }).systems ?? [];
|
|
235
|
+
const systemsBefore = asJson ? systemTimes() : [];
|
|
236
|
+
const stepTimes: number[] = [];
|
|
237
|
+
const renderTimes: number[] = [];
|
|
238
|
+
let framesSeen = 0;
|
|
239
|
+
const t0 = performance.now();
|
|
240
|
+
let frameFailed: string | null = null;
|
|
241
|
+
let quit = false;
|
|
242
|
+
|
|
243
|
+
if (!probing) {
|
|
244
|
+
const advanced = engine.advance(ticks ?? 0);
|
|
245
|
+
if (!advanced.ok) fail(advanced.reason);
|
|
246
|
+
sink.pump();
|
|
247
|
+
} else {
|
|
248
|
+
let pendingReload: { mtimeMs: number } | null = null;
|
|
249
|
+
// Imports in flight: a reload swaps only once the last one has declared
|
|
250
|
+
// everything (a second write of the bundle starts another import, whose
|
|
251
|
+
// beginBundle clears the declarations the first one made).
|
|
252
|
+
let importing = 0;
|
|
253
|
+
if (watchFile) {
|
|
254
|
+
let busy = false;
|
|
255
|
+
watch(watchFile, () => {
|
|
256
|
+
if (busy) return;
|
|
257
|
+
busy = true;
|
|
258
|
+
setTimeout(async () => {
|
|
259
|
+
busy = false;
|
|
260
|
+
const mtimeMs = statSync(watchFile!).mtimeMs;
|
|
261
|
+
importing++;
|
|
262
|
+
try {
|
|
263
|
+
beginBundle();
|
|
264
|
+
await importGame(watchFile!);
|
|
265
|
+
pendingReload = { mtimeMs };
|
|
266
|
+
} catch (e) {
|
|
267
|
+
process.stderr.write(`WARN reload: the bundle failed to import (${e instanceof Error ? e.message : String(e)}); the old one keeps running\n`);
|
|
268
|
+
} finally {
|
|
269
|
+
importing--;
|
|
270
|
+
}
|
|
271
|
+
}, 5);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
275
|
+
let last = performance.now();
|
|
276
|
+
for (;;) {
|
|
277
|
+
if (pendingReload && importing === 0) {
|
|
278
|
+
const { mtimeMs } = pendingReload as { mtimeMs: number };
|
|
279
|
+
pendingReload = null;
|
|
280
|
+
const report = engine.probe.reload() as { ms: number; tick: number } | null;
|
|
281
|
+
const first = engine.frame(0);
|
|
282
|
+
const captured = render && engine.probe.capture(0, 0) !== null;
|
|
283
|
+
const latencyMs = Date.now() - mtimeMs;
|
|
284
|
+
process.stdout.write(JSON.stringify({ event: "reload", ok: report !== null, report, tick: first.tick, captured, latencyMs: +latencyMs.toFixed(1) }) + "\n");
|
|
285
|
+
drain();
|
|
286
|
+
}
|
|
287
|
+
const elapsed = realtime ? performance.now() - last : tickMs;
|
|
288
|
+
last = performance.now();
|
|
289
|
+
const answer = engine.frame(elapsed);
|
|
290
|
+
if (!answer.ok) {
|
|
291
|
+
frameFailed = answer.reason ?? "the frame failed";
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
if (asJson && ++framesSeen > 5) {
|
|
295
|
+
if ((answer.ticks ?? 0) > 0) stepTimes.push(answer.stepMs ?? 0);
|
|
296
|
+
if (answer.rendered) renderTimes.push(answer.renderMs ?? 0);
|
|
297
|
+
}
|
|
298
|
+
sink.pump();
|
|
299
|
+
if (listening !== undefined) extras.og_node_probe_serve(engine.id);
|
|
300
|
+
drain();
|
|
301
|
+
const tick = answer.tick ?? 0;
|
|
302
|
+
if (answer.quit) {
|
|
303
|
+
quit = true;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
if (ticks !== undefined && tick >= ticks) break;
|
|
307
|
+
if (ticks === undefined && commands !== undefined && listening === undefined && !watchFile && tick > untilTick) break;
|
|
308
|
+
if (realtime || listening !== undefined || watchFile) await sleep(Math.max(0, tickMs - (performance.now() - last)));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const ms = performance.now() - t0;
|
|
312
|
+
drain();
|
|
313
|
+
let wavSha256: string | undefined;
|
|
314
|
+
if (audioMode === "capture") {
|
|
315
|
+
const wavPath = resolve(audioOut);
|
|
316
|
+
mkdirSync(dirname(wavPath), { recursive: true });
|
|
317
|
+
const wav = sink.wav();
|
|
318
|
+
writeFileSync(wavPath, wav);
|
|
319
|
+
wavSha256 = createHash("sha256").update(wav).digest("hex");
|
|
320
|
+
outputs.push(wavPath);
|
|
321
|
+
}
|
|
322
|
+
const audioStats = sink.stats();
|
|
323
|
+
sink.close();
|
|
324
|
+
if (frameFailed) process.stderr.write(`og run: ${frameFailed}\n`);
|
|
325
|
+
const dump = engine.probe.dump({ what: "world" }) as unknown as { hash: string; entities: number; tick: number; systems?: SystemTimes };
|
|
326
|
+
// og polish's order statistics (src/node/polish.ts), so the two agree.
|
|
327
|
+
const percentile = (xs: number[], q: number): number => {
|
|
328
|
+
if (xs.length === 0) return 0;
|
|
329
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
330
|
+
return s[Math.min(s.length - 1, Math.floor(q * s.length))]!;
|
|
331
|
+
};
|
|
332
|
+
const r3 = (x: number) => +x.toFixed(3);
|
|
333
|
+
const frames = {
|
|
334
|
+
stepMedianMs: r3(percentile(stepTimes, 0.5)),
|
|
335
|
+
stepP95Ms: r3(percentile(stepTimes, 0.95)),
|
|
336
|
+
stepMaxMs: r3(Math.max(0, ...stepTimes)),
|
|
337
|
+
renderMedianMs: r3(percentile(renderTimes, 0.5)),
|
|
338
|
+
renderP95Ms: r3(percentile(renderTimes, 0.95)),
|
|
339
|
+
renderMaxMs: r3(Math.max(0, ...renderTimes)),
|
|
340
|
+
stepped: stepTimes.length,
|
|
341
|
+
rendered: renderTimes.length,
|
|
342
|
+
};
|
|
343
|
+
// Each system's mean milliseconds a run over this run, slowest first.
|
|
344
|
+
const systems = (dump.systems ?? [])
|
|
345
|
+
.map((s) => {
|
|
346
|
+
const before = systemsBefore.find((b) => b.name === s.name);
|
|
347
|
+
const runs = (s.runs ?? 0) - (before?.runs ?? 0);
|
|
348
|
+
const ms = (s.totalMs ?? 0) - (before?.totalMs ?? 0);
|
|
349
|
+
return { name: s.name, meanMs: runs > 0 ? r3(ms / runs) : 0, totalMs: r3(ms), runs };
|
|
350
|
+
})
|
|
351
|
+
.filter((s) => s.runs > 0)
|
|
352
|
+
.sort((a, b) => b.totalMs - a.totalMs);
|
|
353
|
+
const summary = {
|
|
354
|
+
entry,
|
|
355
|
+
ticks: ticks ?? dump.tick,
|
|
356
|
+
tick: dump.tick,
|
|
357
|
+
hash: dump.hash,
|
|
358
|
+
entities: dump.entities,
|
|
359
|
+
advanceMs: +ms.toFixed(2),
|
|
360
|
+
frames,
|
|
361
|
+
systems,
|
|
362
|
+
runtime: typeof Bun !== "undefined" ? `bun ${Bun.version}` : `node ${process.version}`,
|
|
363
|
+
tier,
|
|
364
|
+
quit,
|
|
365
|
+
unchecked,
|
|
366
|
+
audio: { mode: audioMode, commands: audioStats.commands, receivedHash: audioStats.receivedHash, wavSha256 },
|
|
367
|
+
outputs,
|
|
368
|
+
warnings,
|
|
369
|
+
};
|
|
370
|
+
if (asJson) process.stdout.write(JSON.stringify(summary) + "\n");
|
|
371
|
+
else process.stdout.write(`tick ${dump.tick} hash ${dump.hash} entities ${dump.entities}\n`);
|
|
372
|
+
if (listening !== undefined) extras.og_node_probe_close(engine.id);
|
|
373
|
+
process.exit(frameFailed ? 1 : 0);
|