@origingame/engine 0.0.0-stage → 0.1.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 (158) hide show
  1. package/bin/og.mjs +25 -0
  2. package/dist/web/origin-engine.d.ts +1233 -0
  3. package/dist/web/origin-engine.js +8628 -0
  4. package/dist/web/origin-engine_bg.wasm +0 -0
  5. package/dist/web/origin-engine_bg.wasm.d.ts +366 -0
  6. package/dist/web-publish/origin-engine-lanes.js +8509 -0
  7. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  8. package/dist/web-publish/origin-engine.d.ts +1176 -0
  9. package/dist/web-publish/origin-engine.js +8313 -0
  10. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  11. package/dist/web-publish/origin-engine_bg.wasm.d.ts +347 -0
  12. package/dist/web-publish/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
  13. package/dist/web-publish-2d/origin-engine-lanes.js +8509 -0
  14. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  15. package/dist/web-publish-2d/origin-engine.d.ts +1176 -0
  16. package/dist/web-publish-2d/origin-engine.js +8313 -0
  17. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  18. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +347 -0
  19. package/dist/web-publish-2d/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
  20. package/knowledge/README.md +31 -0
  21. package/knowledge/knowledge.json +13 -0
  22. package/knowledge/looks/catalogue.json +1871 -0
  23. package/knowledge/package.json +33 -0
  24. package/knowledge/rules/platform.json +329 -0
  25. package/knowledge/scaffold/README.md +27 -0
  26. package/knowledge/scaffold/distribution.json +48 -0
  27. package/knowledge/scaffold/origin-engine.json +19 -0
  28. package/knowledge/scaffold/template/.origin/setup +14 -0
  29. package/knowledge/scaffold/template/LICENSES.md +23 -0
  30. package/knowledge/scaffold/template/README.md +37 -0
  31. package/knowledge/scaffold/template/assets/ai/guard.ogt +48 -0
  32. package/knowledge/scaffold/template/assets/audio/click.ogt +9 -0
  33. package/knowledge/scaffold/template/assets/fonts/NotoSansSC-Regular-subset.ttf.ogt +12 -0
  34. package/knowledge/scaffold/template/assets/fonts/OFL.txt +93 -0
  35. package/knowledge/scaffold/template/assets/looks/origin-game.ogt +12 -0
  36. package/knowledge/scaffold/template/assets/nav/yard.ogt +27 -0
  37. package/knowledge/scaffold/template/assets/scenes/main.ogt +34 -0
  38. package/knowledge/scaffold/template/assets/strings/en.ogt +14 -0
  39. package/knowledge/scaffold/template/assets/textures/stone_n.png.ogt +7 -0
  40. package/knowledge/scaffold/template/gitattributes +22 -0
  41. package/knowledge/scaffold/template/package.json +18 -0
  42. package/knowledge/scaffold/template/project.ogt +25 -0
  43. package/knowledge/scaffold/template/src/main.ts +246 -0
  44. package/knowledge/scaffold/template/tests/guard.og.ts +67 -0
  45. package/knowledge/scaffold/template/tests/smoke.og.ts +43 -0
  46. package/knowledge/scaffold/template/tsconfig.json +17 -0
  47. package/knowledge/scripts/lib.ts +203 -0
  48. package/knowledge/scripts/mount.ts +210 -0
  49. package/knowledge/scripts/pack.ts +24 -0
  50. package/knowledge/scripts/scaffold.ts +72 -0
  51. package/knowledge/scripts/validate.ts +749 -0
  52. package/knowledge/skills/og-ai/SKILL.md +386 -0
  53. package/knowledge/skills/og-audio/SKILL.md +191 -0
  54. package/knowledge/skills/og-build/SKILL.md +330 -0
  55. package/knowledge/skills/og-core/SKILL.md +285 -0
  56. package/knowledge/skills/og-feel/SKILL.md +385 -0
  57. package/knowledge/skills/og-game/SKILL.md +1479 -0
  58. package/knowledge/skills/og-i18n/SKILL.md +220 -0
  59. package/knowledge/skills/og-platform-sdk/SKILL.md +199 -0
  60. package/knowledge/skills/og-taste/SKILL.md +537 -0
  61. package/knowledge/skills/og-ui/SKILL.md +444 -0
  62. package/knowledge/wiki/ai.md +627 -0
  63. package/knowledge/wiki/anim.md +1187 -0
  64. package/knowledge/wiki/atmosphere.md +636 -0
  65. package/knowledge/wiki/audio.md +732 -0
  66. package/knowledge/wiki/camera.md +711 -0
  67. package/knowledge/wiki/commands.md +357 -0
  68. package/knowledge/wiki/game.md +220 -0
  69. package/knowledge/wiki/i18n.md +213 -0
  70. package/knowledge/wiki/index.md +1027 -0
  71. package/knowledge/wiki/input.md +1714 -0
  72. package/knowledge/wiki/load.md +1280 -0
  73. package/knowledge/wiki/math.md +166 -0
  74. package/knowledge/wiki/nav.md +1154 -0
  75. package/knowledge/wiki/physics.md +3142 -0
  76. package/knowledge/wiki/platform.md +857 -0
  77. package/knowledge/wiki/probe.md +1692 -0
  78. package/knowledge/wiki/reflect.md +285 -0
  79. package/knowledge/wiki/render.md +3511 -0
  80. package/knowledge/wiki/rng.md +393 -0
  81. package/knowledge/wiki/stream.md +376 -0
  82. package/knowledge/wiki/system.md +197 -0
  83. package/knowledge/wiki/systems.md +43 -0
  84. package/knowledge/wiki/terrain.md +727 -0
  85. package/knowledge/wiki/tilemap.md +422 -0
  86. package/knowledge/wiki/time.md +150 -0
  87. package/knowledge/wiki/transform.md +230 -0
  88. package/knowledge/wiki/tween.md +580 -0
  89. package/knowledge/wiki/ui.md +8000 -0
  90. package/knowledge/wiki/world.md +909 -0
  91. package/package.json +68 -4
  92. package/schema/api.schema.json +29675 -0
  93. package/shell/README.md +22 -0
  94. package/shell/brand/README.md +23 -0
  95. package/shell/brand/icon-192.png +0 -0
  96. package/shell/brand/icon-512.png +0 -0
  97. package/shell/brand/mark-on-paper.svg +6 -0
  98. package/shell/brand/origin-mark.svg +5 -0
  99. package/shell/brand/origin-motion.css +251 -0
  100. package/shell/brand/tokens.css +20 -0
  101. package/shell/brand.ts +132 -0
  102. package/shell/page.ts +715 -0
  103. package/shell/platform.ts +203 -0
  104. package/shell/player.ts +238 -0
  105. package/shell/shell.css +185 -0
  106. package/shell/sw.ts +177 -0
  107. package/shell/transport.ts +515 -0
  108. package/shell/wire.ts +369 -0
  109. package/src/e6.ts +69 -0
  110. package/src/env.d.ts +100 -0
  111. package/src/generated/abi.ts +980 -0
  112. package/src/index.ts +193 -0
  113. package/src/node/audio.ts +86 -0
  114. package/src/node/build.ts +109 -0
  115. package/src/node/device.ts +981 -0
  116. package/src/node/doctor.ts +60 -0
  117. package/src/node/index.ts +102 -0
  118. package/src/node/platform.ts +24 -0
  119. package/src/node/playtest-run.ts +249 -0
  120. package/src/node/png.ts +130 -0
  121. package/src/node/polish.ts +976 -0
  122. package/src/node/run.ts +373 -0
  123. package/src/node/web-build.ts +808 -0
  124. package/src/p2.ts +35 -0
  125. package/src/platform-local.ts +211 -0
  126. package/src/playtest.ts +131 -0
  127. package/src/runtime.ts +1509 -0
  128. package/src/systems/cameras.ts +247 -0
  129. package/src/ui-kit/bind.ts +141 -0
  130. package/src/ui-kit/huds.ts +303 -0
  131. package/src/ui-kit/index.ts +36 -0
  132. package/src/ui-kit/menus.ts +187 -0
  133. package/src/ui-kit/settings.ts +131 -0
  134. package/src/ui-kit/store.ts +222 -0
  135. package/src/ui-kit/story.ts +89 -0
  136. package/src/ui-kit/util.ts +106 -0
  137. package/src/web/a11y.ts +429 -0
  138. package/src/web/audio-worklet.ts +193 -0
  139. package/src/web/audio.ts +307 -0
  140. package/src/web/basis.ts +239 -0
  141. package/src/web/gamepad.ts +123 -0
  142. package/src/web/host.ts +166 -0
  143. package/src/web/input.ts +237 -0
  144. package/src/web/lanes.ts +21 -0
  145. package/src/web/play.ts +20 -0
  146. package/src/web/rpc-ring.ts +110 -0
  147. package/src/web/settle.ts +122 -0
  148. package/src/web/shell.ts +643 -0
  149. package/src/web/text-input.ts +248 -0
  150. package/src/web/worker.ts +1002 -0
  151. package/types/bindings.json +13654 -0
  152. package/types/index.d.ts +20090 -0
  153. package/vendor/basis/LICENSE +201 -0
  154. package/vendor/basis/NOTICE +15 -0
  155. package/vendor/basis/README.md +18 -0
  156. package/vendor/basis/basis_transcoder.js +19 -0
  157. package/vendor/basis/basis_transcoder.wasm +0 -0
  158. package/README.md +0 -3
