@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/ui-kit/store.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type * as Api from "@origingame/engine";
|
|
6
6
|
import { kit } from "./bind.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { hudBox, fit, fmt, frame, grow, iconOr, px, token } from "./util.ts";
|
|
8
8
|
|
|
9
9
|
/** A screen panel: a dialog over the game, wide on desktop, full width on a phone. With `scrim` a press on the backdrop closes it too. */
|
|
10
10
|
function screen(id: string, title: string, width: number, build: () => void, scrim = false): boolean {
|
|
@@ -16,7 +16,7 @@ function screen(id: string, title: string, width: number, build: () => void, scr
|
|
|
16
16
|
/** An item's picture: its icon, else its initial on the slot. */
|
|
17
17
|
function picture(id: string, icon: string | undefined, name: string, size: number): void {
|
|
18
18
|
if (icon && kit.hasIcon(icon)) kit.icon(id, icon, { size });
|
|
19
|
-
else kit.label(id, name.slice(0, 1).toUpperCase(), { step: "xl2", font: "display", color: token("
|
|
19
|
+
else kit.label(id, name.slice(0, 1).toUpperCase(), { step: "xl2", font: "display", color: token("inkMuted"), nowrap: true });
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function shopScreen(id: string, desc: Api.ShopDesc = {}): Api.ShopResult {
|
|
@@ -37,10 +37,10 @@ export function shopScreen(id: string, desc: Api.ShopDesc = {}): Api.ShopResult
|
|
|
37
37
|
} else {
|
|
38
38
|
kit.spacer("Gap", null);
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
if (desc.currencyIcon) iconOr("Icon", desc.currencyIcon, 18, token("
|
|
40
|
+
hudBox("Balance", () => {
|
|
41
|
+
if (desc.currencyIcon) iconOr("Icon", desc.currencyIcon, 18, token("gold"));
|
|
42
42
|
kit.counter("Amount", balance, { font: "mono", step: "base" });
|
|
43
|
-
kit.label("Unit", desc.currency ?? "coins", { step: "base", color: token("
|
|
43
|
+
kit.label("Unit", desc.currency ?? "coins", { step: "base", color: token("inkMuted"), nowrap: true });
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
46
|
const body = (): void => {
|
|
@@ -49,17 +49,17 @@ export function shopScreen(id: string, desc: Api.ShopDesc = {}): Api.ShopResult
|
|
|
49
49
|
picture("Picture", it.icon, it.name ?? "", 36);
|
|
50
50
|
kit.label("Name", it.name ?? "", { step: "base", nowrap: true });
|
|
51
51
|
if (it.owned) kit.badge("Owned", desc.ownedLabel ?? "Owned", "success");
|
|
52
|
-
else kit.label("Price", fmt(it.price ?? 0), { step: "meta", font: "mono", color: token(balance >= (it.price ?? 0) ? "
|
|
52
|
+
else kit.label("Price", fmt(it.price ?? 0), { step: "meta", font: "mono", color: token(balance >= (it.price ?? 0) ? "highlight" : "inkFaint"), nowrap: true });
|
|
53
53
|
});
|
|
54
54
|
if (g.clicked !== null && g.clicked !== undefined) sel = g.clicked;
|
|
55
55
|
const it = items[sel];
|
|
56
56
|
kit.column("Detail", { width: phone ? grow() : px(220), gap: t.sm }, () => {
|
|
57
57
|
if (!it) {
|
|
58
|
-
kit.label("Empty", desc.emptyLabel ?? "Nothing for sale here", { color: token("
|
|
58
|
+
kit.label("Empty", desc.emptyLabel ?? "Nothing for sale here", { color: token("inkMuted") });
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
kit.label("Name", it.name ?? "", { step: "lg", width: grow() });
|
|
62
|
-
if (it.description) kit.label("Description", it.description, { step: "meta", color: token("
|
|
62
|
+
if (it.description) kit.label("Description", it.description, { step: "meta", color: token("inkMuted"), width: grow() });
|
|
63
63
|
kit.keyValue("Price", desc.priceLabel ?? "Price", `${fmt(it.price ?? 0)} ${desc.currency ?? "coins"}`, true);
|
|
64
64
|
const short = balance < (it.price ?? 0);
|
|
65
65
|
// Short of the price, Buy still answers: it refuses, shaking with the error sound.
|
|
@@ -101,7 +101,7 @@ export function inventoryScreen(id: string, desc: Api.InventoryDesc = {}): Api.I
|
|
|
101
101
|
const it = bySlot.get(i);
|
|
102
102
|
if (!it) return;
|
|
103
103
|
picture("Picture", it.icon, it.name ?? "", cell * 0.5);
|
|
104
|
-
if ((it.count ?? 0) > 1) kit.label("Count", `${it.count}`, { step: "xs", font: "mono", color: token("
|
|
104
|
+
if ((it.count ?? 0) > 1) kit.label("Count", `${it.count}`, { step: "xs", font: "mono", color: token("inkMuted"), nowrap: true });
|
|
105
105
|
});
|
|
106
106
|
if (g.clicked !== null && g.clicked !== undefined) sel = g.clicked;
|
|
107
107
|
if (g.dropFrom !== null && g.dropFrom !== undefined && g.dropTo !== null && g.dropTo !== undefined && bySlot.has(g.dropFrom)) {
|
|
@@ -111,11 +111,11 @@ export function inventoryScreen(id: string, desc: Api.InventoryDesc = {}): Api.I
|
|
|
111
111
|
const it = sel >= 0 ? bySlot.get(sel) : undefined;
|
|
112
112
|
kit.column("Detail", { width: phone ? grow() : px(200), gap: t.sm }, () => {
|
|
113
113
|
if (!it) {
|
|
114
|
-
kit.label("Hint", desc.hintLabel ?? "Select an item. Drag it to another slot to move it.", { step: "meta", color: token("
|
|
114
|
+
kit.label("Hint", desc.hintLabel ?? "Select an item. Drag it to another slot to move it.", { step: "meta", color: token("inkMuted"), width: grow() });
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
kit.label("Name", it.name ?? "", { step: "lg", width: grow() });
|
|
118
|
-
if (it.description) kit.label("Description", it.description, { step: "meta", color: token("
|
|
118
|
+
if (it.description) kit.label("Description", it.description, { step: "meta", color: token("inkMuted"), width: grow() });
|
|
119
119
|
if ((it.count ?? 0) > 1) kit.keyValue("Count", desc.countLabel ?? "Count", `${it.count}`, true);
|
|
120
120
|
if (desc.useLabel !== "") {
|
|
121
121
|
if ((kit.button("Use", desc.useLabel ?? "Use", { variant: "primary", width: grow() }) as Api.Response).clicked) out.used = it.id ?? null;
|
|
@@ -143,8 +143,8 @@ export function levelSelect(id: string, desc: Api.LevelSelectDesc = {}): Api.Lev
|
|
|
143
143
|
// Back from every input: the button, a press on the scrim, Escape, B.
|
|
144
144
|
out.closed = screen(id, desc.title ?? "Levels", 720, () => {
|
|
145
145
|
kit.row("Total", { gap: t.xs, cross: "center" }, () => {
|
|
146
|
-
iconOr("Star", "star", 18, token("
|
|
147
|
-
kit.label("Count", `${earned} / ${levels.length * 3}`, { font: "mono", color: token("
|
|
146
|
+
iconOr("Star", "star", 18, token("primary"), starsLabel(earned, levels.length * 3));
|
|
147
|
+
kit.label("Count", `${earned} / ${levels.length * 3}`, { font: "mono", color: token("inkMuted"), nowrap: true });
|
|
148
148
|
});
|
|
149
149
|
// A numeral is the card's face, in display type on a square card; a
|
|
150
150
|
// name takes a wider card and a smaller step, wrapped to two lines (cut
|
|
@@ -157,14 +157,14 @@ export function levelSelect(id: string, desc: Api.LevelSelectDesc = {}): Api.Lev
|
|
|
157
157
|
const l = levels[i]!;
|
|
158
158
|
const name = l.name ?? `${i + 1}`;
|
|
159
159
|
if (l.locked) {
|
|
160
|
-
iconOr("Lock", "lock", 28, token("
|
|
160
|
+
iconOr("Lock", "lock", 28, token("inkFaint"), (desc.lockedLabel ?? "{name}, locked").replaceAll("{name}", name));
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
163
163
|
if ([...name].length > 3) kit.label("Name", name, { step: phone ? "base" : "lg", font: "display", align: "center", width: grow(), maxLines: 2, ellipsis: true });
|
|
164
164
|
else kit.label("Name", name, { step: "xl3", font: "display", nowrap: true });
|
|
165
165
|
const stars = Math.min(3, l.stars ?? 0);
|
|
166
166
|
kit.row("Stars", { gap: 2 }, () => {
|
|
167
|
-
for (let k = 0; k < 3; k++) iconOr(kit.idi("Star", k), "star", 14, token(k < stars ? "
|
|
167
|
+
for (let k = 0; k < 3; k++) iconOr(kit.idi("Star", k), "star", 14, token(k < stars ? "primary" : "controlStrong"), k === 0 ? starsLabel(stars, 3) : "");
|
|
168
168
|
});
|
|
169
169
|
});
|
|
170
170
|
if (g.clicked !== null && g.clicked !== undefined && !levels[g.clicked]?.locked) out.chosen = g.clicked;
|
|
@@ -176,7 +176,7 @@ export function levelSelect(id: string, desc: Api.LevelSelectDesc = {}): Api.Lev
|
|
|
176
176
|
return out;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
const MEDALS: readonly Api.ThemeColor[] = ["warn", "
|
|
179
|
+
const MEDALS: readonly Api.ThemeColor[] = ["warn", "inkMuted", "highlight"];
|
|
180
180
|
|
|
181
181
|
export function leaderboardScreen(id: string, desc: Api.LeaderboardDesc = {}): Api.LeaderboardResult {
|
|
182
182
|
const { t, phone } = frame();
|
|
@@ -203,14 +203,14 @@ export function leaderboardScreen(id: string, desc: Api.LeaderboardDesc = {}): A
|
|
|
203
203
|
});
|
|
204
204
|
} else if (entries.length === 0) {
|
|
205
205
|
kit.column("Empty", { width: grow(), height, align: "center", cross: "center" }, () => {
|
|
206
|
-
kit.label("Message", desc.empty ?? "No scores yet", { color: token("
|
|
206
|
+
kit.label("Message", desc.empty ?? "No scores yet", { color: token("inkMuted"), align: "center", width: grow() });
|
|
207
207
|
});
|
|
208
208
|
} else {
|
|
209
209
|
const player = entries.findIndex((e) => e.player);
|
|
210
210
|
kit.list("Rows", { count: entries.length, rowHeight: t.row, height, width: grow(), selected: player >= 0 ? player : null }, (i) => {
|
|
211
211
|
const e = entries[i]!;
|
|
212
212
|
const rank = e.rank ?? i + 1;
|
|
213
|
-
kit.label("Rank", `${rank}`, { width: px(40), font: "mono", step: rank <= 3 ? "lg" : "body", color: token(rank <= 3 ? MEDALS[rank - 1]! : "
|
|
213
|
+
kit.label("Rank", `${rank}`, { width: px(40), font: "mono", step: rank <= 3 ? "lg" : "body", color: token(rank <= 3 ? MEDALS[rank - 1]! : "inkMuted"), nowrap: true });
|
|
214
214
|
kit.label("Name", e.name ?? "", { width: grow(), nowrap: true });
|
|
215
215
|
kit.label("Score", e.score ?? "", { font: "mono", nowrap: true });
|
|
216
216
|
});
|
package/src/ui-kit/story.ts
CHANGED
|
@@ -15,8 +15,10 @@ export function cutscene(id: string, desc: Api.CutsceneDesc = {}): Api.CutsceneR
|
|
|
15
15
|
kit.setNavigation(true);
|
|
16
16
|
const letterbox = desc.letterbox ?? true;
|
|
17
17
|
if (letterbox) {
|
|
18
|
+
// Cinematic bars are black in every style: not a token, which a light
|
|
19
|
+
// style (kestrel's paper) would turn into cream panels.
|
|
18
20
|
for (const [name, anchor] of [["Top", "top"], ["Bottom", "bottom"]] as const) {
|
|
19
|
-
kit.anchor(`${id}.${name}`, { anchor, margin: 0, width: grow(), height: pct(0.1), background:
|
|
21
|
+
kit.anchor(`${id}.${name}`, { anchor, margin: 0, width: grow(), height: pct(0.1), background: { color: { r: 0, g: 0, b: 0 } } }, () => {});
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
const line = lines[index]!;
|
|
@@ -68,13 +70,13 @@ export function tutorialOverlay(id: string, desc: Api.TutorialDesc = {}): Api.Tu
|
|
|
68
70
|
direction: "column",
|
|
69
71
|
gap: t.sm,
|
|
70
72
|
padding: all(t.lg),
|
|
71
|
-
background: token("
|
|
73
|
+
background: token("panelRaised"),
|
|
72
74
|
radius: t.panel,
|
|
73
75
|
}, () => {
|
|
74
76
|
const count = (desc.stepLabel ?? "{step} of {count}").replaceAll("{step}", `${index + 1}`).replaceAll("{count}", `${steps.length}`);
|
|
75
|
-
kit.label("Count", count, { step: "meta", color: token("
|
|
77
|
+
kit.label("Count", count, { step: "meta", color: token("inkMuted") });
|
|
76
78
|
if (step.title) kit.label("Title", step.title, { step: "lg", width: grow() });
|
|
77
|
-
if (step.text) kit.label("Text", step.text, { step: "reading", color: token("
|
|
79
|
+
if (step.text) kit.label("Text", step.text, { step: "reading", color: token("inkMuted"), width: grow() });
|
|
78
80
|
kit.row("Actions", { width: grow(), gap: t.sm, align: "spaceBetween", cross: "center" }, () => {
|
|
79
81
|
if ((kit.button("Skip", desc.skipLabel ?? "Skip", { variant: "quiet", size: "small" }) as Api.Response).clicked) out.skipped = true;
|
|
80
82
|
const lastStep = index + 1 >= steps.length;
|
package/src/ui-kit/util.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Small helpers the screen templates share: sizes, insets, the frame's
|
|
2
|
-
// tokens, number and time formats, and HUD
|
|
2
|
+
// tokens, number and time formats, and HUD boxes. Nothing here keeps
|
|
3
3
|
// state: what must persist lives in the UI's memory (og.ui.memory).
|
|
4
4
|
|
|
5
5
|
import type * as Api from "@origingame/engine";
|
|
@@ -25,7 +25,7 @@ export function frame(): { theme: Api.UiTheme; viewport: Api.Viewport; t: Tokens
|
|
|
25
25
|
|
|
26
26
|
export interface Tokens {
|
|
27
27
|
xxs: number; xs: number; sm: number; md: number; lg: number; xl: number; xxl: number; xl3: number;
|
|
28
|
-
|
|
28
|
+
small: number; control: number; bar: number; panel: number;
|
|
29
29
|
controlSm: number; controlMd: number; controlLg: number; gutter: number; row: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -34,7 +34,7 @@ function tokens(t: Api.UiTheme): Tokens {
|
|
|
34
34
|
const r = (k: string): number => t.radius[k] ?? 0;
|
|
35
35
|
return {
|
|
36
36
|
xxs: s("xxs"), xs: s("xs"), sm: s("sm"), md: s("md"), lg: s("lg"), xl: s("xl"), xxl: s("xxl"), xl3: s("xl3"),
|
|
37
|
-
|
|
37
|
+
small: r("small"), control: r("control"), bar: r("bar"), panel: r("panel"),
|
|
38
38
|
controlSm: t.sizes.controlSm, controlMd: t.sizes.controlMd, controlLg: t.sizes.controlLg, gutter: t.sizes.gutter, row: t.sizes.row,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
@@ -73,17 +73,17 @@ export function lapTime(seconds: number): string {
|
|
|
73
73
|
return `${m}:${s.toString().padStart(2, "0")}.${(ms % 1000).toString().padStart(3, "0")}`;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/** A HUD
|
|
77
|
-
export function
|
|
76
|
+
/** A HUD box: the style's `hud` fill and frame, cornered by its control radius, that keeps text legible over any art. */
|
|
77
|
+
export function hudBox(id: string, build: () => void, desc: { gap?: number; padX?: number; height?: number } = {}): Api.Response {
|
|
78
78
|
const { t } = frame();
|
|
79
79
|
return kit.row(id, {
|
|
80
80
|
padding: xy(desc.padX ?? t.md, 0),
|
|
81
81
|
height: px(desc.height ?? t.controlMd),
|
|
82
82
|
gap: desc.gap ?? t.sm,
|
|
83
83
|
cross: "center",
|
|
84
|
-
background: token("
|
|
85
|
-
radius: t.
|
|
86
|
-
frame: "
|
|
84
|
+
background: token("hud"),
|
|
85
|
+
radius: t.control,
|
|
86
|
+
frame: "hud",
|
|
87
87
|
}, build) as Api.Response;
|
|
88
88
|
}
|
|
89
89
|
|
package/src/web/shell.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { laneChoice } from "./lanes.ts";
|
|
|
30
30
|
import { createRpcRings, readFrames, waitForFrames, writeFrame, type RpcRings } from "./rpc-ring.ts";
|
|
31
31
|
import { localPlatform, storagePersistence, type PlatformCall, type PlatformHost } from "../platform-local.ts";
|
|
32
32
|
import type { BootPhases } from "./worker.ts";
|
|
33
|
+
import type { TraceDocument } from "../trace.ts";
|
|
33
34
|
|
|
34
35
|
export interface ShellOptions {
|
|
35
36
|
/** The play surface. Its pixel size is set from its CSS size and the device pixel ratio. */
|
|
@@ -78,6 +79,8 @@ export interface ShellOptions {
|
|
|
78
79
|
readonly locale?: string;
|
|
79
80
|
/** What answers og.platform's calls; default: the browser's own storage (`setPlatform` changes it, a Player bridge for instance). */
|
|
80
81
|
readonly platform?: PlatformHost;
|
|
82
|
+
/** Record the run's trace from the boot on, the boot scene's loads included (`trace()` reads it; `trace("start")` begins one later). */
|
|
83
|
+
readonly trace?: boolean;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/** The pixel size of a canvas laid out at `cssWidth`×`cssHeight` on this display, with the DPR cap. */
|
|
@@ -93,6 +96,17 @@ export const PAGE_PREFERENCES = [
|
|
|
93
96
|
["reducedTransparency", "(prefers-reduced-transparency: reduce)"],
|
|
94
97
|
] as const;
|
|
95
98
|
|
|
99
|
+
/**
|
|
100
|
+
* What carries og.platform's rooms on the web (docs/design.md §2.8): the engine's room ops
|
|
101
|
+
* (`{ op: "create" | "join" | "leave" | "send", ... }`, og-net's protocol) in, its room events
|
|
102
|
+
* (`joined`, `peer_joined`, `peer_left`, `message`, `left`, `error`) out, in order.
|
|
103
|
+
*/
|
|
104
|
+
export interface RoomHost {
|
|
105
|
+
/** og.platform.roomInfo's `host`: `player` for the Player's rooms. */
|
|
106
|
+
readonly host: string;
|
|
107
|
+
handle(ops: readonly Record<string, unknown>[], deliver: (events: unknown[]) => void): void;
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
/** The page's motion, contrast and transparency preferences and the platform's editing keys, as the viewport carries them to og-ui. */
|
|
97
111
|
export function pagePreferences(): { reducedMotion: boolean; highContrast: boolean; reducedTransparency: boolean; macKeys: boolean } {
|
|
98
112
|
const query = (q: string) => globalThis.matchMedia?.(q).matches ?? false;
|
|
@@ -203,6 +217,7 @@ export class Shell {
|
|
|
203
217
|
/** When the deadline was last looked at: a late look means the page itself was blocked. */
|
|
204
218
|
#watched = 0;
|
|
205
219
|
#watchdog: ReturnType<typeof setInterval> | undefined;
|
|
220
|
+
#rooms: RoomHost | null = null;
|
|
206
221
|
#platform: PlatformHost = localPlatform(storagePersistence(`og.platform:${globalThis.location?.pathname ?? "/"}`));
|
|
207
222
|
|
|
208
223
|
private constructor(worker: Worker, ready: Ready, rpc: RpcRings | undefined) {
|
|
@@ -250,6 +265,14 @@ export class Shell {
|
|
|
250
265
|
this.#answer(m.frame);
|
|
251
266
|
return;
|
|
252
267
|
}
|
|
268
|
+
if (m.type === "room") {
|
|
269
|
+
const ops = (m as unknown as { ops: Record<string, unknown>[] }).ops ?? [];
|
|
270
|
+
const deliver = (events: unknown[]) => worker.postMessage({ type: "room-events", events });
|
|
271
|
+
if (this.#rooms) this.#rooms.handle(ops, deliver);
|
|
272
|
+
// No room host: what asked for a room hears so (the engine refuses first while its host is none).
|
|
273
|
+
else deliver(ops.filter((o) => o["op"] === "create" || o["op"] === "join").map((o) => ({ type: "error", op: o["op"], code: "unavailable", message: "this page carries no rooms" })));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
253
276
|
if (m.type === "platform") {
|
|
254
277
|
const calls = (m as unknown as { calls: PlatformCall[] }).calls ?? [];
|
|
255
278
|
this.#platform.handle(calls, (request, answer) => worker.postMessage({ type: "platform-answer", request, answer }));
|
|
@@ -347,6 +370,7 @@ export class Shell {
|
|
|
347
370
|
locale: options.locale ?? globalThis.navigator?.language,
|
|
348
371
|
...(options.expect ? { expect: options.expect } : {}),
|
|
349
372
|
...(options.prime ? { prime: true } : {}),
|
|
373
|
+
...(options.trace ? { trace: true } : {}),
|
|
350
374
|
viewport: { width: canvas.clientWidth || canvas.width / dpr, height: canvas.clientHeight || canvas.height / dpr, dpr, coarsePointer: globalThis.matchMedia?.("(pointer: coarse)").matches ?? false, ...pagePreferences() },
|
|
351
375
|
},
|
|
352
376
|
[offscreen],
|
|
@@ -416,6 +440,27 @@ export class Shell {
|
|
|
416
440
|
}
|
|
417
441
|
|
|
418
442
|
/** Call the engine in the Worker: `advance`, `command`, `hash`, `frame`, or `probe` with a member name. */
|
|
443
|
+
/**
|
|
444
|
+
* The run's trace in the Chrome trace format, as `og run --trace` writes it natively (src/trace.ts):
|
|
445
|
+
* buffered in the engine Worker, taken here. `take` (the default) answers the document so far (null
|
|
446
|
+
* when no trace is recorded), `start` begins one (`ShellOptions.trace` starts one at the boot),
|
|
447
|
+
* `stop` ends it.
|
|
448
|
+
*/
|
|
449
|
+
trace(action: "take" | "start" | "stop" = "take"): Promise<TraceDocument | null | boolean> {
|
|
450
|
+
return this.call("trace", action) as Promise<TraceDocument | null | boolean>;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Saves the trace so far as a JSON file the browser downloads (`name`, default `trace.json`): open it in Perfetto. False when none is recorded. */
|
|
454
|
+
async saveTrace(name = "trace.json"): Promise<boolean> {
|
|
455
|
+
const doc = await this.trace("take");
|
|
456
|
+
if (!doc || typeof doc !== "object") return false;
|
|
457
|
+
const url = URL.createObjectURL(new Blob([JSON.stringify(doc)], { type: "application/json" }));
|
|
458
|
+
const a = Object.assign(document.createElement("a"), { href: url, download: name });
|
|
459
|
+
a.click();
|
|
460
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
|
|
419
464
|
call(method: string, ...args: unknown[]): Promise<unknown> {
|
|
420
465
|
if (this.#stopped) return Promise.reject(this.#stopped);
|
|
421
466
|
const id = this.#next++;
|
|
@@ -425,10 +470,23 @@ export class Shell {
|
|
|
425
470
|
});
|
|
426
471
|
}
|
|
427
472
|
|
|
473
|
+
/**
|
|
474
|
+
* What carries og.platform's rooms from now on (og.platform.roomInfo's `host`); null for
|
|
475
|
+
* none, where roomCreate and roomJoin are refused. A host already carrying a room keeps it
|
|
476
|
+
* until its `left`: the engine is told the new host only for its next room. `link` is the room
|
|
477
|
+
* a Play link named (`?room=<id>`, the play page passes it): the engine joins it once, itself,
|
|
478
|
+
* as soon as a host carries rooms (og.RoomJoined answers, as for roomJoin; with none it says
|
|
479
|
+
* this page carries no rooms), and og.platform.roomInfo's `link` names it.
|
|
480
|
+
*/
|
|
481
|
+
setRooms(host: RoomHost | null, link?: string): void {
|
|
482
|
+
this.#rooms = host;
|
|
483
|
+
this.worker.postMessage({ type: "room-host", host: host ? host.host : "none", ...(link ? { join: link } : {}) });
|
|
484
|
+
}
|
|
485
|
+
|
|
428
486
|
/** What answers og.platform's calls from now on (og.platform.info reports its host and capabilities). */
|
|
429
487
|
setPlatform(host: PlatformHost): void {
|
|
430
488
|
this.#platform = host;
|
|
431
|
-
this.worker.postMessage({ type: "platform-host", host: host.host, capabilities: host.capabilities });
|
|
489
|
+
this.worker.postMessage({ type: "platform-host", host: host.host, capabilities: host.capabilities, time: host.time });
|
|
432
490
|
}
|
|
433
491
|
|
|
434
492
|
/** What stopped the engine (a panic, an error, a hang), or null while it runs. */
|
package/src/web/worker.ts
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
// Worker's animation frames while the clock played, so isolated pages never
|
|
45
45
|
// answered.
|
|
46
46
|
|
|
47
|
-
import { createEngine, systemClock, type Engine, type GameToken } from "../runtime.ts";
|
|
47
|
+
import { boundHost, createEngine, systemClock, type Engine, type GameToken } from "../runtime.ts";
|
|
48
48
|
import type { PlatformAnswer, PlatformHost } from "../platform-local.ts";
|
|
49
49
|
import { loadWebHost, type WebGlue } from "./host.ts";
|
|
50
50
|
import { drainRing, stampAt, type InjectedInput } from "./input.ts";
|
|
@@ -53,6 +53,7 @@ import { readFrames, waitForFrames, writeFrame, type RpcRings } from "./rpc-ring
|
|
|
53
53
|
import { loadBasisTranscoder, transcodeKtx2, TEXTURE_FORMATS, type BasisTranscoder, type TextureFormat } from "./basis.ts";
|
|
54
54
|
import type { HostError } from "./shell.ts";
|
|
55
55
|
import { PlatformCounter, settle, SETTLE_FIRST_MS, SETTLE_MS } from "./settle.ts";
|
|
56
|
+
import { Trace, unixMs } from "../trace.ts";
|
|
56
57
|
|
|
57
58
|
interface Start {
|
|
58
59
|
type: "start";
|
|
@@ -92,6 +93,8 @@ interface Start {
|
|
|
92
93
|
* game's first screen (its UI is built by ticks), then the ticks that apply the platform's answers to it (settle.ts).
|
|
93
94
|
*/
|
|
94
95
|
prime?: boolean;
|
|
96
|
+
/** Record the run's trace from the boot on (src/trace.ts; the shell's `trace()` reads it). */
|
|
97
|
+
trace?: boolean;
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
type Viewport = { width: number; height: number; dpr: number; safeArea?: Record<string, number>; covered?: Record<string, number>; coarsePointer?: boolean; reducedMotion?: boolean; highContrast?: boolean; reducedTransparency?: boolean; macKeys?: boolean };
|
|
@@ -112,10 +115,27 @@ type Message =
|
|
|
112
115
|
| { type: "viewport"; viewport: Viewport }
|
|
113
116
|
| { type: "resize"; width: number; height: number; viewport: Viewport }
|
|
114
117
|
| { type: "platform-answer"; request: number; answer: PlatformAnswer }
|
|
115
|
-
| { type: "platform-host"; host: string; capabilities: string[] }
|
|
118
|
+
| { type: "platform-host"; host: string; capabilities: string[]; time?: number }
|
|
119
|
+
| { type: "room-events"; events: unknown[] }
|
|
120
|
+
| { type: "room-host"; host: string; join?: string }
|
|
116
121
|
| AudioMessage
|
|
117
122
|
| AudioLatencyMessage;
|
|
118
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Rooms (docs/design.md §2.8): the engine's room ops (og_room_take) go to the
|
|
126
|
+
* page as `room` messages, which the shell's room host carries (the Player's
|
|
127
|
+
* room.*, docs/platform-asks.md #16); the page's `room-events` come back
|
|
128
|
+
* through og_room_deliver, in order, and `room-host` names what carries them.
|
|
129
|
+
* Ops are taken after every frame and after events are delivered, so
|
|
130
|
+
* lockstep's inputs leave on the frame that made them.
|
|
131
|
+
*/
|
|
132
|
+
function pumpRoom(engine: Engine): void {
|
|
133
|
+
const host = boundHost();
|
|
134
|
+
if (!host) return;
|
|
135
|
+
const ops = JSON.parse(host.abi.og_room_take(engine.id)) as unknown[];
|
|
136
|
+
if (ops.length > 0) self.postMessage({ type: "room", ops });
|
|
137
|
+
}
|
|
138
|
+
|
|
119
139
|
/** og.platform's host in the Worker: the calls go to the page (counted), whose answers come back as `platform-answer`. */
|
|
120
140
|
function toPage(host: string, capabilities: readonly string[], counter: PlatformCounter): PlatformHost {
|
|
121
141
|
return {
|
|
@@ -159,12 +179,21 @@ export interface WebExtras {
|
|
|
159
179
|
og_web_memory?(engine: number): string;
|
|
160
180
|
/** Make one pipeline no frame used yet (og-render's Renderer::warm); false when none is left. */
|
|
161
181
|
og_web_warm?(engine: number): boolean;
|
|
182
|
+
/** `og render`'s every-frame capture (development builds): the display frame drawn last as RGBA8 rows at the canvas's size; empty until its readback lands. */
|
|
183
|
+
og_web_render_frame?(engine: number): Uint8Array;
|
|
162
184
|
/** The first Rust panic recorded (JSON: kind, message, location, tick, lane), or `null`. */
|
|
163
185
|
og_web_panic?(): string;
|
|
164
186
|
/** Test hook: the next item at a lanes site panics (in a lane when the call splits). */
|
|
165
187
|
og_web_lanes_fault?(site: string): boolean;
|
|
188
|
+
/** The trace (crates/og-runtime/src/trace.rs): on or off for an engine (0xffffffff: the loads before one exists). */
|
|
189
|
+
og_web_trace?(engine: number, on: boolean): void;
|
|
190
|
+
/** The trace's spans since the last take: `{ now, events }`, Chrome trace events on the engine's clock. */
|
|
191
|
+
og_web_trace_take?(engine: number, last: boolean): string;
|
|
166
192
|
}
|
|
167
193
|
|
|
194
|
+
/** The trace's key for loads made before the engine exists (og-runtime's `BEFORE_ENGINE`). */
|
|
195
|
+
const TRACE_BEFORE_ENGINE = 0xffffffff;
|
|
196
|
+
|
|
168
197
|
/** Where the runtime asks for a bundle's files: `<root>/cooked/<file>` (crates/og-runtime/src/assets.rs). */
|
|
169
198
|
export function cookedDir(root: string): string {
|
|
170
199
|
return `${root.replace(/\/+$/, "")}/cooked`;
|
|
@@ -514,6 +543,9 @@ export function startWorker(game: GameToken): void {
|
|
|
514
543
|
phaseAt = now;
|
|
515
544
|
};
|
|
516
545
|
const firstFrameMessage = () => ({ type: "frame", first: true, boot: { ...boot, at: Math.round(performance.now()) } });
|
|
546
|
+
/** What the trace's document records beside its events: the renderer's tier and the lanes. */
|
|
547
|
+
let lastTier = "none";
|
|
548
|
+
let lanesState: unknown = null;
|
|
517
549
|
let glueRef: (WebGlue & WebExtras & WebAudioGlue) | null = null;
|
|
518
550
|
let audio: WorkerAudio | null = null;
|
|
519
551
|
/** Captures waiting for their readback (requested on tick N, answered on N + 1). */
|
|
@@ -592,7 +624,21 @@ export function startWorker(game: GameToken): void {
|
|
|
592
624
|
};
|
|
593
625
|
|
|
594
626
|
/** What a frame's answer tells the shell: the UI's focused text control (text-input.ts), a copy, and the accessibility tree when it changed (a11y.ts). */
|
|
627
|
+
/** The run's trace while one is recorded (src/trace.ts): taken after every frame, so the engine holds a frame's spans at most. */
|
|
628
|
+
let trace: Trace | null = null;
|
|
629
|
+
const takeTrace = (last = false) => {
|
|
630
|
+
if (!trace || !engine || !glueRef?.og_web_trace_take) return;
|
|
631
|
+
trace.append(glueRef.og_web_trace_take(engine.id, last), unixMs());
|
|
632
|
+
};
|
|
633
|
+
const traceOn = (on: boolean) => {
|
|
634
|
+
if (!engine || !glueRef?.og_web_trace) return false;
|
|
635
|
+
if (on && !trace) trace = new Trace({ producer: "og web trace", process: "og web" });
|
|
636
|
+
if (!on) trace = null;
|
|
637
|
+
glueRef.og_web_trace(engine.id, on);
|
|
638
|
+
return true;
|
|
639
|
+
};
|
|
595
640
|
const relay = (answered: ReturnType<Engine["frame"]>) => {
|
|
641
|
+
takeTrace();
|
|
596
642
|
if (!answered.ok) return;
|
|
597
643
|
const target = JSON.stringify(answered.text ?? null);
|
|
598
644
|
if (target !== textTarget) {
|
|
@@ -676,6 +722,7 @@ export function startWorker(game: GameToken): void {
|
|
|
676
722
|
}
|
|
677
723
|
behind = answered.behind ?? 0;
|
|
678
724
|
relay(answered);
|
|
725
|
+
pumpRoom(engine);
|
|
679
726
|
if (typeof answered.tick === "number") lastTick = answered.tick;
|
|
680
727
|
if (!firstFrame && answered.rendered !== false) {
|
|
681
728
|
firstFrame = true;
|
|
@@ -777,11 +824,25 @@ export function startWorker(game: GameToken): void {
|
|
|
777
824
|
};
|
|
778
825
|
|
|
779
826
|
const calls: Record<string, (e: Engine, args: unknown[]) => unknown> = {
|
|
780
|
-
|
|
827
|
+
// A harness's ticks carry the room as the page's own frames do: lockstep's inputs leave with them.
|
|
828
|
+
advance: (e, [n]) => {
|
|
829
|
+
const answered = e.advance(Number(n));
|
|
830
|
+
pumpRoom(e);
|
|
831
|
+
return answered;
|
|
832
|
+
},
|
|
781
833
|
command: (e, [action, args]) => e.command(String(action), (args as string[]) ?? []),
|
|
782
834
|
hash: (e) => e.hash(),
|
|
783
835
|
/** What the engine holds (ProbeMemory: the wasm linear memory, the counted heap, the world, streaming, audio), in every build. */
|
|
784
836
|
memory: (e) => JSON.parse(glueRef?.og_web_memory?.(e.id) ?? "null"),
|
|
837
|
+
/**
|
|
838
|
+
* The display frame drawn last as RGBA8 rows (the probe's Capture::frame,
|
|
839
|
+
* `og render`'s every-frame capture), or null until its readback lands: the
|
|
840
|
+
* first call turns the keeping of frames on; ask again before the next frame.
|
|
841
|
+
*/
|
|
842
|
+
renderFrame: (e) => {
|
|
843
|
+
const rgba = glueRef?.og_web_render_frame?.(e.id);
|
|
844
|
+
return rgba && rgba.length > 0 ? rgba : null;
|
|
845
|
+
},
|
|
785
846
|
/** What the lanes did so far (crates/og-runtime/src/lanes.rs `report`). */
|
|
786
847
|
lanes: () => JSON.parse(glueRef?.og_web_lanes?.() ?? "null"),
|
|
787
848
|
/** Test hook: the next item at lanes site `site` panics (og_web_lanes_fault); false when the module has no such hook or site. */
|
|
@@ -789,8 +850,14 @@ export function startWorker(game: GameToken): void {
|
|
|
789
850
|
frame: (e, [ms]) => {
|
|
790
851
|
const answered = e.frame(Number(ms));
|
|
791
852
|
relay(answered);
|
|
853
|
+
pumpRoom(e);
|
|
792
854
|
return answered;
|
|
793
855
|
},
|
|
856
|
+
/** og.net on this page: og.net.status (lockstep running, its start tick, the seats, the first divergent tick) and the shared hash, as `og run --json` reports them natively (`lockstep`, `netHash`). */
|
|
857
|
+
net: (e) => {
|
|
858
|
+
const abi = boundHost()!.abi;
|
|
859
|
+
return { status: JSON.parse(abi.og_net_status(e.id)), hash: JSON.parse(abi.og_net_hash(e.id)), room: JSON.parse(abi.og_platform_room_info(e.id)) };
|
|
860
|
+
},
|
|
794
861
|
// The frames played since the last reset (at most FRAME_RECORDS, oldest first); `true` resets.
|
|
795
862
|
timings: (_, [reset]) => {
|
|
796
863
|
const n = Math.min(recorded, FRAME_RECORDS);
|
|
@@ -799,6 +866,22 @@ export function startWorker(game: GameToken): void {
|
|
|
799
866
|
if (reset) recorded = 0;
|
|
800
867
|
return answer;
|
|
801
868
|
},
|
|
869
|
+
/**
|
|
870
|
+
* The run's trace (src/trace.ts): `start` begins one (dropping any), `stop` ends it, `take` (the
|
|
871
|
+
* default) answers the document so far, a Chrome trace as `og run --trace` writes it; null when
|
|
872
|
+
* none is recorded or the module cannot trace.
|
|
873
|
+
*/
|
|
874
|
+
trace: (_, [action]) => {
|
|
875
|
+
const a = String(action ?? "take");
|
|
876
|
+
if (a === "start") {
|
|
877
|
+
traceOn(false);
|
|
878
|
+
return traceOn(true);
|
|
879
|
+
}
|
|
880
|
+
if (a === "stop") return traceOn(false);
|
|
881
|
+
if (!trace) return null;
|
|
882
|
+
takeTrace(true);
|
|
883
|
+
return trace.document({ tier: lastTier, lanes: lanesState });
|
|
884
|
+
},
|
|
802
885
|
probe: (e, [member, ...rest]) => {
|
|
803
886
|
const f = (e.probe as unknown as Record<string, unknown>)[String(member)];
|
|
804
887
|
if (typeof f !== "function") throw new TypeError(`probe has no member '${String(member)}'`);
|
|
@@ -870,6 +953,8 @@ export function startWorker(game: GameToken): void {
|
|
|
870
953
|
installFetcher(glue, fetched.manifest ?? "", data.basis, () => (rendering && engine ? engine.id : null));
|
|
871
954
|
}
|
|
872
955
|
phase("bundle");
|
|
956
|
+
// A trace from the boot on records the boot scene's loads too.
|
|
957
|
+
if (data.trace) glue.og_web_trace?.(TRACE_BEFORE_ENGINE, true);
|
|
873
958
|
const created = createEngine(game, {
|
|
874
959
|
...(data.seed === undefined ? {} : { seed: data.seed }),
|
|
875
960
|
...(data.root === undefined ? {} : { root: data.root }),
|
|
@@ -881,6 +966,10 @@ export function startWorker(game: GameToken): void {
|
|
|
881
966
|
});
|
|
882
967
|
if (!created.ok) throw new Error(created.reason);
|
|
883
968
|
engine = created.value;
|
|
969
|
+
if (data.trace) {
|
|
970
|
+
traceOn(true);
|
|
971
|
+
glue.og_web_trace?.(TRACE_BEFORE_ENGINE, false);
|
|
972
|
+
}
|
|
884
973
|
// og.platform: calls go to the page, which answers them (the Player, or the browser's storage).
|
|
885
974
|
// The time is the browser's until the Player's answers carry its own (docs/platform-asks.md).
|
|
886
975
|
engine.setClock(systemClock("browser"));
|
|
@@ -896,6 +985,8 @@ export function startWorker(game: GameToken): void {
|
|
|
896
985
|
rendering = true;
|
|
897
986
|
}
|
|
898
987
|
phase("attach");
|
|
988
|
+
lastTier = tier;
|
|
989
|
+
lanesState = lanes;
|
|
899
990
|
// The textures go to the renderer's encodings, so after it is attached (the game starts later).
|
|
900
991
|
const loaded = fetched ? await transcodeBundle(glue, rendering ? engine.id : null, fetched, data.basis, progress, data.expect ? received : undefined) : null;
|
|
901
992
|
phase("transcode");
|
|
@@ -982,7 +1073,17 @@ export function startWorker(game: GameToken): void {
|
|
|
982
1073
|
engine.answerPlatform(data.request, data.answer);
|
|
983
1074
|
platformCounter.answered();
|
|
984
1075
|
}
|
|
985
|
-
if (data.type === "platform-host") engine.attachPlatform(toPage(data.host, data.capabilities, platformCounter));
|
|
1076
|
+
if (data.type === "platform-host") engine.attachPlatform({ ...toPage(data.host, data.capabilities, platformCounter), time: data.time });
|
|
1077
|
+
if (data.type === "room-host") {
|
|
1078
|
+
boundHost()?.abi.og_room_set_host(engine.id, JSON.stringify({ host: data.host, ...(data.join ? { join: data.join } : {}) }));
|
|
1079
|
+
// A Play link's join leaves now, not with the next frame (the page may be behind its gate).
|
|
1080
|
+
pumpRoom(engine);
|
|
1081
|
+
}
|
|
1082
|
+
if (data.type === "room-events") {
|
|
1083
|
+
const abi = boundHost()?.abi;
|
|
1084
|
+
for (const event of data.events) abi?.og_room_deliver(engine.id, JSON.stringify(event));
|
|
1085
|
+
pumpRoom(engine);
|
|
1086
|
+
}
|
|
986
1087
|
if (data.type === "play") {
|
|
987
1088
|
playing = data.on;
|
|
988
1089
|
last = 0;
|