@origingame/engine 0.3.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/dist/web/origin-engine.d.ts +38 -2
- package/dist/web/origin-engine.js +250 -10
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +14 -2
- package/dist/web-publish/origin-engine-lanes.js +255 -34
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +39 -3
- package/dist/web-publish/origin-engine.js +253 -32
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +15 -3
- package/dist/web-publish-2d/origin-engine-lanes.js +253 -32
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +38 -2
- package/dist/web-publish-2d/origin-engine.js +250 -29
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +14 -2
- package/knowledge/README.md +2 -1
- package/knowledge/guides/three-to-og.md +393 -0
- package/knowledge/knowledge.json +2 -1
- 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 +140 -21
- package/knowledge/skills/og-audio/SKILL.md +2 -2
- package/knowledge/skills/og-build/SKILL.md +22 -3
- package/knowledge/skills/og-core/SKILL.md +20 -5
- package/knowledge/skills/og-feel/SKILL.md +2 -2
- package/knowledge/skills/og-game/SKILL.md +481 -35
- 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 +1 -1
- package/knowledge/skills/og-ui/SKILL.md +15 -8
- package/knowledge/wiki/ai.md +20 -20
- package/knowledge/wiki/anim.md +63 -55
- package/knowledge/wiki/atmosphere.md +13 -13
- package/knowledge/wiki/audio.md +18 -18
- 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 +57 -3
- package/knowledge/wiki/input.md +47 -47
- package/knowledge/wiki/load.md +57 -57
- 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 +95 -95
- package/knowledge/wiki/platform.md +44 -44
- package/knowledge/wiki/probe.md +86 -86
- package/knowledge/wiki/reflect.md +16 -4
- package/knowledge/wiki/render.md +1168 -170
- 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 +1834 -305
- package/shell/platform.ts +5 -0
- package/shell/player.ts +8 -2
- package/shell/transport.ts +6 -0
- package/src/generated/abi.ts +37 -1
- package/src/geom.ts +260 -0
- package/src/index.ts +8 -1
- package/src/node/playtest-run.ts +1 -0
- package/src/platform-local.ts +2 -0
- package/src/playtest.ts +8 -0
- package/src/runtime.ts +87 -6
- package/src/ui-kit/settings.ts +9 -0
- package/src/web/shell.ts +1 -1
- package/src/web/worker.ts +2 -2
- package/types/bindings.json +697 -402
- package/types/index.d.ts +1045 -127
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))),
|
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) {
|
package/src/web/shell.ts
CHANGED
|
@@ -486,7 +486,7 @@ export class Shell {
|
|
|
486
486
|
/** What answers og.platform's calls from now on (og.platform.info reports its host and capabilities). */
|
|
487
487
|
setPlatform(host: PlatformHost): void {
|
|
488
488
|
this.#platform = host;
|
|
489
|
-
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 });
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
/** What stopped the engine (a panic, an error, a hang), or null while it runs. */
|
package/src/web/worker.ts
CHANGED
|
@@ -115,7 +115,7 @@ type Message =
|
|
|
115
115
|
| { type: "viewport"; viewport: Viewport }
|
|
116
116
|
| { type: "resize"; width: number; height: number; viewport: Viewport }
|
|
117
117
|
| { type: "platform-answer"; request: number; answer: PlatformAnswer }
|
|
118
|
-
| { type: "platform-host"; host: string; capabilities: string[] }
|
|
118
|
+
| { type: "platform-host"; host: string; capabilities: string[]; time?: number }
|
|
119
119
|
| { type: "room-events"; events: unknown[] }
|
|
120
120
|
| { type: "room-host"; host: string; join?: string }
|
|
121
121
|
| AudioMessage
|
|
@@ -1073,7 +1073,7 @@ export function startWorker(game: GameToken): void {
|
|
|
1073
1073
|
engine.answerPlatform(data.request, data.answer);
|
|
1074
1074
|
platformCounter.answered();
|
|
1075
1075
|
}
|
|
1076
|
-
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
1077
|
if (data.type === "room-host") {
|
|
1078
1078
|
boundHost()?.abi.og_room_set_host(engine.id, JSON.stringify({ host: data.host, ...(data.join ? { join: data.join } : {}) }));
|
|
1079
1079
|
// A Play link's join leaves now, not with the next frame (the page may be behind its gate).
|