@@ -0,0 +1,203 @@
1
+ // The knowledge package as the platform receives it: `payload()` maps the
2
+ // package (and, in the engine repository, templates/game) onto the
3
+ // platform's production layout (`game/…`, `game-scaffold/…`) through
4
+ // knowledge.json, and the helpers read what the platform's own tooling
5
+ // reads (SKILL.md front matter, pack names and hashes).
6
+
7
+ import { createHash } from "node:crypto";
8
+ import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
9
+ import { join, relative } from "node:path";
10
+
11
+ export const PACKAGE_ROOT = join(import.meta.dir, "..");
12
+
13
+ export interface Roots {
14
+ /** packages/knowledge (or a copy of it). */
15
+ readonly pkg: string;
16
+ /** The project template the scaffold overlay carries: templates/game in the repository, scaffold/template/ in the published package. */
17
+ readonly template: string;
18
+ /** packages/engine, whose version the scaffold pins. */
19
+ readonly engine: string;
20
+ }
21
+
22
+ /** Whether `dir` is the published @origingame/engine package (the knowledge ships inside it as knowledge/). */
23
+ function isEnginePackage(dir: string): boolean {
24
+ try {
25
+ return (JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as { name?: string }).name === "@origingame/engine";
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /** The roots in the engine repository (packages/knowledge beside packages/engine), or in the published @origingame/engine, whose knowledge/ this is. */
32
+ export function defaultRoots(pkg = PACKAGE_ROOT): Roots {
33
+ const repoTemplate = join(pkg, "..", "..", "templates", "game");
34
+ const parent = join(pkg, "..");
35
+ return {
36
+ pkg,
37
+ template: existsSync(join(repoTemplate, "project.ogt")) ? repoTemplate : join(pkg, "scaffold", "template"),
38
+ engine: isEnginePackage(parent) ? parent : join(parent, "engine"),
39
+ };
40
+ }
41
+
42
+ export interface Mount {
43
+ /** A package path (`skills/`, `wiki/`), or `@template/` for the project template. */
44
+ readonly from: string;
45
+ /** Its place in the platform's production tree. */
46
+ readonly to: string;
47
+ }
48
+
49
+ export interface Manifest {
50
+ readonly schemaVersion: 1;
51
+ readonly overlay: string;
52
+ readonly mounts: readonly Mount[];
53
+ }
54
+
55
+ export function readManifest(pkg: string): Manifest {
56
+ return JSON.parse(readFileSync(join(pkg, "knowledge.json"), "utf8")) as Manifest;
57
+ }
58
+
59
+ /** One file of the payload: its production path, where it came from, its bytes. */
60
+ export interface PayloadFile {
61
+ readonly path: string;
62
+ readonly source: string;
63
+ readonly bytes: Uint8Array;
64
+ readonly symlink: boolean;
65
+ }
66
+
67
+ /** Files under `dir`, sorted, skipping build output a template never carries. */
68
+ export function walk(dir: string, skip: ReadonlySet<string> = new Set()): string[] {
69
+ const out: string[] = [];
70
+ const visit = (d: string) => {
71
+ for (const name of readdirSync(d).sort()) {
72
+ const full = join(d, name);
73
+ const rel = relative(dir, full);
74
+ if (skip.has(name) || skip.has(rel)) continue;
75
+ const st = lstatSync(full);
76
+ if (st.isDirectory()) visit(full);
77
+ else out.push(full);
78
+ }
79
+ };
80
+ if (existsSync(dir)) visit(dir);
81
+ return out;
82
+ }
83
+
84
+ /** What never travels with a template: build and run output (the template's .gitignore) and installs. */
85
+ export const TEMPLATE_SKIP: ReadonlySet<string> = new Set(["cooked", "dist", "og-out", ".og", "node_modules", ".DS_Store"]);
86
+
87
+ /**
88
+ * Template files stored under another name than the scaffold gives them
89
+ * (packages/create's RENAMED): the template's `gitattributes` lands in the
90
+ * overlay as `.gitattributes`, which in the engine repository would route
91
+ * the template's plain-blob media through LFS.
92
+ */
93
+ export const TEMPLATE_RENAMED: ReadonlyMap<string, string> = new Map([["gitattributes", ".gitattributes"]]);
94
+
95
+ const fatal = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
96
+
97
+ /** Whether bytes are text the platform's payload can carry (UTF-8, no NUL). */
98
+ export function isText(bytes: Uint8Array): boolean {
99
+ if (bytes.includes(0)) return false;
100
+ try {
101
+ fatal.decode(bytes);
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * The production-tree payload: every mounted file. Template binaries are
110
+ * included here (so the validator sees them) and flagged by `isText`; the
111
+ * published bundle carries text only and names binaries in distribution.json.
112
+ */
113
+ export function payload(roots: Roots): PayloadFile[] {
114
+ const manifest = readManifest(roots.pkg);
115
+ const files: PayloadFile[] = [];
116
+ for (const m of manifest.mounts) {
117
+ const base = m.from === "@template/" ? roots.template : join(roots.pkg, m.from);
118
+ const skip = m.from === "@template/" ? TEMPLATE_SKIP : new Set<string>();
119
+ const sources = m.from.endsWith("/") ? walk(base, skip) : [base];
120
+ for (const source of sources) {
121
+ const found = m.from.endsWith("/") ? relative(base, source).split("\\").join("/") : "";
122
+ const rel = m.from === "@template/" ? (TEMPLATE_RENAMED.get(found) ?? found) : found;
123
+ const path = m.to.endsWith("/") ? m.to + rel : m.to;
124
+ const symlink = existsSync(source) ? lstatSync(source).isSymbolicLink() : false;
125
+ files.push({ path, source, bytes: existsSync(source) && !symlink ? new Uint8Array(readFileSync(source)) : new Uint8Array(), symlink });
126
+ }
127
+ }
128
+ return files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
129
+ }
130
+
131
+ export function sha256(bytes: Uint8Array | string): string {
132
+ return createHash("sha256").update(bytes).digest("hex");
133
+ }
134
+
135
+ /** Pack names as agent-server/src/core/knowledge.ts `packsFromFiles` groups production paths. */
136
+ export function packName(path: string): string {
137
+ const parts = path.split("/");
138
+ if (path === "game/instructions.md") return "instructions";
139
+ if (parts[0] === "game" && parts[1] === "skills" && parts[2]) return `skill:${parts[2]}`;
140
+ if (parts[0] === "game-scaffold" && parts[1]) return `scaffold:${parts[1]}`;
141
+ return "game";
142
+ }
143
+
144
+ /** Each pack's content hash, as `packsFromFiles` computes it over the text files. */
145
+ export function packs(files: readonly PayloadFile[]): Record<string, string> {
146
+ const groups = new Map<string, Record<string, string>>();
147
+ for (const f of files) {
148
+ if (!isText(f.bytes)) continue;
149
+ const name = packName(f.path);
150
+ const g = groups.get(name) ?? {};
151
+ g[f.path] = fatal.decode(f.bytes);
152
+ groups.set(name, g);
153
+ }
154
+ return Object.fromEntries(
155
+ [...groups.entries()]
156
+ .sort(([a], [b]) => a.localeCompare(b))
157
+ .map(([name, g]) => [name, sha256(Object.keys(g).sort().map((p) => `${p}\0${sha256(g[p]!)}`).join("\n")).slice(0, 16)]),
158
+ );
159
+ }
160
+
161
+ /** A SKILL.md's front matter as `key: value` pairs, or null without the `---` fences. */
162
+ export function frontMatter(text: string): Record<string, string> | null {
163
+ const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
164
+ if (lines[0]?.trim() !== "---") return null;
165
+ const end = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
166
+ if (end < 0) return null;
167
+ const out: Record<string, string> = {};
168
+ let key = "";
169
+ for (const line of lines.slice(1, end)) {
170
+ const m = /^([A-Za-z][\w-]*):(.*)$/.exec(line);
171
+ if (m) {
172
+ key = m[1]!;
173
+ const rest = m[2]!.trim();
174
+ out[key] = ["", ">", "|", ">-", "|-"].includes(rest) ? "" : rest.replace(/^["']|["']$/g, "");
175
+ } else if (key && /^[ \t]/.test(line)) {
176
+ out[key] = out[key] ? `${out[key]} ${line.trim()}` : line.trim();
177
+ }
178
+ }
179
+ return out;
180
+ }
181
+
182
+ /** The top-level `key value` entries of an `.ogt` document (values as written, quotes removed). */
183
+ export function ogtTopLevel(text: string): { type: string; entries: Map<string, string> } {
184
+ const lines = text.split(/\r?\n/);
185
+ const sep = lines.findIndex((l) => l.trim() === "---");
186
+ const header = lines.slice(0, sep < 0 ? 0 : sep).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
187
+ const type = header.find((l) => l.startsWith("type "))?.slice(5).trim() ?? "";
188
+ const entries = new Map<string, string>();
189
+ let depth = 0;
190
+ for (const raw of lines.slice(sep + 1)) {
191
+ const line = raw.replace(/(^|\s)#.*$/, "").trim();
192
+ if (!line) continue;
193
+ if (line === "}") {
194
+ depth--;
195
+ continue;
196
+ }
197
+ const [key, ...rest] = line.split(/\s+/);
198
+ const opens = line.endsWith("{");
199
+ if (depth === 0) entries.set(key!, (opens ? rest.slice(0, -1) : rest).join(" ").replace(/^"|"$/g, ""));
200
+ if (opens) depth++;
201
+ }
202
+ return { type, entries };
203
+ }
@@ -0,0 +1,210 @@
1
+ // Materialises the mounts knowledge.json declares into an OriginGame
2
+ // platform checkout's production tree (packages/game-knowledge/production):
3
+ // skills as game/skills/<name>/, the wiki as og-game's reference/, the
4
+ // project template plus origin-engine.json and distribution.json as the
5
+ // scaffold overlay game-scaffold/<overlay>/.
6
+ //
7
+ // bun scripts/mount.ts <origingame checkout> [--check] [--commit <rev> | --package <dir>]
8
+ //
9
+ // The package owns, in the platform tree, exactly the directories it mounts:
10
+ // game/skills/<each of its skills>/ and game-scaffold/<overlay>/. Within
11
+ // them the result is byte-for-byte the package's text payload: files are
12
+ // written only when their bytes differ, files the package no longer has are
13
+ // removed, and nothing outside them is touched, so a second run changes
14
+ // nothing. Binary template files (models, textures, audio, fonts) are never
15
+ // written: the platform payload is UTF-8 text only and distribution.json
16
+ // names them with their digests. The package is validated against the
17
+ // platform rules first; an invalid package mounts nothing.
18
+ //
19
+ // --check writes nothing and exits 1 when the platform tree differs, naming
20
+ // each added, changed and removed path. --commit mounts the package as
21
+ // committed at <rev> (a `git archive` of packages/knowledge, templates/game
22
+ // and packages/engine/package.json) instead of the working tree, so work in
23
+ // progress in this checkout never reaches the platform. --package mounts the
24
+ // knowledge of an installed @origingame/engine (the directory `bun install`
25
+ // put it in; the package carries this directory as knowledge/): what the
26
+ // platform does at a pinned version (docs/release.md §4 L2).
27
+
28
+ import { execFileSync } from "node:child_process";
29
+ import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, writeFileSync } from "node:fs";
30
+ import { tmpdir } from "node:os";
31
+ import { dirname, join, resolve } from "node:path";
32
+ import { defaultRoots, isText, PACKAGE_ROOT, payload, walk, type Roots } from "./lib.ts";
33
+ import { validate } from "./validate.ts";
34
+
35
+ export const PRODUCTION = "packages/game-knowledge/production";
36
+
37
+ export interface Change {
38
+ readonly kind: "added" | "changed" | "removed";
39
+ readonly path: string;
40
+ }
41
+
42
+ export interface MountResult {
43
+ /** The platform's production root. */
44
+ readonly production: string;
45
+ /** The directories the package owns there, production-relative, each ending in `/`. */
46
+ readonly owned: readonly string[];
47
+ /** Text files mounted (written or already identical). */
48
+ readonly files: number;
49
+ /** Template binaries left to distribution.json. */
50
+ readonly binaries: readonly string[];
51
+ readonly changes: readonly Change[];
52
+ }
53
+
54
+ /** The production-relative directories the payload occupies: game/skills/<name>/ and game-scaffold/<overlay>/. */
55
+ export function ownedRoots(paths: readonly string[]): string[] {
56
+ const owned = new Set<string>();
57
+ for (const p of paths) {
58
+ const parts = p.split("/");
59
+ if (parts[0] === "game" && parts[1] === "skills" && parts.length > 3) owned.add(`game/skills/${parts[2]}/`);
60
+ else if (parts[0] === "game-scaffold" && parts.length > 2) owned.add(`game-scaffold/${parts[1]}/`);
61
+ else throw new Error(`${p}: a mount outside game/skills/<name>/ and game-scaffold/<overlay>/; fix knowledge.json`);
62
+ }
63
+ return [...owned].sort();
64
+ }
65
+
66
+ function executable(path: string): boolean {
67
+ return (statSync(path).mode & 0o111) !== 0;
68
+ }
69
+
70
+ /** Compares (and with `write`, materialises) the package's payload in `production`. */
71
+ export function mount(roots: Roots, production: string, write: boolean): MountResult {
72
+ for (const top of ["game/skills", "game-scaffold"]) {
73
+ if (!existsSync(join(production, top)) || !statSync(join(production, top)).isDirectory()) {
74
+ throw new Error(`${production}: no ${top}/; pass the root of an OriginGame checkout (it holds ${PRODUCTION})`);
75
+ }
76
+ }
77
+ const problems = validate(roots);
78
+ if (problems.length) throw new Error(`the knowledge package is invalid against the platform rules; nothing mounted:\n${problems.join("\n")}`);
79
+
80
+ const all = payload(roots);
81
+ const text = all.filter((f) => isText(f.bytes));
82
+ const binaries = all.filter((f) => !isText(f.bytes)).map((f) => f.path);
83
+ const owned = ownedRoots(all.map((f) => f.path));
84
+ const wanted = new Map(text.map((f) => [f.path, f]));
85
+ const changes: Change[] = [];
86
+
87
+ for (const f of text) {
88
+ const target = join(production, f.path);
89
+ const exists = existsSync(target);
90
+ if (exists && lstatSync(target).isDirectory()) throw new Error(`${f.path}: a directory in the platform tree where the package has a file`);
91
+ const same = exists && Buffer.compare(readFileSync(target), Buffer.from(f.bytes)) === 0;
92
+ const mode = executable(f.source) ? 0o755 : 0o644;
93
+ const modeSame = exists && (statSync(target).mode & 0o777) === mode;
94
+ if (same && modeSame) continue;
95
+ changes.push({ kind: exists ? "changed" : "added", path: f.path });
96
+ if (write) {
97
+ mkdirSync(dirname(target), { recursive: true });
98
+ if (!same) writeFileSync(target, f.bytes);
99
+ chmodSync(target, mode);
100
+ }
101
+ }
102
+
103
+ for (const root of owned) {
104
+ const dir = join(production, root);
105
+ for (const full of walk(dir)) {
106
+ const path = `${root}${full.slice(dir.length).split("\\").join("/")}`;
107
+ if (wanted.has(path)) continue;
108
+ changes.push({ kind: "removed", path });
109
+ if (write) rmSync(full, { force: true });
110
+ }
111
+ if (write) pruneEmpty(dir);
112
+ }
113
+
114
+ changes.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
115
+ return { production, owned, files: text.length, binaries, changes };
116
+ }
117
+
118
+ /** Removes empty directories under `dir` (not `dir` itself). */
119
+ function pruneEmpty(dir: string): void {
120
+ if (!existsSync(dir)) return;
121
+ for (const name of readdirSync(dir)) {
122
+ const full = join(dir, name);
123
+ if (!lstatSync(full).isDirectory()) continue;
124
+ pruneEmpty(full);
125
+ if (readdirSync(full).length === 0) rmdirSync(full);
126
+ }
127
+ }
128
+
129
+ /** What a mount reads from the engine repository. */
130
+ export const SOURCES = ["packages/knowledge", "templates/game", "packages/engine/package.json"] as const;
131
+
132
+ const git = (repo: string, ...args: string[]) =>
133
+ execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
134
+
135
+ /** The working tree's short revision, `+dirty` when the mounted sources differ from it, or `unversioned`. */
136
+ export function revision(repo: string): string {
137
+ try {
138
+ const head = git(repo, "rev-parse", "--short", "HEAD");
139
+ return git(repo, "status", "--porcelain", "--", ...SOURCES) ? `${head}+dirty` : head;
140
+ } catch {
141
+ return "unversioned";
142
+ }
143
+ }
144
+
145
+ /** The package as committed at `rev`, exported into a temporary directory: its roots, short revision and a cleanup. */
146
+ export function committed(repo: string, rev: string): { roots: Roots; revision: string; cleanup: () => void } {
147
+ const short = git(repo, "rev-parse", "--short", `${rev}^{commit}`);
148
+ const dir = mkdtempSync(join(tmpdir(), "og-knowledge-mount-"));
149
+ execFileSync("sh", ["-c", 'git -C "$1" archive "$2" -- "$3" "$4" "$5" | tar -x -C "$6"', "sh", repo, short, ...SOURCES, dir], {
150
+ stdio: ["ignore", "ignore", "pipe"],
151
+ });
152
+ return { roots: defaultRoots(join(dir, "packages/knowledge")), revision: short, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
153
+ }
154
+
155
+ /** An installed @origingame/engine (the directory `bun install` put it in, or its knowledge/): the knowledge's roots and `@origingame/engine@<version>`. */
156
+ export function installed(dir: string): { roots: Roots; revision: string; cleanup: () => void } {
157
+ const manifest = join(dir, "package.json");
158
+ if (!existsSync(manifest)) throw new Error(`${dir}: no package.json; --package takes the installed @origingame/engine directory`);
159
+ const pkg = JSON.parse(readFileSync(manifest, "utf8")) as { name?: string; version?: string };
160
+ let knowledge = dir;
161
+ if (pkg.name === "@origingame/engine") knowledge = join(dir, "knowledge");
162
+ else if (pkg.name !== "@origingame/engine-knowledge") throw new Error(`${dir}: ${pkg.name ?? "no name"}, not @origingame/engine`);
163
+ if (!existsSync(join(knowledge, "knowledge.json"))) throw new Error(`${knowledge}: no knowledge.json; the published @origingame/engine carries the knowledge in knowledge/`);
164
+ const roots = defaultRoots(knowledge);
165
+ if (!existsSync(join(roots.engine, "package.json"))) throw new Error(`${roots.engine}: no @origingame/engine package.json`);
166
+ const engine = JSON.parse(readFileSync(join(roots.engine, "package.json"), "utf8")) as { name?: string; version?: string };
167
+ return { roots, revision: `${engine.name}@${engine.version} (knowledge)`, cleanup: () => {} };
168
+ }
169
+
170
+ export function summary(r: MountResult, rev: string, check: boolean): string {
171
+ const count = (k: Change["kind"]) => r.changes.filter((c) => c.kind === k).length;
172
+ const lines = [
173
+ `${check ? "check" : "mount"}: engine knowledge ${rev} -> ${r.production}`,
174
+ `owned: ${r.owned.join(", ")}`,
175
+ `text files: ${r.files}; binaries left to distribution.json: ${r.binaries.length}`,
176
+ `${check ? "differences" : "changes"}: ${count("added")} added, ${count("changed")} changed, ${count("removed")} removed`,
177
+ ...r.changes.map((c) => ` ${c.kind.padEnd(7)} ${c.path}`),
178
+ ];
179
+ return `${lines.join("\n")}\n`;
180
+ }
181
+
182
+ if (import.meta.main) {
183
+ const args = process.argv.slice(2);
184
+ const check = args.includes("--check");
185
+ const ci = args.indexOf("--commit");
186
+ const commit = ci >= 0 ? args[ci + 1] : undefined;
187
+ const pi = args.indexOf("--package");
188
+ const pkgDir = pi >= 0 ? args[pi + 1] : undefined;
189
+ const positional = args.filter((a, i) => !a.startsWith("--") && !(ci >= 0 && i === ci + 1) && !(pi >= 0 && i === pi + 1));
190
+ if ((ci >= 0 && !commit) || (pi >= 0 && !pkgDir) || (commit && pkgDir) || positional.length !== 1 || args.some((a) => a.startsWith("--") && a !== "--check" && a !== "--commit" && a !== "--package")) {
191
+ process.stderr.write("usage: bun scripts/mount.ts <origingame checkout> [--check] [--commit <rev> | --package <dir>]\n");
192
+ process.exit(2);
193
+ }
194
+ const repo = join(PACKAGE_ROOT, "..", "..");
195
+ const production = join(resolve(positional[0]!), PRODUCTION);
196
+ let code = 0;
197
+ let source: { roots: Roots; revision: string; cleanup: () => void } | undefined;
198
+ try {
199
+ source = commit ? committed(repo, commit) : pkgDir ? installed(resolve(pkgDir)) : { roots: defaultRoots(), revision: revision(repo), cleanup: () => {} };
200
+ const result = mount(source.roots, production, !check);
201
+ process.stdout.write(summary(result, source.revision, check));
202
+ if (check && result.changes.length) code = 1;
203
+ } catch (e) {
204
+ process.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`);
205
+ code = 1;
206
+ } finally {
207
+ source?.cleanup();
208
+ }
209
+ process.exit(code);
210
+ }
@@ -0,0 +1,24 @@
1
+ // prepack: copies the project template's text files into scaffold/template/
2
+ // so the published package carries the scaffold overlay without the engine
3
+ // repository (binaries stay out: distribution.json names them). Ignored by Git.
4
+
5
+ import { cpSync, mkdirSync, readFileSync, rmSync } from "node:fs";
6
+ import { dirname, join, relative } from "node:path";
7
+ import { defaultRoots, isText, PACKAGE_ROOT, TEMPLATE_SKIP, walk } from "./lib.ts";
8
+
9
+ const source = join(PACKAGE_ROOT, "..", "..", "templates", "game");
10
+ const target = join(PACKAGE_ROOT, "scaffold", "template");
11
+ if (defaultRoots().template !== source) {
12
+ process.stderr.write(`no ${source}: pack inside the engine repository\n`);
13
+ process.exit(1);
14
+ }
15
+ rmSync(target, { recursive: true, force: true });
16
+ let n = 0;
17
+ for (const full of walk(source, TEMPLATE_SKIP)) {
18
+ if (!isText(new Uint8Array(readFileSync(full)))) continue;
19
+ const to = join(target, relative(source, full));
20
+ mkdirSync(dirname(to), { recursive: true });
21
+ cpSync(full, to);
22
+ n++;
23
+ }
24
+ process.stdout.write(`copied ${n} template text files into scaffold/template/\n`);
@@ -0,0 +1,72 @@
1
+ // Writes or checks scaffold/distribution.json: the project template's binary
2
+ // files (models, textures, audio, fonts) with their sizes, digests and
3
+ // licences. The platform's scaffold payload is UTF-8 text only, so binaries
4
+ // travel from the engine distribution (the published @origingame/create-game
5
+ // template) and are verified against these digests, as the platform's
6
+ // game-scaffold/origin-engine/distribution.json does for its overlay.
7
+ //
8
+ // bun scripts/scaffold.ts --write | --check
9
+
10
+ import { readFileSync, writeFileSync } from "node:fs";
11
+ import { join, relative } from "node:path";
12
+ import { defaultRoots, isText, sha256, TEMPLATE_SKIP, walk } from "./lib.ts";
13
+
14
+ /** Licence per template file, from the template's LICENSES.md table. */
15
+ function licences(template: string): Map<string, string> {
16
+ const out = new Map<string, string>();
17
+ let text = "";
18
+ try {
19
+ text = readFileSync(join(template, "LICENSES.md"), "utf8");
20
+ } catch {
21
+ return out;
22
+ }
23
+ for (const line of text.split("\n")) {
24
+ if (!line.startsWith("| `")) continue;
25
+ const cells = line.split("|").map((c) => c.trim());
26
+ for (const m of cells[1]!.matchAll(/`([^`]+)`/g)) out.set(m[1]!, `${cells[2]} (LICENSES.md)`);
27
+ }
28
+ return out;
29
+ }
30
+
31
+ export function distribution(template: string): string {
32
+ const lic = licences(template);
33
+ const files = walk(template, TEMPLATE_SKIP)
34
+ .map((full) => ({ full, rel: relative(template, full).split("\\").join("/"), bytes: new Uint8Array(readFileSync(full)) }))
35
+ .filter((f) => !isText(f.bytes))
36
+ .map((f) => ({
37
+ path: f.rel,
38
+ bytes: f.bytes.length,
39
+ sha256: sha256(f.bytes),
40
+ licence: lic.get(f.rel) ?? "unrecorded: add it to the template's LICENSES.md",
41
+ source: `templates/game/${f.rel} of origingame/origin-engine; shipped in @origingame/create-game's template/`,
42
+ }));
43
+ return `${JSON.stringify(
44
+ {
45
+ schema: 1,
46
+ about: "Binary files of the Origin Engine project template that a scaffolded Game needs but that are not knowledge text. The scaffolder copies them from the published template (@origingame/create-game) and verifies each digest.",
47
+ files,
48
+ },
49
+ null,
50
+ 2,
51
+ )}\n`;
52
+ }
53
+
54
+ if (import.meta.main) {
55
+ const roots = defaultRoots();
56
+ const path = join(roots.pkg, "scaffold/distribution.json");
57
+ const want = distribution(roots.template);
58
+ if (process.argv.includes("--write")) {
59
+ writeFileSync(path, want);
60
+ process.stdout.write(`wrote scaffold/distribution.json from ${relative(process.cwd(), roots.template) || "."}\n`);
61
+ } else {
62
+ let have = "";
63
+ try {
64
+ have = readFileSync(path, "utf8");
65
+ } catch {}
66
+ if (have !== want) {
67
+ process.stderr.write("scaffold/distribution.json differs from the template's binaries; run bun scripts/scaffold.ts --write\n");
68
+ process.exit(1);
69
+ }
70
+ process.stdout.write("scaffold/distribution.json matches the template.\n");
71
+ }
72
+ }