@origingame/engine 0.3.0 → 0.5.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/dist/web/origin-engine.d.ts +44 -5
- package/dist/web/origin-engine.js +279 -19
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +18 -5
- package/dist/web-publish/origin-engine-lanes.js +289 -48
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +44 -5
- package/dist/web-publish/origin-engine.js +279 -38
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +18 -5
- package/dist/web-publish-2d/origin-engine-lanes.js +289 -48
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +44 -5
- package/dist/web-publish-2d/origin-engine.js +279 -38
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +18 -5
- 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 +123 -2
- package/knowledge/package.json +3 -2
- package/knowledge/scaffold/origin-engine.json +2 -2
- package/knowledge/scaffold/template/package.json +1 -1
- package/knowledge/scaffold/template/project.ogt +1 -1
- package/knowledge/scripts/validate.ts +209 -27
- package/knowledge/skills/og-audio/SKILL.md +33 -7
- package/knowledge/skills/og-build/SKILL.md +66 -7
- package/knowledge/skills/og-core/SKILL.md +24 -8
- package/knowledge/skills/og-feel/SKILL.md +11 -3
- package/knowledge/skills/og-game/SKILL.md +680 -51
- 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 +7 -1
- package/knowledge/skills/og-taste/SKILL.md +7 -0
- package/knowledge/skills/og-ui/SKILL.md +39 -8
- package/knowledge/wiki/ai.md +20 -20
- package/knowledge/wiki/anim.md +537 -63
- package/knowledge/wiki/atmosphere.md +13 -13
- package/knowledge/wiki/audio.md +160 -20
- package/knowledge/wiki/camera.md +13 -13
- package/knowledge/wiki/commands.md +11 -11
- package/knowledge/wiki/debug.md +13 -13
- package/knowledge/wiki/game.md +8 -8
- package/knowledge/wiki/i18n.md +7 -7
- package/knowledge/wiki/index.md +81 -3
- package/knowledge/wiki/input.md +47 -47
- package/knowledge/wiki/load.md +83 -57
- package/knowledge/wiki/math.md +102 -102
- package/knowledge/wiki/nav.md +34 -34
- package/knowledge/wiki/net.md +10 -10
- package/knowledge/wiki/path.md +14 -14
- package/knowledge/wiki/physics.md +111 -100
- package/knowledge/wiki/platform.md +44 -44
- package/knowledge/wiki/probe.md +86 -86
- package/knowledge/wiki/reflect.md +30 -18
- package/knowledge/wiki/render.md +1243 -176
- 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 +14 -14
- package/knowledge/wiki/transform.md +8 -8
- package/knowledge/wiki/tween.md +17 -17
- package/knowledge/wiki/ui.md +245 -269
- package/knowledge/wiki/video.md +5 -5
- package/knowledge/wiki/world.md +36 -36
- package/package.json +4 -4
- package/schema/api.schema.json +2238 -81
- package/shell/page.ts +4 -0
- package/shell/platform.ts +5 -0
- package/shell/player.ts +8 -2
- package/shell/transport.ts +6 -0
- package/src/generated/abi.ts +40 -1
- package/src/geom.ts +260 -0
- package/src/index.ts +19 -1
- package/src/node/device.ts +5 -2
- package/src/node/playtest-run.ts +1 -0
- package/src/node/polish.ts +34 -15
- package/src/node/run.ts +23 -6
- package/src/platform-local.ts +2 -0
- package/src/playtest.ts +8 -0
- package/src/runtime.ts +88 -6
- package/src/ui-kit/settings.ts +9 -0
- package/src/web/gl-state.ts +907 -0
- package/src/web/shell.ts +1 -1
- package/src/web/worker.ts +8 -2
- package/types/bindings.json +1222 -453
- package/types/index.d.ts +1868 -315
package/src/node/polish.ts
CHANGED
|
@@ -611,7 +611,8 @@ interface PlaytestMeasures {
|
|
|
611
611
|
hud: { gameplayShots: number; worstShare: number; worstCentre: number };
|
|
612
612
|
ui: { screenChanges: number; withMotion: number; presses: number; pressesWithSound: number; motionTicks: number };
|
|
613
613
|
latency: { inputs: number; answered: number; medianTicks: number; maxTicks: number; baselineShare: number; sound: number };
|
|
614
|
-
|
|
614
|
+
/** Per signal, the moments that had it; `whole`, the moments with three of the four (hit stop, shake, floating text, a sound: og-feel §7's rule); `credit`, each moment's min(3, its signals) summed, what the score reads. */
|
|
615
|
+
damage: { moments: number; hitStop: number; shake: number; floating: number; sound: number; flash: number; particles: number; whole: number; credit: number; facts: string[] };
|
|
615
616
|
idle: { share: number };
|
|
616
617
|
particles: { peakSystems: number; bursts: number };
|
|
617
618
|
/** `rendered` false: no renderer (tier `none`), so render and frame time were not measured. */
|
|
@@ -757,22 +758,39 @@ function measure(rec: RunRecord, frameDir: string, frames: number, relTo: string
|
|
|
757
758
|
const tick = rec.facts[i]!.tick;
|
|
758
759
|
if (hit && (moments.length === 0 || tick - moments.at(-1)! > 3)) moments.push(tick);
|
|
759
760
|
}
|
|
760
|
-
const dmg = { moments: moments.length, hitStop: 0, shake: 0, floating: 0, sound: 0, flash: 0, particles: 0, facts: [...factKeys].slice(0, 6) };
|
|
761
|
+
const dmg = { moments: moments.length, hitStop: 0, shake: 0, floating: 0, sound: 0, flash: 0, particles: 0, whole: 0, credit: 0, facts: [...factKeys].slice(0, 6) };
|
|
762
|
+
// The four signals an impact is held to, in the order a finding names them: three of four make it whole.
|
|
763
|
+
const FOUR = ["hit stop", "shake", "floating text", "a sound"] as const;
|
|
764
|
+
const FIX: Record<(typeof FOUR)[number], string> = { "hit stop": "og-feel §3 (hit stop by impact weight)", shake: "og-feel §4 (trauma, not offsets)", "floating text": "og-feel §7 (floating text)", "a sound": "og-audio §1 (every event sounds; a hit's sound on its tick)" };
|
|
765
|
+
const lacking = new Map<(typeof FOUR)[number], number>(FOUR.map((k) => [k, 0]));
|
|
766
|
+
let short = 0;
|
|
761
767
|
for (const t of moments) {
|
|
762
768
|
const i = tickIndex.get(t) ?? -1;
|
|
763
|
-
|
|
764
|
-
const w = sig.slice(Math.max(1, i - 3), Math.min(s.length, i + 9));
|
|
765
|
-
const held = s.slice(Math.max(0, i - 3), Math.min(s.length, i + 9)).some((p) => p.hitStop > 0 || p.scale < 1);
|
|
766
|
-
|
|
767
|
-
if (
|
|
768
|
-
if (
|
|
769
|
-
if (
|
|
769
|
+
// A moment the record cannot place is short of all four.
|
|
770
|
+
const w = i < 0 ? [] : sig.slice(Math.max(1, i - 3), Math.min(s.length, i + 9));
|
|
771
|
+
const held = i >= 0 && s.slice(Math.max(0, i - 3), Math.min(s.length, i + 9)).some((p) => p.hitStop > 0 || p.scale < 1);
|
|
772
|
+
const has = { "hit stop": held || w.some((x) => x.hitStop), shake: w.some((x) => x.shake), "floating text": w.some((x) => x.floating), "a sound": w.some((x) => x.sfx) };
|
|
773
|
+
if (has["hit stop"]) dmg.hitStop++;
|
|
774
|
+
if (has.shake) dmg.shake++;
|
|
775
|
+
if (has["floating text"]) dmg.floating++;
|
|
776
|
+
if (has["a sound"]) dmg.sound++;
|
|
770
777
|
if (w.some((x) => x.tween)) dmg.flash++;
|
|
771
778
|
if (w.some((x) => x.particles)) dmg.particles++;
|
|
779
|
+
const n = FOUR.filter((k) => has[k]).length;
|
|
780
|
+
dmg.credit += Math.min(3, n);
|
|
781
|
+
if (n >= 3) {
|
|
782
|
+
dmg.whole++;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
// Short of three: each signal it lacks is owed. A whole moment owes none,
|
|
786
|
+
// so a party game's bump with hit stop, shake and a sound is not asked
|
|
787
|
+
// for a floating number (og-feel §7: damage and score numbers).
|
|
788
|
+
short++;
|
|
789
|
+
for (const k of FOUR) if (!has[k]) lacking.set(k, lacking.get(k)! + 1);
|
|
772
790
|
}
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
791
|
+
for (const k of FOUR) {
|
|
792
|
+
const n = lacking.get(k)!;
|
|
793
|
+
if (n > 0) findings.push({ measure: "hit feedback", where: rec.playtest, detail: `${n} of ${dmg.moments} damage moments (${dmg.facts.join(", ")}) had no ${k} within 8 ticks, and fewer than three of hit stop, shake, floating text and a sound (${short} short)`, fix: FIX[k] });
|
|
776
794
|
}
|
|
777
795
|
|
|
778
796
|
// Idle frames, particles, frame time.
|
|
@@ -844,7 +862,8 @@ function scores(ms: PlaytestMeasures[], missingKeys: number, hasLocales: boolean
|
|
|
844
862
|
const lat = ms.filter((m) => m.latency.inputs > 0);
|
|
845
863
|
const latency = lat.length === 0 ? null : Math.round(lat.reduce((n, m) => n + (m.latency.answered / m.latency.inputs) * (m.latency.medianTicks <= 1 ? 100 : m.latency.medianTicks <= 3 ? 70 : m.latency.medianTicks <= 6 ? 40 : 0), 0) / lat.length);
|
|
846
864
|
const dm = sum((m) => m.damage.moments);
|
|
847
|
-
|
|
865
|
+
// Per impact: each moment scores min(3, its signals) of 3, so three of four is 100 and no moment's lack hides behind another's signal.
|
|
866
|
+
const hitFeedback = dm === 0 ? null : Math.round((100 * sum((m) => m.damage.credit)) / (3 * dm));
|
|
848
867
|
const gameplay = ms.filter((m) => m.hud.gameplayShots > 0);
|
|
849
868
|
const hud = gameplay.length === 0 ? null : Math.min(...gameplay.map((m) => Math.min(band(m.hud.worstShare, 15, 45), band(m.hud.worstCentre, 5, 30))));
|
|
850
869
|
const timed = ms.filter((m) => m.frame.rendered);
|
|
@@ -894,7 +913,7 @@ function markdown(project: string, ms: PlaytestMeasures[], sc: Scores, findings:
|
|
|
894
913
|
L.push(`| Locale keys | ${show(sc.locale)} | string tables without missing keys (og cook) |`);
|
|
895
914
|
L.push(`| **Feel** | **${show(sc.feel)}** | mean of the feel rows |`);
|
|
896
915
|
L.push(`| Input latency | ${show(sc.latency)} | inputs answered, by the median ticks to the answer (0–1: 100) |`);
|
|
897
|
-
L.push(`| Hit feedback | ${show(sc.hitFeedback)} | damage
|
|
916
|
+
L.push(`| Hit feedback | ${show(sc.hitFeedback)} | each damage moment with three of hit stop, shake, floating text and a sound (every moment so: 100) |`);
|
|
898
917
|
L.push(`| Idle frames | ${show(sc.idle)} | ticks where nothing new is seen or heard (10 %: 100, 60 %: 0) |`);
|
|
899
918
|
L.push(`| Frame time | ${sc.frameTime === null && ms.length > 0 && ms.every((m) => !m.frame.rendered) ? "n/a (no renderer)" : show(sc.frameTime)} | p95 step + render (8.3 ms: 100, 33 ms: 0), worst playtest${ms.some((m) => !m.frame.rendered) ? "; no renderer: not measured" : ""} |`);
|
|
900
919
|
L.push(`| Sound on input | ${show(sc.inputSound)} | inputs answered by a sound within 6 ticks |`, "");
|
|
@@ -920,7 +939,7 @@ function markdown(project: string, ms: PlaytestMeasures[], sc: Scores, findings:
|
|
|
920
939
|
L.push(`| HUD | ${m.hud.gameplayShots} gameplay frame(s), up to ${m.hud.worstShare} % of the surface, ${m.hud.worstCentre} % of the centre |`);
|
|
921
940
|
L.push(`| UI | ${m.ui.screenChanges} screen change(s), ${m.ui.withMotion} with motion; ${m.ui.motionTicks} ticks of UI motion; ${m.ui.presses} press(es), ${m.ui.pressesWithSound} with a ui-bus sound |`);
|
|
922
941
|
L.push(`| Inputs | ${m.latency.inputs}, ${m.latency.answered} answered, median ${m.latency.medianTicks} ticks, max ${m.latency.maxTicks}; ${m.latency.sound} with a sound; ${m.latency.baselineShare} % of input-free ticks also change something |`);
|
|
923
|
-
L.push(`| Damage | ${m.damage.moments} moment(s)${m.damage.facts.length ? ` from ${m.damage.facts.map((f) => `\`${f}\``).join(", ")}` : ""}: hit stop ${m.damage.hitStop}, shake ${m.damage.shake}, floating text ${m.damage.floating}, sound ${m.damage.sound}, tween ${m.damage.flash}, particles ${m.damage.particles} |`);
|
|
942
|
+
L.push(`| Damage | ${m.damage.moments} moment(s)${m.damage.facts.length ? ` from ${m.damage.facts.map((f) => `\`${f}\``).join(", ")}` : ""}: hit stop ${m.damage.hitStop}, shake ${m.damage.shake}, floating text ${m.damage.floating}, sound ${m.damage.sound} (three of four: ${m.damage.whole}), tween ${m.damage.flash}, particles ${m.damage.particles} |`);
|
|
924
943
|
L.push(`| Idle | ${m.idle.share} % of ticks |`);
|
|
925
944
|
L.push(`| Particles | peak ${m.particles.peakSystems} system(s), ${m.particles.bursts} burst(s) |`);
|
|
926
945
|
if (m.frame.rendered) L.push(`| Frame (${m.frame.tier}) | step ${m.frame.stepMedianMs} ms median, ${m.frame.stepP95Ms} p95; render ${m.frame.renderMedianMs} median, ${m.frame.renderP95Ms} p95; frame p95 ${m.frame.frameP95Ms}, max ${m.frame.frameMaxMs} |`);
|
package/src/node/run.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { loadNativeHost, native } from "./index.ts";
|
|
|
44
44
|
import { openAudio, type AudioSink } from "./audio.ts";
|
|
45
45
|
import { nativePlatform } from "./platform.ts";
|
|
46
46
|
import { Trace, unixMs, type TraceDocument } from "../trace.ts";
|
|
47
|
-
import { beginBundle, boundHost, createEngine, fixedClock, type Engine, type GameToken } from "../runtime.ts";
|
|
47
|
+
import { beginBundle, boundHost, createEngine, fixedClock, type Engine, type GameToken, type Result } from "../runtime.ts";
|
|
48
48
|
|
|
49
49
|
const argv = process.argv.slice(2);
|
|
50
50
|
let entry = "";
|
|
@@ -298,7 +298,7 @@ if (!probing) {
|
|
|
298
298
|
if (!advanced.ok) fail(advanced.reason);
|
|
299
299
|
sink.pump();
|
|
300
300
|
} else {
|
|
301
|
-
let pendingReload: { mtimeMs: number } | null = null;
|
|
301
|
+
let pendingReload: { mtimeMs: number; importMs: number } | null = null;
|
|
302
302
|
// Imports in flight: a reload swaps only once the last one has declared
|
|
303
303
|
// everything (a second write of the bundle starts another import, whose
|
|
304
304
|
// beginBundle clears the declarations the first one made).
|
|
@@ -313,9 +313,10 @@ if (!probing) {
|
|
|
313
313
|
const mtimeMs = statSync(watchFile!).mtimeMs;
|
|
314
314
|
importing++;
|
|
315
315
|
try {
|
|
316
|
+
const started = performance.now();
|
|
316
317
|
beginBundle();
|
|
317
318
|
await importGame(watchFile!);
|
|
318
|
-
pendingReload = { mtimeMs };
|
|
319
|
+
pendingReload = { mtimeMs, importMs: performance.now() - started };
|
|
319
320
|
} catch (e) {
|
|
320
321
|
process.stderr.write(`WARN reload: the bundle failed to import (${e instanceof Error ? e.message : String(e)}); the old one keeps running\n`);
|
|
321
322
|
} finally {
|
|
@@ -328,13 +329,29 @@ if (!probing) {
|
|
|
328
329
|
let last = performance.now();
|
|
329
330
|
for (;;) {
|
|
330
331
|
if (pendingReload && importing === 0) {
|
|
331
|
-
const { mtimeMs } = pendingReload as { mtimeMs: number };
|
|
332
|
+
const { mtimeMs, importMs } = pendingReload as { mtimeMs: number; importMs: number };
|
|
332
333
|
pendingReload = null;
|
|
334
|
+
const restoring = performance.now();
|
|
333
335
|
const report = engine.probe.reload() as { ms: number; tick: number } | null;
|
|
336
|
+
const restored = performance.now();
|
|
334
337
|
const first = engine.frame(0);
|
|
335
|
-
const
|
|
338
|
+
const submitted = performance.now();
|
|
339
|
+
let rendered = false;
|
|
340
|
+
if (first.rendered) {
|
|
341
|
+
// Measure the completed first frame, without rendering and encoding
|
|
342
|
+
// an unused second frame through probe.capture.
|
|
343
|
+
const waited: Result<void> = JSON.parse(extras.og_node_wait_gpu(engine.id));
|
|
344
|
+
if (!waited.ok) fail(`reload's first frame: ${waited.reason}`);
|
|
345
|
+
rendered = true;
|
|
346
|
+
}
|
|
347
|
+
const timings = {
|
|
348
|
+
importMs,
|
|
349
|
+
reloadMs: restored - restoring,
|
|
350
|
+
frameMs: submitted - restored,
|
|
351
|
+
gpuWaitMs: performance.now() - submitted,
|
|
352
|
+
};
|
|
336
353
|
const latencyMs = Date.now() - mtimeMs;
|
|
337
|
-
process.stdout.write(JSON.stringify({ event: "reload", ok: report !== null, report, tick: first.tick,
|
|
354
|
+
process.stdout.write(JSON.stringify({ event: "reload", ok: report !== null && first.ok, report, tick: first.tick, rendered, timings, latencyMs: +latencyMs.toFixed(1) }) + "\n");
|
|
338
355
|
drain();
|
|
339
356
|
}
|
|
340
357
|
const elapsed = realtime ? performance.now() - last : tickMs;
|
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
|
@@ -127,6 +127,14 @@ export interface PlaytestDesc {
|
|
|
127
127
|
readonly seed?: number;
|
|
128
128
|
/** The render target, `WIDTHxHEIGHT` (default 1280x720). */
|
|
129
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;
|
|
130
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. */
|
|
131
139
|
readonly runs?: number;
|
|
132
140
|
/**
|
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). */
|
|
@@ -1335,6 +1415,7 @@ export const render = Object.freeze({
|
|
|
1335
1415
|
parse(abi().og_render_update_material(world.id, material, json(desc))),
|
|
1336
1416
|
capabilities: (world: WorldImpl) => parse(abi().og_render_capabilities(world.id)),
|
|
1337
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)),
|
|
1338
1419
|
look: (world: WorldImpl) => parse(abi().og_render_look(world.id)),
|
|
1339
1420
|
text: (world: WorldImpl, desc?: unknown) => parse(abi().og_render_text(world.id, json(desc))),
|
|
1340
1421
|
sprite: (world: WorldImpl, desc?: unknown) => parse(abi().og_render_sprite(world.id, json(desc))),
|
|
@@ -1389,6 +1470,7 @@ export const anim = Object.freeze({
|
|
|
1389
1470
|
graph: (world: WorldImpl, desc?: unknown) => parse(abi().og_anim_graph(world.id, json(desc))),
|
|
1390
1471
|
humanoid: (world: WorldImpl, desc?: unknown) => parse(abi().og_anim_humanoid(world.id, json(desc))),
|
|
1391
1472
|
retarget: (world: WorldImpl, desc?: unknown) => parse(abi().og_anim_retarget(world.id, json(desc))),
|
|
1473
|
+
mirror: (world: WorldImpl, desc?: unknown) => parse(abi().og_anim_mirror(world.id, json(desc))),
|
|
1392
1474
|
rig2dInfo: (world: WorldImpl, rig: number) => parse(abi().og_anim_rig2d_info(world.id, rig)),
|
|
1393
1475
|
skeleton2dBoxes: (world: WorldImpl, entity: number) => parse(abi().og_anim_skeleton2d_boxes(world.id, entity)),
|
|
1394
1476
|
// One lookup for a flipbook's or a 2D rig's clips (an index, -1 when
|
package/src/ui-kit/settings.ts
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
// game's GameSettings. The game owns the values (a resource, a save); the
|
|
3
3
|
// screen answers the new ones every tick and calls the hooks: onChange on
|
|
4
4
|
// every change (apply it at once), onSave when it closes after changes.
|
|
5
|
+
// A new graphics preset is applied to SettingsDesc.world by the screen
|
|
6
|
+
// itself (og.render.applyQuality) before onChange.
|
|
5
7
|
|
|
6
8
|
import type * as Api from "@origingame/engine";
|
|
9
|
+
import { render } from "../runtime.ts";
|
|
7
10
|
import { kit } from "./bind.ts";
|
|
8
11
|
import { frame, grow, px, token } from "./util.ts";
|
|
9
12
|
|
|
@@ -119,6 +122,12 @@ export function settingsScreen(id: string, desc: Api.SettingsDesc = {}): Api.Set
|
|
|
119
122
|
const changed = JSON.stringify(s) !== JSON.stringify(before);
|
|
120
123
|
if (changed) {
|
|
121
124
|
dirty = true;
|
|
125
|
+
// The preset is RenderSettings' quality fields (og.render.applyQuality):
|
|
126
|
+
// the next frame draws it, and the probe reads what is drawn.
|
|
127
|
+
if (s.quality !== before.quality && desc.world) {
|
|
128
|
+
const r = render.applyQuality(desc.world as unknown as Parameters<typeof render.applyQuality>[0], s.quality) as Api.Result<void>;
|
|
129
|
+
if (!r.ok) throw new Error(`og.ui.settingsScreen: ${r.reason}`);
|
|
130
|
+
}
|
|
122
131
|
desc.onChange?.(s);
|
|
123
132
|
}
|
|
124
133
|
if (closed && dirty) {
|