@origingame/engine 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/og.mjs +1 -1
- package/dist/web/origin-engine.d.ts +255 -5
- package/dist/web/origin-engine.js +1584 -125
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +80 -3
- package/dist/web-publish/origin-engine-lanes.js +1288 -82
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +199 -6
- package/dist/web-publish/origin-engine.js +1285 -79
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +65 -4
- package/dist/web-publish-2d/origin-engine-lanes.js +1285 -79
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +198 -5
- package/dist/web-publish-2d/origin-engine.js +1282 -76
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +64 -3
- package/knowledge/README.md +2 -1
- package/knowledge/guides/three-to-og.md +393 -0
- package/knowledge/knowledge.json +2 -1
- package/knowledge/looks/catalogue.json +40 -40
- package/knowledge/package.json +3 -2
- package/knowledge/scaffold/distribution.json +14 -0
- package/knowledge/scaffold/origin-engine.json +2 -2
- package/knowledge/scaffold/template/README.md +2 -1
- package/knowledge/scaffold/template/assets/audio/sting.ogt +10 -0
- package/knowledge/scaffold/template/assets/timelines/intro.ogt +101 -0
- package/knowledge/scaffold/template/assets/timelines/narration.srt +11 -0
- package/knowledge/scaffold/template/host.d.ts +46 -0
- package/knowledge/scaffold/template/package.json +1 -1
- package/knowledge/scaffold/template/project.ogt +1 -1
- package/knowledge/scaffold/template/src/main.ts +1 -1
- package/knowledge/scaffold/template/tests/frames.ts +134 -0
- package/knowledge/scaffold/template/tests/intro.og.ts +65 -0
- package/knowledge/scaffold/template/tests/smoke.og.ts +30 -36
- package/knowledge/scaffold/template/tsconfig.json +1 -1
- package/knowledge/scripts/mount.ts +2 -1
- package/knowledge/scripts/validate.ts +159 -31
- package/knowledge/skills/og-audio/SKILL.md +2 -2
- package/knowledge/skills/og-build/SKILL.md +29 -7
- package/knowledge/skills/og-core/SKILL.md +31 -5
- package/knowledge/skills/og-feel/SKILL.md +2 -2
- package/knowledge/skills/og-game/SKILL.md +1331 -16
- package/knowledge/skills/og-i18n/SKILL.md +7 -7
- package/knowledge/skills/og-model/SKILL.md +374 -0
- package/knowledge/skills/og-platform-sdk/SKILL.md +2 -1
- package/knowledge/skills/og-taste/SKILL.md +82 -73
- package/knowledge/skills/og-ui/SKILL.md +80 -58
- package/knowledge/wiki/ai.md +20 -20
- package/knowledge/wiki/anim.md +392 -41
- package/knowledge/wiki/atmosphere.md +36 -13
- package/knowledge/wiki/audio.md +18 -18
- package/knowledge/wiki/camera.md +19 -13
- package/knowledge/wiki/commands.md +19 -13
- package/knowledge/wiki/debug.md +414 -0
- package/knowledge/wiki/game.md +8 -8
- package/knowledge/wiki/i18n.md +7 -7
- package/knowledge/wiki/index.md +317 -8
- package/knowledge/wiki/input.md +47 -47
- package/knowledge/wiki/load.md +238 -70
- package/knowledge/wiki/math.md +2607 -0
- package/knowledge/wiki/nav.md +34 -34
- package/knowledge/wiki/net.md +340 -0
- package/knowledge/wiki/path.md +402 -0
- package/knowledge/wiki/physics.md +95 -95
- package/knowledge/wiki/platform.md +463 -29
- package/knowledge/wiki/probe.md +681 -67
- package/knowledge/wiki/reflect.md +29 -17
- package/knowledge/wiki/render.md +4993 -1998
- package/knowledge/wiki/rng.md +14 -14
- package/knowledge/wiki/shapes.md +727 -0
- package/knowledge/wiki/stream.md +15 -15
- package/knowledge/wiki/system.md +8 -8
- package/knowledge/wiki/systems.md +2 -2
- package/knowledge/wiki/terrain.md +24 -20
- package/knowledge/wiki/tilemap.md +13 -13
- package/knowledge/wiki/time.md +4 -4
- package/knowledge/wiki/timeline.md +396 -0
- package/knowledge/wiki/transform.md +8 -8
- package/knowledge/wiki/tween.md +17 -17
- package/knowledge/wiki/ui.md +560 -654
- package/knowledge/wiki/video.md +157 -0
- package/knowledge/wiki/world.md +36 -36
- package/package.json +4 -3
- package/schema/api.schema.json +28151 -19640
- package/shell/page.ts +26 -2
- package/shell/platform.ts +51 -0
- package/shell/player.ts +15 -2
- package/shell/transport.ts +6 -0
- package/src/depth.ts +71 -0
- package/src/e6.ts +32 -1
- package/src/env.d.ts +5 -1
- package/src/generated/abi.ts +226 -4
- package/src/geom.ts +260 -0
- package/src/index.ts +32 -4
- package/src/light.ts +18 -0
- package/src/math.ts +622 -0
- package/src/node/index.ts +17 -0
- package/src/node/playtest-run.ts +115 -18
- package/src/node/polish.ts +13 -3
- package/src/node/render.ts +291 -0
- package/src/node/run.ts +86 -5
- package/src/p2.ts +19 -1
- package/src/platform-local.ts +2 -0
- package/src/playtest.ts +65 -4
- package/src/runtime.ts +100 -9
- package/src/targets.ts +28 -0
- package/src/trace.ts +169 -0
- package/src/ui-kit/huds.ts +34 -34
- package/src/ui-kit/menus.ts +12 -12
- package/src/ui-kit/settings.ts +11 -2
- package/src/ui-kit/store.ts +18 -18
- package/src/ui-kit/story.ts +6 -4
- package/src/ui-kit/util.ts +8 -8
- package/src/web/shell.ts +59 -1
- package/src/web/worker.ts +105 -4
- package/types/bindings.json +9368 -6619
- package/types/index.d.ts +16377 -11520
package/src/node/run.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// bun src/node/run.ts --entry <game.js|.ts> [--root <project>] [--ticks N] [--seed S]
|
|
6
6
|
// [--setup action[:arg...]]... [--commands "t:action[:args];..."] [--out <dir>]
|
|
7
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>]
|
|
8
|
+
// [--audio null|device|<out.wav>] [--platform-store <file.json>] [--clock <UTC ms>] [--locale <tag>]
|
|
9
|
+
// [--trace <out.json> [--trace-append]] [--room ws://127.0.0.1:P/<room>] [--json]
|
|
9
10
|
//
|
|
10
11
|
// - Without --commands, --port or --watch: advance --ticks N at once and print
|
|
11
12
|
// the world hash (M2's hello world).
|
|
@@ -19,11 +20,21 @@
|
|
|
19
20
|
// through the probe's `reload` (snapshot, swap, restore), printing one JSON
|
|
20
21
|
// line per reload with the latency from the file write to the first frame
|
|
21
22
|
// of the new bundle.
|
|
23
|
+
// - With --room: the engine joins that room through `og relay` (og.platform's
|
|
24
|
+
// rooms, og.net's lockstep), the link pumped around every frame, and the run
|
|
25
|
+
// takes frames (never one big advance), so a tick lockstep holds waits for
|
|
26
|
+
// the other players' inputs; it ends at `quit`, --ticks or the script's end.
|
|
22
27
|
// - Without a renderer (no usable adapter, as in the platform's engine and
|
|
23
28
|
// probe sandboxes, or OG_RENDERER=none) the run is the same run: the play
|
|
24
29
|
// surface keeps --size, a `screenshot` writes no PNG and is listed in
|
|
25
30
|
// --json's `unchecked` ("no renderer, frame unchecked"), the tier is
|
|
26
31
|
// `none`, and the exit status does not change.
|
|
32
|
+
// - With --trace: the run in the Chrome trace format (src/trace.ts and
|
|
33
|
+
// crates/og-runtime/src/trace.rs have the threads): each tick with its
|
|
34
|
+
// systems, each frame with its render passes' CPU encoding and their GPU
|
|
35
|
+
// time, each load, og-lanes' jobs on their pool threads, every span where
|
|
36
|
+
// it ran; --trace-append adds them to the file og wrote first (its
|
|
37
|
+
// metadata, its origin and the cook's spans).
|
|
27
38
|
|
|
28
39
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
29
40
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -32,7 +43,8 @@ import { pathToFileURL } from "node:url";
|
|
|
32
43
|
import { loadNativeHost, native } from "./index.ts";
|
|
33
44
|
import { openAudio, type AudioSink } from "./audio.ts";
|
|
34
45
|
import { nativePlatform } from "./platform.ts";
|
|
35
|
-
import {
|
|
46
|
+
import { Trace, unixMs, type TraceDocument } from "../trace.ts";
|
|
47
|
+
import { beginBundle, boundHost, createEngine, fixedClock, type Engine, type GameToken } from "../runtime.ts";
|
|
36
48
|
|
|
37
49
|
const argv = process.argv.slice(2);
|
|
38
50
|
let entry = "";
|
|
@@ -51,6 +63,9 @@ let audioOut = "null";
|
|
|
51
63
|
let platformStore: string | undefined;
|
|
52
64
|
let locale: string | undefined;
|
|
53
65
|
let clock: number | undefined;
|
|
66
|
+
let tracePath: string | undefined;
|
|
67
|
+
let traceAppend = false;
|
|
68
|
+
let room: string | undefined;
|
|
54
69
|
const setup: string[] = [];
|
|
55
70
|
for (let i = 0; i < argv.length; i++) {
|
|
56
71
|
const a = argv[i]!;
|
|
@@ -79,6 +94,9 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
79
94
|
else if (a === "--platform-store") platformStore = resolve(next());
|
|
80
95
|
else if (a === "--locale") locale = next();
|
|
81
96
|
else if (a === "--clock") clock = Number(next());
|
|
97
|
+
else if (a === "--trace") tracePath = resolve(next());
|
|
98
|
+
else if (a === "--trace-append") traceAppend = true;
|
|
99
|
+
else if (a === "--room") room = next();
|
|
82
100
|
else if (a === "--headless") {
|
|
83
101
|
// Native runs are always headless; accepted for `og run --headless`.
|
|
84
102
|
} else fail(`unknown argument '${a}'`);
|
|
@@ -97,6 +115,17 @@ if (port !== undefined && (!Number.isInteger(port) || port < 0 || port > 65535))
|
|
|
97
115
|
|
|
98
116
|
loadNativeHost();
|
|
99
117
|
const extras = native();
|
|
118
|
+
|
|
119
|
+
// --trace: the Chrome trace (src/trace.ts), continuing og's prelude (the cook) when appending.
|
|
120
|
+
const trace = tracePath
|
|
121
|
+
? new Trace({ producer: "og run --trace", process: "og run", ...(traceAppend && existsSync(tracePath) ? { prior: JSON.parse(readFileSync(tracePath, "utf8")) as Partial<TraceDocument> } : {}) })
|
|
122
|
+
: undefined;
|
|
123
|
+
/** Takes the engine's spans (each where it ran) onto the trace; `last` waits for the GPU's frames in flight. */
|
|
124
|
+
function takeTrace(engineId: number, last = false): void {
|
|
125
|
+
if (!trace) return;
|
|
126
|
+
const taken = extras.og_node_trace_take(engineId, last);
|
|
127
|
+
trace.append(taken, unixMs());
|
|
128
|
+
}
|
|
100
129
|
let version = 0;
|
|
101
130
|
/**
|
|
102
131
|
* Import a bundle as a fresh module. A re-import goes through a copy under
|
|
@@ -131,15 +160,22 @@ function projectProfile(text: string | undefined): "2d" | "3d" | undefined {
|
|
|
131
160
|
}
|
|
132
161
|
|
|
133
162
|
const game = await importGame(entry);
|
|
163
|
+
// Loads made while the engine is created (its boot scene's) are traced too.
|
|
164
|
+
const BEFORE_ENGINE = 0xffffffff;
|
|
165
|
+
if (tracePath) extras.og_node_trace(BEFORE_ENGINE, true);
|
|
134
166
|
const project = projectText(root);
|
|
135
167
|
const profile = projectProfile(project);
|
|
136
|
-
const probing = commands !== undefined || port !== undefined || watchFile !== undefined;
|
|
168
|
+
const probing = commands !== undefined || port !== undefined || watchFile !== undefined || room !== undefined;
|
|
137
169
|
// With a renderer, GameDesc.start runs once it is attached (as the web Worker
|
|
138
170
|
// does), so og.render.capabilities reports the play surface's size there.
|
|
139
171
|
const deferStart = probing && render;
|
|
140
172
|
const created = createEngine(game, { ...(seed === undefined ? {} : { seed }), ...(root ? { root } : {}), ...(project === undefined ? {} : { project }), ...(profile ? { profile } : {}), ...(locale ? { locale } : {}), ...(deferStart ? { deferStart } : {}) });
|
|
141
173
|
if (!created.ok) fail(`${entry}: ${created.reason}`);
|
|
142
174
|
const engine: Engine = created.value;
|
|
175
|
+
if (tracePath) {
|
|
176
|
+
extras.og_node_trace(engine.id, true);
|
|
177
|
+
extras.og_node_trace(BEFORE_ENGINE, false);
|
|
178
|
+
}
|
|
143
179
|
// og.platform: the local store, in memory unless --platform-store names a file;
|
|
144
180
|
// its clock the system's, or with --clock one that starts there and moves with the ticks.
|
|
145
181
|
if (clock !== undefined) engine.setClock(fixedClock(engine, clock));
|
|
@@ -216,6 +252,23 @@ if (port !== undefined) {
|
|
|
216
252
|
process.stdout.write(`probe: ws://127.0.0.1:${listening}/probe\n`);
|
|
217
253
|
}
|
|
218
254
|
|
|
255
|
+
if (room !== undefined) {
|
|
256
|
+
const linked = JSON.parse(extras.og_node_room_connect(engine.id, room)) as { ok: boolean; reason?: string };
|
|
257
|
+
if (!linked.ok) fail(`--room ${room}: ${linked.reason}`);
|
|
258
|
+
// The relay joins the room the URL names: wait for its answer before the first tick.
|
|
259
|
+
for (let i = 0; i < 2000 && (JSON.parse(abiRoomInfo()) as { state: string }).state !== "in"; i++) {
|
|
260
|
+
extras.og_node_room_pump(engine.id);
|
|
261
|
+
await new Promise((r) => setTimeout(r, 1));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function abiRoomInfo(): string {
|
|
266
|
+
return boundHost()!.abi.og_platform_room_info(engine.id);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
type NetStatus = { running: boolean; startTick: number | null; waits: number; desync: number | null };
|
|
270
|
+
const netStatus = (): NetStatus => JSON.parse(boundHost()!.abi.og_net_status(engine.id)) as NetStatus;
|
|
271
|
+
|
|
219
272
|
let untilTick = -1;
|
|
220
273
|
if (commands !== undefined) {
|
|
221
274
|
const report = engine.probe.commands(commands) as { scheduled: number; untilTick: number; warnings: string[] } | null;
|
|
@@ -286,7 +339,9 @@ if (!probing) {
|
|
|
286
339
|
}
|
|
287
340
|
const elapsed = realtime ? performance.now() - last : tickMs;
|
|
288
341
|
last = performance.now();
|
|
342
|
+
if (room !== undefined) extras.og_node_room_pump(engine.id);
|
|
289
343
|
const answer = engine.frame(elapsed);
|
|
344
|
+
if (room !== undefined) extras.og_node_room_pump(engine.id);
|
|
290
345
|
if (!answer.ok) {
|
|
291
346
|
frameFailed = answer.reason ?? "the frame failed";
|
|
292
347
|
break;
|
|
@@ -297,6 +352,7 @@ if (!probing) {
|
|
|
297
352
|
}
|
|
298
353
|
sink.pump();
|
|
299
354
|
if (listening !== undefined) extras.og_node_probe_serve(engine.id);
|
|
355
|
+
takeTrace(engine.id);
|
|
300
356
|
drain();
|
|
301
357
|
const tick = answer.tick ?? 0;
|
|
302
358
|
if (answer.quit) {
|
|
@@ -310,10 +366,22 @@ if (!probing) {
|
|
|
310
366
|
const left = answer.pending ?? (tick > untilTick ? 0 : 1);
|
|
311
367
|
if (ticks === undefined && commands !== undefined && listening === undefined && !watchFile && left === 0) break;
|
|
312
368
|
if (realtime || listening !== undefined || watchFile) await sleep(Math.max(0, tickMs - (performance.now() - last)));
|
|
369
|
+
// In a room a frame that took no tick waits for the other players' inputs,
|
|
370
|
+
// and outside lockstep (its lobby, or a game that leaves it off) the run
|
|
371
|
+
// keeps the tick's pace with the other peers: an unpaced lobby would
|
|
372
|
+
// spend the run's --ticks before they arrive.
|
|
373
|
+
else if (room !== undefined && (answer.ticks ?? 0) === 0) await sleep(1);
|
|
374
|
+
else if (room !== undefined && !netStatus().running) await sleep(Math.max(0, tickMs - (performance.now() - last)));
|
|
313
375
|
}
|
|
314
376
|
}
|
|
315
377
|
const ms = performance.now() - t0;
|
|
316
378
|
drain();
|
|
379
|
+
if (tracePath) {
|
|
380
|
+
takeTrace(engine.id, true);
|
|
381
|
+
mkdirSync(dirname(tracePath), { recursive: true });
|
|
382
|
+
writeFileSync(tracePath, JSON.stringify(trace!.document()));
|
|
383
|
+
outputs.push(tracePath);
|
|
384
|
+
}
|
|
317
385
|
let wavSha256: string | undefined;
|
|
318
386
|
if (audioMode === "capture") {
|
|
319
387
|
const wavPath = resolve(audioOut);
|
|
@@ -327,6 +395,8 @@ const audioStats = sink.stats();
|
|
|
327
395
|
sink.close();
|
|
328
396
|
if (frameFailed) process.stderr.write(`og run: ${frameFailed}\n`);
|
|
329
397
|
const dump = engine.probe.dump({ what: "world" }) as unknown as { hash: string; entities: number; tick: number; systems?: SystemTimes };
|
|
398
|
+
// The script's expectations (`expect:` steps): a failure fails the run.
|
|
399
|
+
const expectations = engine.probe.expectations();
|
|
330
400
|
// og polish's order statistics (src/node/polish.ts), so the two agree.
|
|
331
401
|
const percentile = (xs: number[], q: number): number => {
|
|
332
402
|
if (xs.length === 0) return 0;
|
|
@@ -359,19 +429,30 @@ const summary = {
|
|
|
359
429
|
ticks: ticks ?? dump.tick,
|
|
360
430
|
tick: dump.tick,
|
|
361
431
|
hash: dump.hash,
|
|
432
|
+
// og.net's shared hash: the world without this peer's session, what lockstep peers compare.
|
|
433
|
+
netHash: JSON.parse(boundHost()!.abi.og_net_hash(engine.id)) as string,
|
|
434
|
+
// With --room: lockstep's state at the end (the tick it started, the waits, the first divergent tick).
|
|
435
|
+
...(room !== undefined ? { lockstep: netStatus() } : {}),
|
|
362
436
|
entities: dump.entities,
|
|
363
437
|
advanceMs: +ms.toFixed(2),
|
|
364
438
|
frames,
|
|
365
439
|
systems,
|
|
366
440
|
runtime: typeof Bun !== "undefined" ? `bun ${Bun.version}` : `node ${process.version}`,
|
|
367
441
|
tier,
|
|
442
|
+
// Whether the device timed each render pass on the GPU (RenderCapabilities.gpuTimestamps): the trace's gpu thread and ProbeTimings.gpuPasses.
|
|
443
|
+
gpuTimestamps: (JSON.parse(boundHost()!.abi.og_render_capabilities(engine.id)) as { gpuTimestamps?: boolean }).gpuTimestamps ?? false,
|
|
368
444
|
quit,
|
|
369
445
|
unchecked,
|
|
370
446
|
audio: { mode: audioMode, commands: audioStats.commands, receivedHash: audioStats.receivedHash, wavSha256 },
|
|
371
447
|
outputs,
|
|
372
448
|
warnings,
|
|
449
|
+
expectations,
|
|
373
450
|
};
|
|
374
451
|
if (asJson) process.stdout.write(JSON.stringify(summary) + "\n");
|
|
375
|
-
else
|
|
452
|
+
else {
|
|
453
|
+
process.stdout.write(`tick ${dump.tick} hash ${dump.hash} entities ${dump.entities}\n`);
|
|
454
|
+
if (expectations.results.length > 0) process.stdout.write(`expectations: ${expectations.passed} passed, ${expectations.failed} failed, ${expectations.unchecked} unchecked\n`);
|
|
455
|
+
}
|
|
376
456
|
if (listening !== undefined) extras.og_node_probe_close(engine.id);
|
|
377
|
-
|
|
457
|
+
if (room !== undefined) extras.og_node_room_close(engine.id);
|
|
458
|
+
process.exit(frameFailed || expectations.failed > 0 ? 1 : 0);
|
package/src/p2.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The phase 2 modules' namespaces (IDL `terrain`): thin calls into the host's binding symbols,
|
|
1
|
+
// The phase 2 and depth modules' namespaces (IDL `terrain`, `stream`, `path`, `timeline`): thin calls into the host's binding symbols,
|
|
2
2
|
// typed by the generated declarations through index.ts.
|
|
3
3
|
|
|
4
4
|
import type { HostAbi } from "./generated/abi.ts";
|
|
@@ -33,3 +33,21 @@ export const stream = Object.freeze({
|
|
|
33
33
|
budget: (world: W) => parse(abi().og_stream_budget(world.id)),
|
|
34
34
|
setBudget: (world: W, bytes: number) => parse(abi().og_stream_set_budget(world.id, bytes)),
|
|
35
35
|
});
|
|
36
|
+
|
|
37
|
+
// Depth S2 (IDL `path`, `timeline`).
|
|
38
|
+
export const path = Object.freeze({
|
|
39
|
+
create: (world: W, desc?: unknown) => parse(abi().og_path_create(world.id, json(desc))),
|
|
40
|
+
find: (world: W, name: string) => parse(abi().og_path_find(world.id, name)),
|
|
41
|
+
sample: (world: W, p: number, t: number) => parse(abi().og_path_sample(world.id, p, t)),
|
|
42
|
+
length: (world: W, p: number) => parse(abi().og_path_length(world.id, p)),
|
|
43
|
+
nearest: (world: W, p: number, point: unknown) =>
|
|
44
|
+
parse(abi().og_path_nearest(world.id, p, JSON.stringify(point))),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const timeline = Object.freeze({
|
|
48
|
+
info: (world: W, asset: number) => parse(abi().og_timeline_info(world.id, asset)),
|
|
49
|
+
play: (world: W, entity: number) => parse(abi().og_timeline_play(world.id, entity)),
|
|
50
|
+
pause: (world: W, entity: number) => parse(abi().og_timeline_pause(world.id, entity)),
|
|
51
|
+
seek: (world: W, entity: number, time: number) => parse(abi().og_timeline_seek(world.id, entity, time)),
|
|
52
|
+
stop: (world: W, entity: number) => parse(abi().og_timeline_stop(world.id, entity)),
|
|
53
|
+
});
|
package/src/platform-local.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface PlatformHost {
|
|
|
28
28
|
/** `player`, `local` or `none` (og.platform.info). */
|
|
29
29
|
readonly host: string;
|
|
30
30
|
readonly capabilities: readonly string[];
|
|
31
|
+
/** The Player hello's UTC milliseconds, sampled when this host attaches. */
|
|
32
|
+
readonly time?: number | undefined;
|
|
31
33
|
/** Handle calls in order; answer every one but `telemetry`, now or later. */
|
|
32
34
|
handle(calls: readonly PlatformCall[], answer: (request: number, result: PlatformAnswer) => void): void;
|
|
33
35
|
}
|
package/src/playtest.ts
CHANGED
|
@@ -52,6 +52,42 @@ export interface PlaytestRun {
|
|
|
52
52
|
wav(file: string): { rate: number; frames: number; peak: number; rms: number };
|
|
53
53
|
/** The runs of PlaytestDesc.then, in order, each with its own files (empty without `then`). */
|
|
54
54
|
readonly then: readonly PlaytestRun[];
|
|
55
|
+
/** og.net's shared hash at the end (og.net.hash): the world without this process's session, what lockstep peers compare. */
|
|
56
|
+
readonly netHash: string;
|
|
57
|
+
/** With PlaytestDesc.room: og.net.status at the end (running, the start tick, the seats, the first divergent tick); null outside a room. */
|
|
58
|
+
readonly lockstep: PlaytestLockstep | null;
|
|
59
|
+
/** With PlaytestDesc.room: the room's other processes (peer scripts 1, 2, ...), each with its own files; empty outside a room. */
|
|
60
|
+
readonly peers: readonly PlaytestRun[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** og.net.status of a playtest process in a room, at its end. */
|
|
64
|
+
export interface PlaytestLockstep {
|
|
65
|
+
readonly running: boolean;
|
|
66
|
+
readonly peer: number | null;
|
|
67
|
+
readonly startTick: number | null;
|
|
68
|
+
readonly players: number | null;
|
|
69
|
+
readonly waits: number;
|
|
70
|
+
/** The first divergent tick, once og.Desync was sent. */
|
|
71
|
+
readonly desync: number | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** PlaytestDesc.room: the script's processes play together in one room. */
|
|
75
|
+
export interface PlaytestRoom {
|
|
76
|
+
/** How many processes join the room (2 to 16). */
|
|
77
|
+
readonly players: number;
|
|
78
|
+
/** Each process's script by its place (0 is the playtest's own `commands`); a process without one runs `commands`. */
|
|
79
|
+
readonly commands?: readonly string[];
|
|
80
|
+
/** The room's name in the relay (default `playtest`). */
|
|
81
|
+
readonly name?: string;
|
|
82
|
+
/**
|
|
83
|
+
* The tick every process stops at (`og run --ticks`), when the scripts do not end first: their
|
|
84
|
+
* `netHash` there is what lockstep makes equal. Leave it out to end each script by its `quit`
|
|
85
|
+
* (or its last step). A script's steps are on the room's shared ticks once lockstep runs (a
|
|
86
|
+
* peer that takes the authority's snapshot at the start moves to its tick and its steps stay
|
|
87
|
+
* on theirs, a step whose tick it jumped past running at once), so a `quit` or an `expect` at
|
|
88
|
+
* tick t runs at tick t on every peer.
|
|
89
|
+
*/
|
|
90
|
+
readonly ticks?: number;
|
|
55
91
|
}
|
|
56
92
|
|
|
57
93
|
/** The checks a playtest makes; a playtest that made none fails. */
|
|
@@ -91,6 +127,14 @@ export interface PlaytestDesc {
|
|
|
91
127
|
readonly seed?: number;
|
|
92
128
|
/** The render target, `WIDTHxHEIGHT` (default 1280x720). */
|
|
93
129
|
readonly size?: string;
|
|
130
|
+
/**
|
|
131
|
+
* `action[:args];...` run once through the command channel after `start`, before the first
|
|
132
|
+
* tick (`og run --setup`), in every process of the playtest: the stand-ins for what a host does
|
|
133
|
+
* around the game, such as `cover:12,12,40,40`, the published page's Back over the play surface
|
|
134
|
+
* (`Viewport.covered`), so the frames lay out as the page's. Not part of `commands`: the
|
|
135
|
+
* published page plays a script without it and covers itself.
|
|
136
|
+
*/
|
|
137
|
+
readonly setup?: string;
|
|
94
138
|
/** Run the script this many times in fresh processes (default 1) and report every end hash in `run.hashes`: a determinism check. The files are the first run's. */
|
|
95
139
|
readonly runs?: number;
|
|
96
140
|
/**
|
|
@@ -106,9 +150,25 @@ export interface PlaytestDesc {
|
|
|
106
150
|
* process has its own store in memory.
|
|
107
151
|
*/
|
|
108
152
|
readonly store?: "keep";
|
|
109
|
-
/**
|
|
153
|
+
/**
|
|
154
|
+
* Play the script in a room (docs/design.md §2.8, §2.10): the playtest starts a relay
|
|
155
|
+
* (`og relay`, in its own process) and runs `players` processes at once, each `og run
|
|
156
|
+
* --room ws://127.0.0.1:P/<name>` with its own script (`room.commands[i]`, else `commands`)
|
|
157
|
+
* and its own files. The first is the run; the others are `run.peers`. Every process stops
|
|
158
|
+
* at `room.ticks`, or at its script's `quit`, which lands on one tick of the room for all.
|
|
159
|
+
* `runs` is refused with it; each `then` step relaunches every process at once into a room of
|
|
160
|
+
* its own (`<name>.then-<n>`), each running the step's script, its runs in `run.then[n].peers`;
|
|
161
|
+
* `store: "keep"` keeps one store per place across them.
|
|
162
|
+
*/
|
|
163
|
+
readonly room?: PlaytestRoom;
|
|
164
|
+
/** More processes after the script, in order, each a fresh `og run` (a relaunch): their runs are `run.then`. With `room`, every place of the room relaunches at once. */
|
|
110
165
|
readonly then?: readonly PlaytestStep[];
|
|
111
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Checks over what the run wrote. Optional when the script carries `expect:` steps: each
|
|
168
|
+
* expectation is a check (a failed one fails the playtest with its tick and observed value,
|
|
169
|
+
* a frame one without a renderer is unchecked), so a playtest can be its script alone.
|
|
170
|
+
*/
|
|
171
|
+
readonly check?: (run: PlaytestRun, t: PlaytestChecks) => void | Promise<void>;
|
|
112
172
|
}
|
|
113
173
|
|
|
114
174
|
/** A later process of a playtest (`PlaytestDesc.then`). */
|
|
@@ -124,8 +184,9 @@ export const PLAYTEST_CLOCK = 1767225600000;
|
|
|
124
184
|
|
|
125
185
|
/** Declare a playtest: the default export of a `tests/*.og.ts` file. */
|
|
126
186
|
export function playtest(desc: PlaytestDesc): PlaytestDesc {
|
|
127
|
-
|
|
128
|
-
|
|
187
|
+
const expects = /(^|;)\s*[\d.]+:expect:/.test(desc.commands ?? "");
|
|
188
|
+
if (!desc.name || !desc.commands || (typeof desc.check !== "function" && !expects)) {
|
|
189
|
+
throw new Error("playtest({ name, commands, check }) needs all three (check may be left out when the commands carry expect: steps)");
|
|
129
190
|
}
|
|
130
191
|
return Object.freeze({ ...desc });
|
|
131
192
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface ColumnSpec {
|
|
|
42
42
|
readonly stride: number;
|
|
43
43
|
/** The stride is each field's capacity (`str(32)`), not the kind's. */
|
|
44
44
|
readonly capacity?: boolean;
|
|
45
|
+
/** The list factory (`list(u32(), 8)`): no kind of its own, a list field's kind is its element's. */
|
|
46
|
+
readonly list?: boolean;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export const COLUMNS: ReadonlyMap<string, ColumnSpec> = new Map(
|
|
@@ -122,6 +124,8 @@ export interface FieldSpec {
|
|
|
122
124
|
readonly options: Record<string, unknown>;
|
|
123
125
|
/** A `str` field's capacity in bytes. */
|
|
124
126
|
readonly capacity?: number;
|
|
127
|
+
/** A list field's capacity in elements (`list(u32(), 8)`); `kind` is the element's. */
|
|
128
|
+
readonly list?: number;
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
export interface Token {
|
|
@@ -138,6 +142,7 @@ function fieldList(fields: Readonly<Record<string, FieldSpec>>): unknown[] {
|
|
|
138
142
|
default: spec?.default,
|
|
139
143
|
options: spec?.options ?? {},
|
|
140
144
|
...(spec?.capacity === undefined ? {} : { capacity: spec.capacity }),
|
|
145
|
+
...(spec?.list === undefined ? {} : { list: spec.list }),
|
|
141
146
|
}));
|
|
142
147
|
}
|
|
143
148
|
|
|
@@ -150,13 +155,18 @@ export function defineRecord(kind: "component" | "resource", name: string, field
|
|
|
150
155
|
return token;
|
|
151
156
|
}
|
|
152
157
|
|
|
153
|
-
|
|
158
|
+
type DeclField = { name: string; column?: string; type?: string; default?: unknown; list?: number };
|
|
159
|
+
|
|
160
|
+
function builtinFields(fields: ReadonlyArray<DeclField>): Record<string, FieldSpec> {
|
|
154
161
|
const out: Record<string, FieldSpec> = {};
|
|
155
|
-
for (const f of fields)
|
|
162
|
+
for (const f of fields) {
|
|
163
|
+
const spec = { kind: f.column ?? f.type ?? "", default: f.default, options: Object.freeze({}) };
|
|
164
|
+
out[f.name] = Object.freeze(f.list === undefined ? spec : { ...spec, list: f.list });
|
|
165
|
+
}
|
|
156
166
|
return out;
|
|
157
167
|
}
|
|
158
168
|
|
|
159
|
-
type Decl = { name: string; schema: string; fields: Array<
|
|
169
|
+
type Decl = { name: string; schema: string; fields: Array<DeclField> };
|
|
160
170
|
|
|
161
171
|
/** The engine-owned tokens on `og`, under their schema names (`og.Transform`). */
|
|
162
172
|
export const BUILTINS: Readonly<Record<string, Token>> = Object.freeze(
|
|
@@ -316,7 +326,9 @@ function layoutOf(component: Token): { names: string[]; kinds: ColumnSpec[] } {
|
|
|
316
326
|
const kind = COLUMNS.get(field.kind);
|
|
317
327
|
if (!kind) throw new Error(`'${component.name}.${n}' is not a column field`);
|
|
318
328
|
// A str column's stride is the field's capacity: one byte lane per byte.
|
|
319
|
-
|
|
329
|
+
if (kind.capacity) return { ...kind, stride: field.capacity ?? 0 };
|
|
330
|
+
// A list's is its element's, once for the count and once per element.
|
|
331
|
+
return field.list === undefined ? kind : { ...kind, stride: kind.stride * (field.list + 1) };
|
|
320
332
|
});
|
|
321
333
|
layouts.set(component, (l = { names, kinds }));
|
|
322
334
|
}
|
|
@@ -483,6 +495,9 @@ interface RecordField {
|
|
|
483
495
|
readonly size: number;
|
|
484
496
|
readonly readonly: boolean;
|
|
485
497
|
readonly variants?: readonly string[];
|
|
498
|
+
/** A list's element kind (`f32`, `f64`, `u32`, `i32`, `handle`) and capacity in elements. */
|
|
499
|
+
readonly element?: string;
|
|
500
|
+
readonly capacity?: number;
|
|
486
501
|
}
|
|
487
502
|
|
|
488
503
|
interface RecordLayout {
|
|
@@ -599,9 +614,55 @@ const LANES: Readonly<Record<string, readonly string[]>> = {
|
|
|
599
614
|
Color: ["r", "g", "b"],
|
|
600
615
|
};
|
|
601
616
|
|
|
617
|
+
/** Bytes of one lane of a list's element kind. */
|
|
618
|
+
const LANE_SIZE: Readonly<Record<string, number>> = { f32: 4, f64: 8, u32: 4, i32: 4, handle: 8 };
|
|
619
|
+
|
|
620
|
+
function readLane(v: DataView, o: number, element: string): number {
|
|
621
|
+
switch (element) {
|
|
622
|
+
case "f32": return v.getFloat32(o, true);
|
|
623
|
+
case "f64": return v.getFloat64(o, true);
|
|
624
|
+
case "u32": return v.getUint32(o, true);
|
|
625
|
+
case "i32": return v.getInt32(o, true);
|
|
626
|
+
default: return v.getUint32(o, true) + v.getUint32(o + 4, true) * 4294967296;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function writeLane(v: DataView, o: number, element: string, n: number): void {
|
|
631
|
+
switch (element) {
|
|
632
|
+
case "f32": return v.setFloat32(o, n, true);
|
|
633
|
+
case "f64": return v.setFloat64(o, n, true);
|
|
634
|
+
case "u32": return v.setUint32(o, n, true);
|
|
635
|
+
case "i32": return v.setInt32(o, n, true);
|
|
636
|
+
default:
|
|
637
|
+
v.setUint32(o, n % 4294967296, true);
|
|
638
|
+
v.setUint32(o + 4, Math.floor(n / 4294967296), true);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** A list element checked as the engine's JSON path checks it; throws what it refuses. */
|
|
643
|
+
function listElement(value: unknown, element: string, i: number): number {
|
|
644
|
+
const n = number(value, `element ${i}`);
|
|
645
|
+
const whole = (lo: number, hi: number, what: string) => {
|
|
646
|
+
if (!Number.isInteger(n) || n < lo || n > hi) throw new Error(`element ${i}: ${n} is not ${what}`);
|
|
647
|
+
};
|
|
648
|
+
if (element === "u32") whole(0, 4294967295, "a u32");
|
|
649
|
+
else if (element === "i32") whole(-2147483648, 2147483647, "an i32");
|
|
650
|
+
else if (element === "handle") whole(0, 9007199254740991, "a handle");
|
|
651
|
+
return n;
|
|
652
|
+
}
|
|
653
|
+
|
|
602
654
|
function decodeField(v: DataView, bytes: Uint8Array, f: RecordField): unknown {
|
|
603
655
|
const o = f.offset;
|
|
604
656
|
switch (f.kind) {
|
|
657
|
+
case "list": {
|
|
658
|
+
// A frozen copy: `push` throws instead of changing nothing.
|
|
659
|
+
const element = f.element ?? "f32";
|
|
660
|
+
const lane = LANE_SIZE[element] ?? 4;
|
|
661
|
+
const count = Math.min(readLane(v, o, element), f.capacity ?? 0);
|
|
662
|
+
const out: number[] = [];
|
|
663
|
+
for (let i = 0; i < count; i++) out.push(readLane(v, o + lane * (i + 1), element));
|
|
664
|
+
return Object.freeze(out);
|
|
665
|
+
}
|
|
605
666
|
case "f32": return v.getFloat32(o, true);
|
|
606
667
|
case "f64": return v.getFloat64(o, true);
|
|
607
668
|
case "u32": return v.getUint32(o, true);
|
|
@@ -639,6 +700,23 @@ function number(value: unknown, what: string): number {
|
|
|
639
700
|
function encodeField(v: DataView, bytes: Uint8Array, f: RecordField, value: unknown): void {
|
|
640
701
|
const o = f.offset;
|
|
641
702
|
switch (f.kind) {
|
|
703
|
+
case "list": {
|
|
704
|
+
// The whole list: an array, at most the capacity, every element checked before a byte moves.
|
|
705
|
+
const element = f.element ?? "f32";
|
|
706
|
+
const capacity = f.capacity ?? 0;
|
|
707
|
+
const items = value === null || value === undefined ? [] : value;
|
|
708
|
+
if (!Array.isArray(items)) throw new Error(`a list is written whole, as an array, got ${JSON.stringify(value)}`);
|
|
709
|
+
if (items.length > capacity) {
|
|
710
|
+
const kind = element === "handle" ? "Asset" : element;
|
|
711
|
+
throw new Error(`${items.length} elements and the field holds ${capacity}: remove ${items.length - capacity}, or declare the field list(${kind}(), ${items.length}) or larger`);
|
|
712
|
+
}
|
|
713
|
+
const lanes = items.map((x, i) => listElement(x, element, i));
|
|
714
|
+
const lane = LANE_SIZE[element] ?? 4;
|
|
715
|
+
bytes.fill(0, o, o + f.size);
|
|
716
|
+
writeLane(v, o, element, lanes.length);
|
|
717
|
+
lanes.forEach((n, i) => writeLane(v, o + lane * (i + 1), element, n));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
642
720
|
case "f32": return v.setFloat32(o, number(value, "the value"), true);
|
|
643
721
|
case "f64": return v.setFloat64(o, number(value, "the value"), true);
|
|
644
722
|
case "u32": {
|
|
@@ -732,7 +810,9 @@ function recordHandle(world: WorldImpl, r: RecordCopy): object {
|
|
|
732
810
|
if (!refresh(r)) {
|
|
733
811
|
const answer = parse<Result<Record<string, unknown>>>(host!.abi.og_world_resource(id, name));
|
|
734
812
|
if (!answer.ok) throw new Error(`${name}: ${answer.reason}`);
|
|
735
|
-
|
|
813
|
+
const value = answer.value[key];
|
|
814
|
+
// A list reads as a frozen copy on this path too.
|
|
815
|
+
return (lastValue = Array.isArray(value) ? Object.freeze(value) : value);
|
|
736
816
|
}
|
|
737
817
|
}
|
|
738
818
|
return (lastValue = decodeField(r.view, r.bytes, field()));
|
|
@@ -1122,7 +1202,7 @@ export class Engine {
|
|
|
1122
1202
|
/** Attach what answers og.platform's calls: the native host's local store, the web shell's Player bridge. */
|
|
1123
1203
|
attachPlatform(host: PlatformHost): void {
|
|
1124
1204
|
this.#platform = host;
|
|
1125
|
-
abi().og_platform_set_host(this.id, JSON.stringify({ host: host.host, capabilities: host.capabilities, time: this.#clock.now(), clock: this.#clock.kind }));
|
|
1205
|
+
abi().og_platform_set_host(this.id, JSON.stringify({ host: host.host, capabilities: host.capabilities, time: host.time ?? this.#clock.now(), clock: host.time === undefined ? this.#clock.kind : "player" }));
|
|
1126
1206
|
}
|
|
1127
1207
|
|
|
1128
1208
|
/** Set the clock og.platform tells the game (before attachPlatform, which tells it the time). */
|
|
@@ -1311,6 +1391,10 @@ function makeProbe(id: number) {
|
|
|
1311
1391
|
commands: (script: string) => parse(a().og_engine_probe_commands(id, script)),
|
|
1312
1392
|
listen: (seconds?: number) => parse(a().og_engine_probe_listen(id, seconds)),
|
|
1313
1393
|
memory: () => parse(a().og_engine_probe_memory(id)),
|
|
1394
|
+
describe: () => parse(a().og_engine_probe_describe(id)),
|
|
1395
|
+
expect: (what: string) => parse(a().og_engine_probe_expect(id, what)),
|
|
1396
|
+
expectations: () => parse(a().og_engine_probe_expectations(id)),
|
|
1397
|
+
saveScene: (write?: boolean, spawned?: boolean) => parse(a().og_engine_probe_save_scene(id, write, spawned)),
|
|
1314
1398
|
});
|
|
1315
1399
|
}
|
|
1316
1400
|
|
|
@@ -1331,6 +1415,7 @@ export const render = Object.freeze({
|
|
|
1331
1415
|
parse(abi().og_render_update_material(world.id, material, json(desc))),
|
|
1332
1416
|
capabilities: (world: WorldImpl) => parse(abi().og_render_capabilities(world.id)),
|
|
1333
1417
|
applyLook: (world: WorldImpl, look: number) => parse(abi().og_render_apply_look(world.id, look)),
|
|
1418
|
+
applyQuality: (world: WorldImpl, preset: string) => parse(abi().og_render_apply_quality(world.id, preset)),
|
|
1334
1419
|
look: (world: WorldImpl) => parse(abi().og_render_look(world.id)),
|
|
1335
1420
|
text: (world: WorldImpl, desc?: unknown) => parse(abi().og_render_text(world.id, json(desc))),
|
|
1336
1421
|
sprite: (world: WorldImpl, desc?: unknown) => parse(abi().og_render_sprite(world.id, json(desc))),
|
|
@@ -1363,17 +1448,21 @@ export const load = Object.freeze({
|
|
|
1363
1448
|
environment: (world: WorldImpl, path: string) => parse(abi().og_load_environment(world.id, path)),
|
|
1364
1449
|
emitter: (world: WorldImpl, path: string) => parse(abi().og_load_emitter(world.id, path)),
|
|
1365
1450
|
shader: (world: WorldImpl, path: string) => parse(abi().og_load_shader(world.id, path)),
|
|
1366
|
-
|
|
1451
|
+
style: (world: WorldImpl, path: string) => parse(abi().og_load_style(world.id, path)),
|
|
1367
1452
|
look: (world: WorldImpl, path: string) => parse(abi().og_load_look(world.id, path)),
|
|
1368
1453
|
lookInfo: (world: WorldImpl, look: number) => parse(abi().og_load_look_info(world.id, look)),
|
|
1369
1454
|
palette: (world: WorldImpl, path: string) => parse(abi().og_load_palette(world.id, path)),
|
|
1370
1455
|
soundInfo: (world: WorldImpl, sound: number) => parse(abi().og_load_sound_info(world.id, sound)),
|
|
1456
|
+
video: (world: WorldImpl, path: string) => parse(abi().og_load_video(world.id, path)),
|
|
1457
|
+
videoInfo: (world: WorldImpl, video: number) => parse(abi().og_load_video_info(world.id, video)),
|
|
1371
1458
|
status: (world: WorldImpl, asset: number) => abi().og_load_status(world.id, asset),
|
|
1372
1459
|
error: (world: WorldImpl, asset: number) => parse(abi().og_load_error(world.id, asset)),
|
|
1373
1460
|
gltfInfo: (world: WorldImpl, asset: number) => parse(abi().og_load_gltf_info(world.id, asset)),
|
|
1374
1461
|
animation: (world: WorldImpl, path: string) => parse(abi().og_load_animation(world.id, path)),
|
|
1462
|
+
timeline: (world: WorldImpl, path: string) => parse(abi().og_load_timeline(world.id, path)),
|
|
1375
1463
|
flipbook: (world: WorldImpl, path: string) => parse(abi().og_load_flipbook(world.id, path)),
|
|
1376
1464
|
rig: (world: WorldImpl, path: string) => parse(abi().og_load_rig(world.id, path)),
|
|
1465
|
+
rig2d: (world: WorldImpl, path: string) => parse(abi().og_load_rig2d(world.id, path)),
|
|
1377
1466
|
tilemap: (world: WorldImpl, path: string) => parse(abi().og_load_tilemap(world.id, path)),
|
|
1378
1467
|
});
|
|
1379
1468
|
|
|
@@ -1381,8 +1470,10 @@ export const anim = Object.freeze({
|
|
|
1381
1470
|
graph: (world: WorldImpl, desc?: unknown) => parse(abi().og_anim_graph(world.id, json(desc))),
|
|
1382
1471
|
humanoid: (world: WorldImpl, desc?: unknown) => parse(abi().og_anim_humanoid(world.id, json(desc))),
|
|
1383
1472
|
retarget: (world: WorldImpl, desc?: unknown) => parse(abi().og_anim_retarget(world.id, json(desc))),
|
|
1384
|
-
|
|
1385
|
-
|
|
1473
|
+
rig2dInfo: (world: WorldImpl, rig: number) => parse(abi().og_anim_rig2d_info(world.id, rig)),
|
|
1474
|
+
skeleton2dBoxes: (world: WorldImpl, entity: number) => parse(abi().og_anim_skeleton2d_boxes(world.id, entity)),
|
|
1475
|
+
// One lookup for a flipbook's or a 2D rig's clips (an index, -1 when
|
|
1476
|
+
// missing) and a loaded model's (og.load.animation: a ClipAsset, 0 when missing).
|
|
1386
1477
|
clip: (info: { clips?: ReadonlyArray<{ clip: number; name: string }>; skeleton?: number }, name: string): number =>
|
|
1387
1478
|
info?.clips?.find((c) => c.name === name)?.clip ?? (info?.skeleton !== undefined ? 0 : -1),
|
|
1388
1479
|
});
|
package/src/targets.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Depth S8's additions (IDL `render` render targets and custom post
|
|
2
|
+
// passes, `load.post`): 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
|
+
export const targetsRender = Object.freeze({
|
|
20
|
+
renderTarget: (world: W, desc?: unknown) => parse(abi().og_render_render_target(world.id, json(desc))),
|
|
21
|
+
setPostParam: (world: W, post: number, name: string, value: ReadonlyArray<number>) =>
|
|
22
|
+
parse(abi().og_render_set_post_param(world.id, post, name, JSON.stringify(value))),
|
|
23
|
+
postErrors: (world: W) => parse(abi().og_render_post_errors(world.id)),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const targetsLoad = Object.freeze({
|
|
27
|
+
post: (world: W, path: string) => parse(abi().og_load_post(world.id, path)),
|
|
28
|
+
});
|