@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.
Files changed (84) hide show
  1. package/dist/web/origin-engine.d.ts +38 -2
  2. package/dist/web/origin-engine.js +250 -10
  3. package/dist/web/origin-engine_bg.wasm +0 -0
  4. package/dist/web/origin-engine_bg.wasm.d.ts +14 -2
  5. package/dist/web-publish/origin-engine-lanes.js +255 -34
  6. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  7. package/dist/web-publish/origin-engine.d.ts +39 -3
  8. package/dist/web-publish/origin-engine.js +253 -32
  9. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  10. package/dist/web-publish/origin-engine_bg.wasm.d.ts +15 -3
  11. package/dist/web-publish-2d/origin-engine-lanes.js +253 -32
  12. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  13. package/dist/web-publish-2d/origin-engine.d.ts +38 -2
  14. package/dist/web-publish-2d/origin-engine.js +250 -29
  15. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  16. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +14 -2
  17. package/knowledge/README.md +2 -1
  18. package/knowledge/guides/three-to-og.md +393 -0
  19. package/knowledge/knowledge.json +2 -1
  20. package/knowledge/package.json +3 -2
  21. package/knowledge/scaffold/origin-engine.json +2 -2
  22. package/knowledge/scaffold/template/package.json +1 -1
  23. package/knowledge/scaffold/template/project.ogt +1 -1
  24. package/knowledge/scripts/validate.ts +140 -21
  25. package/knowledge/skills/og-audio/SKILL.md +2 -2
  26. package/knowledge/skills/og-build/SKILL.md +22 -3
  27. package/knowledge/skills/og-core/SKILL.md +20 -5
  28. package/knowledge/skills/og-feel/SKILL.md +2 -2
  29. package/knowledge/skills/og-game/SKILL.md +481 -35
  30. package/knowledge/skills/og-i18n/SKILL.md +7 -7
  31. package/knowledge/skills/og-model/SKILL.md +374 -0
  32. package/knowledge/skills/og-platform-sdk/SKILL.md +1 -1
  33. package/knowledge/skills/og-ui/SKILL.md +15 -8
  34. package/knowledge/wiki/ai.md +20 -20
  35. package/knowledge/wiki/anim.md +63 -55
  36. package/knowledge/wiki/atmosphere.md +13 -13
  37. package/knowledge/wiki/audio.md +18 -18
  38. package/knowledge/wiki/camera.md +13 -13
  39. package/knowledge/wiki/commands.md +11 -11
  40. package/knowledge/wiki/debug.md +13 -13
  41. package/knowledge/wiki/game.md +8 -8
  42. package/knowledge/wiki/i18n.md +7 -7
  43. package/knowledge/wiki/index.md +57 -3
  44. package/knowledge/wiki/input.md +47 -47
  45. package/knowledge/wiki/load.md +57 -57
  46. package/knowledge/wiki/nav.md +34 -34
  47. package/knowledge/wiki/net.md +10 -10
  48. package/knowledge/wiki/path.md +14 -14
  49. package/knowledge/wiki/physics.md +95 -95
  50. package/knowledge/wiki/platform.md +44 -44
  51. package/knowledge/wiki/probe.md +86 -86
  52. package/knowledge/wiki/reflect.md +16 -4
  53. package/knowledge/wiki/render.md +1168 -170
  54. package/knowledge/wiki/rng.md +14 -14
  55. package/knowledge/wiki/shapes.md +727 -0
  56. package/knowledge/wiki/stream.md +15 -15
  57. package/knowledge/wiki/system.md +8 -8
  58. package/knowledge/wiki/systems.md +2 -2
  59. package/knowledge/wiki/terrain.md +24 -20
  60. package/knowledge/wiki/tilemap.md +13 -13
  61. package/knowledge/wiki/time.md +4 -4
  62. package/knowledge/wiki/timeline.md +14 -14
  63. package/knowledge/wiki/transform.md +8 -8
  64. package/knowledge/wiki/tween.md +17 -17
  65. package/knowledge/wiki/ui.md +245 -269
  66. package/knowledge/wiki/video.md +5 -5
  67. package/knowledge/wiki/world.md +36 -36
  68. package/package.json +4 -4
  69. package/schema/api.schema.json +1834 -305
  70. package/shell/platform.ts +5 -0
  71. package/shell/player.ts +8 -2
  72. package/shell/transport.ts +6 -0
  73. package/src/generated/abi.ts +37 -1
  74. package/src/geom.ts +260 -0
  75. package/src/index.ts +8 -1
  76. package/src/node/playtest-run.ts +1 -0
  77. package/src/platform-local.ts +2 -0
  78. package/src/playtest.ts +8 -0
  79. package/src/runtime.ts +87 -6
  80. package/src/ui-kit/settings.ts +9 -0
  81. package/src/web/shell.ts +1 -1
  82. package/src/web/worker.ts +2 -2
  83. package/types/bindings.json +697 -402
  84. 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
- function builtinFields(fields: ReadonlyArray<{ name: string; column?: string; type?: string; default?: unknown }>): Record<string, FieldSpec> {
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) out[f.name] = Object.freeze({ kind: f.column ?? f.type ?? "", default: f.default, options: Object.freeze({}) });
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<{ name: string; column?: string; type?: string; default?: unknown }> };
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
- return kind.capacity ? { ...kind, stride: field.capacity ?? 0 } : kind;
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
- return (lastValue = answer.value[key]);
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))),
@@ -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).