sci2-ts 0.0.0-stage → 0.2.2
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/CHANGELOG.md +78 -0
- package/LICENSE +21 -0
- package/README.md +113 -2
- package/apps/viewer/editor.html +43 -0
- package/apps/viewer/explore.html +27 -0
- package/apps/viewer/index.html +26 -0
- package/apps/viewer/package.json +27 -0
- package/apps/viewer/play.html +26 -0
- package/apps/viewer/plugin.d.ts +18 -0
- package/apps/viewer/plugin.js +75 -0
- package/apps/viewer/plugin.ts +84 -0
- package/apps/viewer/src/adlib.d.ts +20 -0
- package/apps/viewer/src/adlib.js +55 -0
- package/apps/viewer/src/adlib.ts +62 -0
- package/apps/viewer/src/app.d.ts +5 -0
- package/apps/viewer/src/app.js +5 -0
- package/apps/viewer/src/app.ts +5 -0
- package/apps/viewer/src/asset-index.d.ts +22 -0
- package/apps/viewer/src/asset-index.js +74 -0
- package/apps/viewer/src/asset-index.ts +90 -0
- package/apps/viewer/src/audio.d.ts +28 -0
- package/apps/viewer/src/audio.js +102 -0
- package/apps/viewer/src/audio.ts +110 -0
- package/apps/viewer/src/editor-handles.d.ts +63 -0
- package/apps/viewer/src/editor-handles.js +332 -0
- package/apps/viewer/src/editor-handles.ts +368 -0
- package/apps/viewer/src/editor-languages.d.ts +23 -0
- package/apps/viewer/src/editor-languages.js +157 -0
- package/apps/viewer/src/editor-languages.ts +135 -0
- package/apps/viewer/src/editor.css +213 -0
- package/apps/viewer/src/editor.d.ts +1 -0
- package/apps/viewer/src/editor.js +393 -0
- package/apps/viewer/src/editor.ts +412 -0
- package/apps/viewer/src/explore/classes.d.ts +7 -0
- package/apps/viewer/src/explore/classes.js +63 -0
- package/apps/viewer/src/explore/classes.ts +61 -0
- package/apps/viewer/src/explore/context.d.ts +33 -0
- package/apps/viewer/src/explore/context.js +96 -0
- package/apps/viewer/src/explore/context.ts +124 -0
- package/apps/viewer/src/explore/explore.css +93 -0
- package/apps/viewer/src/explore/explore.d.ts +1 -0
- package/apps/viewer/src/explore/explore.js +102 -0
- package/apps/viewer/src/explore/explore.ts +104 -0
- package/apps/viewer/src/explore/inspect.d.ts +5 -0
- package/apps/viewer/src/explore/inspect.js +208 -0
- package/apps/viewer/src/explore/inspect.ts +279 -0
- package/apps/viewer/src/explore/live.d.ts +22 -0
- package/apps/viewer/src/explore/live.js +318 -0
- package/apps/viewer/src/explore/live.ts +365 -0
- package/apps/viewer/src/explore/map.d.ts +11 -0
- package/apps/viewer/src/explore/map.js +182 -0
- package/apps/viewer/src/explore/map.ts +193 -0
- package/apps/viewer/src/files.d.ts +21 -0
- package/apps/viewer/src/files.js +69 -0
- package/apps/viewer/src/files.ts +72 -0
- package/apps/viewer/src/game-profile.d.ts +19 -0
- package/apps/viewer/src/game-profile.js +4 -0
- package/apps/viewer/src/game-profile.ts +26 -0
- package/apps/viewer/src/main.d.ts +1 -0
- package/apps/viewer/src/main.js +473 -0
- package/apps/viewer/src/main.ts +527 -0
- package/apps/viewer/src/music.d.ts +14 -0
- package/apps/viewer/src/music.js +32 -0
- package/apps/viewer/src/music.ts +34 -0
- package/apps/viewer/src/opl-worklet.d.ts +1 -0
- package/apps/viewer/src/opl-worklet.js +47 -0
- package/apps/viewer/src/opl-worklet.ts +55 -0
- package/apps/viewer/src/overlay.d.ts +31 -0
- package/apps/viewer/src/overlay.js +103 -0
- package/apps/viewer/src/overlay.ts +123 -0
- package/apps/viewer/src/play.css +14 -0
- package/apps/viewer/src/play.d.ts +1 -0
- package/apps/viewer/src/play.js +179 -0
- package/apps/viewer/src/play.ts +177 -0
- package/apps/viewer/src/saves.d.ts +10 -0
- package/apps/viewer/src/saves.js +50 -0
- package/apps/viewer/src/saves.ts +51 -0
- package/apps/viewer/src/session.d.ts +91 -0
- package/apps/viewer/src/session.js +301 -0
- package/apps/viewer/src/session.ts +321 -0
- package/apps/viewer/src/sound-preview.d.ts +7 -0
- package/apps/viewer/src/sound-preview.js +214 -0
- package/apps/viewer/src/sound-preview.ts +228 -0
- package/apps/viewer/src/style.css +64 -0
- package/apps/viewer/vite.config.ts +25 -0
- package/bin/sci-ts.mjs +9 -0
- package/lib/994.sc +49 -0
- package/lib/995.sc +53 -0
- package/lib/996.sc +299 -0
- package/lib/997.sc +198 -0
- package/lib/998.sc +338 -0
- package/lib/999.sc +189 -0
- package/lib/resources.d.ts +2 -0
- package/lib/resources.js +84 -0
- package/lib/resources.ts +88 -0
- package/lib/system.sh +62 -0
- package/package.json +75 -4
- package/packages/content/room.schema.json +267 -0
- package/packages/content/src/index.d.ts +3 -0
- package/packages/content/src/index.js +3 -0
- package/packages/content/src/index.ts +3 -0
- package/packages/content/src/room.d.ts +111 -0
- package/packages/content/src/room.js +886 -0
- package/packages/content/src/room.ts +1011 -0
- package/packages/content/src/target.d.ts +80 -0
- package/packages/content/src/target.js +1 -0
- package/packages/content/src/target.ts +60 -0
- package/packages/content/src/yarn.d.ts +90 -0
- package/packages/content/src/yarn.js +214 -0
- package/packages/content/src/yarn.ts +261 -0
- package/packages/sci/src/analysis/game.d.ts +90 -0
- package/packages/sci/src/analysis/game.js +221 -0
- package/packages/sci/src/analysis/game.ts +305 -0
- package/packages/sci/src/analysis/runtime.d.ts +58 -0
- package/packages/sci/src/analysis/runtime.js +133 -0
- package/packages/sci/src/analysis/runtime.ts +163 -0
- package/packages/sci/src/audio/adlib.d.ts +73 -0
- package/packages/sci/src/audio/adlib.js +280 -0
- package/packages/sci/src/audio/adlib.ts +329 -0
- package/packages/sci/src/audio/index.d.ts +43 -0
- package/packages/sci/src/audio/index.js +99 -0
- package/packages/sci/src/audio/index.ts +122 -0
- package/packages/sci/src/audio/midi.d.ts +92 -0
- package/packages/sci/src/audio/midi.js +242 -0
- package/packages/sci/src/audio/midi.ts +262 -0
- package/packages/sci/src/audio/opl2.d.ts +74 -0
- package/packages/sci/src/audio/opl2.js +283 -0
- package/packages/sci/src/audio/opl2.ts +283 -0
- package/packages/sci/src/audio/sol.d.ts +32 -0
- package/packages/sci/src/audio/sol.js +81 -0
- package/packages/sci/src/audio/sol.ts +100 -0
- package/packages/sci/src/audio/write.d.ts +35 -0
- package/packages/sci/src/audio/write.js +91 -0
- package/packages/sci/src/audio/write.ts +108 -0
- package/packages/sci/src/gfx/cel.d.ts +36 -0
- package/packages/sci/src/gfx/cel.js +82 -0
- package/packages/sci/src/gfx/cel.ts +110 -0
- package/packages/sci/src/gfx/palette.d.ts +24 -0
- package/packages/sci/src/gfx/palette.js +49 -0
- package/packages/sci/src/gfx/palette.ts +56 -0
- package/packages/sci/src/gfx/pic-file.d.ts +46 -0
- package/packages/sci/src/gfx/pic-file.js +114 -0
- package/packages/sci/src/gfx/pic-file.ts +155 -0
- package/packages/sci/src/gfx/pic.d.ts +27 -0
- package/packages/sci/src/gfx/pic.js +34 -0
- package/packages/sci/src/gfx/pic.ts +53 -0
- package/packages/sci/src/gfx/quantize.d.ts +42 -0
- package/packages/sci/src/gfx/quantize.js +199 -0
- package/packages/sci/src/gfx/quantize.ts +218 -0
- package/packages/sci/src/gfx/view-file.d.ts +62 -0
- package/packages/sci/src/gfx/view-file.js +182 -0
- package/packages/sci/src/gfx/view-file.ts +233 -0
- package/packages/sci/src/gfx/view.d.ts +28 -0
- package/packages/sci/src/gfx/view.js +45 -0
- package/packages/sci/src/gfx/view.ts +62 -0
- package/packages/sci/src/index.d.ts +38 -0
- package/packages/sci/src/index.js +38 -0
- package/packages/sci/src/index.ts +38 -0
- package/packages/sci/src/motion/pathfinding.d.ts +40 -0
- package/packages/sci/src/motion/pathfinding.js +140 -0
- package/packages/sci/src/motion/pathfinding.ts +147 -0
- package/packages/sci/src/resource/archive.d.ts +16 -0
- package/packages/sci/src/resource/archive.js +51 -0
- package/packages/sci/src/resource/archive.ts +56 -0
- package/packages/sci/src/resource/lzs.d.ts +12 -0
- package/packages/sci/src/resource/lzs.js +64 -0
- package/packages/sci/src/resource/lzs.ts +66 -0
- package/packages/sci/src/resource/manager.d.ts +45 -0
- package/packages/sci/src/resource/manager.js +158 -0
- package/packages/sci/src/resource/manager.ts +184 -0
- package/packages/sci/src/resource/map.d.ts +18 -0
- package/packages/sci/src/resource/map.js +34 -0
- package/packages/sci/src/resource/map.ts +44 -0
- package/packages/sci/src/resource/types.d.ts +37 -0
- package/packages/sci/src/resource/types.js +45 -0
- package/packages/sci/src/resource/types.ts +57 -0
- package/packages/sci/src/script/assembly.d.ts +67 -0
- package/packages/sci/src/script/assembly.js +748 -0
- package/packages/sci/src/script/assembly.ts +714 -0
- package/packages/sci/src/script/compiler.d.ts +75 -0
- package/packages/sci/src/script/compiler.js +993 -0
- package/packages/sci/src/script/compiler.ts +1040 -0
- package/packages/sci/src/script/disasm.d.ts +81 -0
- package/packages/sci/src/script/disasm.js +318 -0
- package/packages/sci/src/script/disasm.ts +323 -0
- package/packages/sci/src/script/kernel-names.d.ts +6 -0
- package/packages/sci/src/script/kernel-names.js +165 -0
- package/packages/sci/src/script/kernel-names.ts +165 -0
- package/packages/sci/src/script/opcodes.d.ts +22 -0
- package/packages/sci/src/script/opcodes.js +45 -0
- package/packages/sci/src/script/opcodes.ts +72 -0
- package/packages/sci/src/script/script.d.ts +56 -0
- package/packages/sci/src/script/script.js +63 -0
- package/packages/sci/src/script/script.ts +111 -0
- package/packages/sci/src/script/sexpr.d.ts +45 -0
- package/packages/sci/src/script/sexpr.js +151 -0
- package/packages/sci/src/script/sexpr.ts +139 -0
- package/packages/sci/src/script/vocab.d.ts +15 -0
- package/packages/sci/src/script/vocab.js +44 -0
- package/packages/sci/src/script/vocab.ts +48 -0
- package/packages/sci/src/script/world.d.ts +25 -0
- package/packages/sci/src/script/world.js +65 -0
- package/packages/sci/src/script/world.ts +70 -0
- package/packages/sci/src/text/font.d.ts +37 -0
- package/packages/sci/src/text/font.js +103 -0
- package/packages/sci/src/text/font.ts +125 -0
- package/packages/sci/src/text/message.d.ts +69 -0
- package/packages/sci/src/text/message.js +171 -0
- package/packages/sci/src/text/message.ts +205 -0
- package/packages/sci/src/vm/kernels/arrays.d.ts +19 -0
- package/packages/sci/src/vm/kernels/arrays.js +182 -0
- package/packages/sci/src/vm/kernels/arrays.ts +164 -0
- package/packages/sci/src/vm/kernels/audio.d.ts +70 -0
- package/packages/sci/src/vm/kernels/audio.js +347 -0
- package/packages/sci/src/vm/kernels/audio.ts +349 -0
- package/packages/sci/src/vm/kernels/core.d.ts +5 -0
- package/packages/sci/src/vm/kernels/core.js +87 -0
- package/packages/sci/src/vm/kernels/core.ts +91 -0
- package/packages/sci/src/vm/kernels/graphics.d.ts +141 -0
- package/packages/sci/src/vm/kernels/graphics.js +467 -0
- package/packages/sci/src/vm/kernels/graphics.ts +502 -0
- package/packages/sci/src/vm/kernels/index.d.ts +8 -0
- package/packages/sci/src/vm/kernels/index.js +29 -0
- package/packages/sci/src/vm/kernels/index.ts +32 -0
- package/packages/sci/src/vm/kernels/lists.d.ts +6 -0
- package/packages/sci/src/vm/kernels/lists.js +127 -0
- package/packages/sci/src/vm/kernels/lists.ts +111 -0
- package/packages/sci/src/vm/kernels/motion.d.ts +7 -0
- package/packages/sci/src/vm/kernels/motion.js +204 -0
- package/packages/sci/src/vm/kernels/motion.ts +194 -0
- package/packages/sci/src/vm/kernels/palette.d.ts +82 -0
- package/packages/sci/src/vm/kernels/palette.js +292 -0
- package/packages/sci/src/vm/kernels/palette.ts +303 -0
- package/packages/sci/src/vm/kernels/saves.d.ts +45 -0
- package/packages/sci/src/vm/kernels/saves.js +123 -0
- package/packages/sci/src/vm/kernels/saves.ts +149 -0
- package/packages/sci/src/vm/kernels/system.d.ts +62 -0
- package/packages/sci/src/vm/kernels/system.js +110 -0
- package/packages/sci/src/vm/kernels/system.ts +128 -0
- package/packages/sci/src/vm/kernels/text.d.ts +13 -0
- package/packages/sci/src/vm/kernels/text.js +338 -0
- package/packages/sci/src/vm/kernels/text.ts +328 -0
- package/packages/sci/src/vm/memory.d.ts +138 -0
- package/packages/sci/src/vm/memory.js +168 -0
- package/packages/sci/src/vm/memory.ts +266 -0
- package/packages/sci/src/vm/replace.d.ts +11 -0
- package/packages/sci/src/vm/replace.js +14 -0
- package/packages/sci/src/vm/replace.ts +16 -0
- package/packages/sci/src/vm/savegame.d.ts +89 -0
- package/packages/sci/src/vm/savegame.js +161 -0
- package/packages/sci/src/vm/savegame.ts +188 -0
- package/packages/sci/src/vm/value.d.ts +17 -0
- package/packages/sci/src/vm/value.js +12 -0
- package/packages/sci/src/vm/value.ts +25 -0
- package/packages/sci/src/vm/vm.d.ts +155 -0
- package/packages/sci/src/vm/vm.js +1053 -0
- package/packages/sci/src/vm/vm.ts +914 -0
- package/tools/adlib-render.d.ts +1 -0
- package/tools/adlib-render.js +49 -0
- package/tools/adlib-render.ts +42 -0
- package/tools/analyze.d.ts +1 -0
- package/tools/analyze.js +25 -0
- package/tools/analyze.ts +25 -0
- package/tools/art/build.d.ts +34 -0
- package/tools/art/build.js +175 -0
- package/tools/art/build.ts +153 -0
- package/tools/art.d.ts +1 -0
- package/tools/art.js +31 -0
- package/tools/art.ts +29 -0
- package/tools/asm.d.ts +1 -0
- package/tools/asm.js +61 -0
- package/tools/asm.ts +55 -0
- package/tools/cli.d.ts +1 -0
- package/tools/cli.js +99 -0
- package/tools/cli.ts +100 -0
- package/tools/editor-server.d.ts +29 -0
- package/tools/editor-server.js +162 -0
- package/tools/editor-server.ts +176 -0
- package/tools/explore.d.ts +1 -0
- package/tools/explore.js +305 -0
- package/tools/explore.ts +250 -0
- package/tools/game/audio.d.ts +19 -0
- package/tools/game/audio.js +143 -0
- package/tools/game/audio.ts +130 -0
- package/tools/game/build.d.ts +46 -0
- package/tools/game/build.js +259 -0
- package/tools/game/build.ts +283 -0
- package/tools/game/defaults.d.ts +26 -0
- package/tools/game/defaults.js +82 -0
- package/tools/game/defaults.ts +83 -0
- package/tools/game/font.d.ts +3 -0
- package/tools/game/font.js +124 -0
- package/tools/game/font.ts +126 -0
- package/tools/game/kit.d.ts +7 -0
- package/tools/game/kit.js +7 -0
- package/tools/game/kit.ts +10 -0
- package/tools/game/new.d.ts +8 -0
- package/tools/game/new.js +39 -0
- package/tools/game/new.ts +38 -0
- package/tools/game/target.d.ts +7 -0
- package/tools/game/target.js +41 -0
- package/tools/game/target.ts +42 -0
- package/tools/game/template/resources.ts.tmpl +43 -0
- package/tools/game/template/rooms/1.room.yaml +9 -0
- package/tools/game/template/rooms/1.yarn +17 -0
- package/tools/game/template/scripts/0.sc +24 -0
- package/tools/game.d.ts +1 -0
- package/tools/game.js +77 -0
- package/tools/game.ts +70 -0
- package/tools/mod-build.d.ts +28 -0
- package/tools/mod-build.js +173 -0
- package/tools/mod-build.ts +179 -0
- package/tools/mod-target.d.ts +16 -0
- package/tools/mod-target.js +26 -0
- package/tools/mod-target.ts +32 -0
- package/tools/mod.d.ts +1 -0
- package/tools/mod.js +47 -0
- package/tools/mod.ts +45 -0
- package/tools/msg.d.ts +1 -0
- package/tools/msg.js +40 -0
- package/tools/msg.ts +36 -0
- package/tools/node-files.d.ts +3 -0
- package/tools/node-files.js +34 -0
- package/tools/node-files.ts +33 -0
- package/tools/patches.d.ts +3 -0
- package/tools/patches.js +13 -0
- package/tools/patches.ts +13 -0
- package/tools/pic-build.d.ts +9 -0
- package/tools/pic-build.js +26 -0
- package/tools/pic-build.ts +27 -0
- package/tools/pic.d.ts +1 -0
- package/tools/pic.js +64 -0
- package/tools/pic.ts +61 -0
- package/tools/play.d.ts +1 -0
- package/tools/play.js +54 -0
- package/tools/play.ts +54 -0
- package/tools/png.d.ts +16 -0
- package/tools/png.js +124 -0
- package/tools/png.ts +123 -0
- package/tools/res.d.ts +1 -0
- package/tools/res.js +48 -0
- package/tools/res.ts +50 -0
- package/tools/scenario.d.ts +1 -0
- package/tools/scenario.js +74 -0
- package/tools/scenario.ts +80 -0
- package/tools/site/build.d.ts +8 -0
- package/tools/site/build.js +110 -0
- package/tools/site/build.ts +116 -0
- package/tools/site/markdown.d.ts +11 -0
- package/tools/site/markdown.js +88 -0
- package/tools/site/markdown.ts +84 -0
- package/tools/view-build.d.ts +34 -0
- package/tools/view-build.js +47 -0
- package/tools/view-build.ts +71 -0
- package/tools/view.d.ts +1 -0
- package/tools/view.js +54 -0
- package/tools/view.ts +52 -0
package/tools/asm.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCI2 assembly.
|
|
3
|
+
* pnpm asm dump <script> [file.sca] write a script as assembly text
|
|
4
|
+
* pnpm asm build <file.sca> [outDir] assemble into <n>.SCR + <n>.HEP patch files
|
|
5
|
+
* pnpm asm roundtrip [script...] disassemble + reassemble, compare bytes (all by default)
|
|
6
|
+
*
|
|
7
|
+
* Mods (diffs against these dumps) are built with `pnpm mod`, see tools/mod.ts.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { ResourceManager, ResourceType, ScriptWorld, assemble, toAssembly } from "../packages/sci/src/index.ts";
|
|
11
|
+
import { nodeFiles } from "./node-files.ts";
|
|
12
|
+
import { writePatch } from "./patches.ts";
|
|
13
|
+
|
|
14
|
+
const [command = "roundtrip", ...args] = process.argv.slice(2);
|
|
15
|
+
const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
|
|
16
|
+
const world = await ScriptWorld.open(rm);
|
|
17
|
+
await world.loadAllClasses();
|
|
18
|
+
const ctx = world.asmContext();
|
|
19
|
+
|
|
20
|
+
const firstDiff = (a: Uint8Array, b: Uint8Array) => {
|
|
21
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) if (a[i] !== b[i]) return i;
|
|
22
|
+
return -1;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (command === "dump") {
|
|
26
|
+
const text = toAssembly(await world.script(Number(args[0])), ctx);
|
|
27
|
+
if (args[1]) writeFileSync(args[1], text);
|
|
28
|
+
else process.stdout.write(text);
|
|
29
|
+
} else if (command === "build") {
|
|
30
|
+
build(readFileSync(args[0]!, "utf8"), args[1] ?? "out/mods");
|
|
31
|
+
} else if (command === "roundtrip") {
|
|
32
|
+
const numbers = args.length ? args.map(Number) : rm.list(ResourceType.Script).map((r) => r.number).filter((n) => rm.has({ type: ResourceType.Heap, number: n }));
|
|
33
|
+
let ok = 0;
|
|
34
|
+
const failures: string[] = [];
|
|
35
|
+
for (const n of numbers) {
|
|
36
|
+
const s = await world.script(n);
|
|
37
|
+
try {
|
|
38
|
+
const out = assemble(toAssembly(s, ctx), ctx);
|
|
39
|
+
const dc = firstDiff(s.code, out.code), dh = firstDiff(s.heap, out.heap);
|
|
40
|
+
if (dc < 0 && dh < 0) ok++;
|
|
41
|
+
else failures.push(`${n}: ${dc >= 0 ? `code differs at $${dc.toString(16)} (${s.code.length} vs ${out.code.length} bytes)` : ""} ${dh >= 0 ? `heap differs at $${dh.toString(16)} (${s.heap.length} vs ${out.heap.length} bytes)` : ""}`);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
failures.push(`${n}: ${(e as Error).message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
console.log(`${ok}/${numbers.length} scripts round-trip byte-exact`);
|
|
47
|
+
if (failures.length) console.log(failures.slice(0, 20).join("\n"));
|
|
48
|
+
process.exitCode = failures.length ? 1 : 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function build(text: string, dir: string) {
|
|
52
|
+
const out = assemble(text, ctx);
|
|
53
|
+
writePatch(dir, ResourceType.Script, out.number, out.code);
|
|
54
|
+
writePatch(dir, ResourceType.Heap, out.number, out.heap);
|
|
55
|
+
}
|
package/tools/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/tools/cli.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sci-ts: the command for making games with sci-ts. Run it in a game's folder, or give it one.
|
|
3
|
+
*
|
|
4
|
+
* sci-ts new <dir> a new game: a room in YAML and Yarn, the game, placeholder art
|
|
5
|
+
* sci-ts build [dir] [--out <dir>] RESOURCE.MAP, RESOURCE.000 (and .SFX) in <dir>/out/game
|
|
6
|
+
* sci-ts play [dir] [--port <n>] build it, then serve the player for it
|
|
7
|
+
* sci-ts edit [dir] [--port <n>] build it, then serve the live editor for its rooms
|
|
8
|
+
* sci-ts site [dir] [--out <dir>] a static site: the game's README and docs, and the game
|
|
9
|
+
* in the player (in <dir>/out/site)
|
|
10
|
+
* sci-ts art check|build <art.json> [out] check or build editor-made art (tools/art.ts)
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
15
|
+
import { writeResourceArchive } from "../packages/sci/src/index.js";
|
|
16
|
+
import { GameBuildError, buildGame } from "./game/build.js";
|
|
17
|
+
import { newGame } from "./game/new.js";
|
|
18
|
+
import { buildSite } from "./site/build.js";
|
|
19
|
+
const VIEWER = resolve(import.meta.dirname, "../apps/viewer");
|
|
20
|
+
const usage = `usage: sci-ts new <dir>
|
|
21
|
+
sci-ts build|play|edit|site [dir] [--out <dir>] [--port <n>]
|
|
22
|
+
sci-ts art check|build <art.json> [out]`;
|
|
23
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
24
|
+
const option = (name) => {
|
|
25
|
+
const i = rest.indexOf(`--${name}`);
|
|
26
|
+
return i >= 0 ? rest.splice(i, 2)[1] : undefined;
|
|
27
|
+
};
|
|
28
|
+
const out = option("out"), port = option("port");
|
|
29
|
+
const dir = resolve(rest[0] ?? ".");
|
|
30
|
+
const shown = (p) => relative(process.cwd(), p) || ".";
|
|
31
|
+
/** Builds the game in `dir` into `to`; prints warnings and a summary. */
|
|
32
|
+
async function build(to) {
|
|
33
|
+
const game = await buildGame(dir);
|
|
34
|
+
const { map, volume } = writeResourceArchive(game.resources);
|
|
35
|
+
mkdirSync(to, { recursive: true });
|
|
36
|
+
writeFileSync(join(to, "RESOURCE.MAP"), map);
|
|
37
|
+
writeFileSync(join(to, "RESOURCE.000"), volume);
|
|
38
|
+
for (const [name, data] of Object.entries(game.files))
|
|
39
|
+
writeFileSync(join(to, name), data);
|
|
40
|
+
for (const w of game.warnings)
|
|
41
|
+
console.warn(`warning: ${w}`);
|
|
42
|
+
console.log(`${shown(to)}: ${game.resources.length} resources, ${game.classes.size} classes`);
|
|
43
|
+
}
|
|
44
|
+
/** Serves the viewer app for the game in `dir`, built into `to`; `page` is what to open. */
|
|
45
|
+
async function serve(to, page) {
|
|
46
|
+
process.env.SCI_GAME = to;
|
|
47
|
+
process.env.SCI_GAME_SOURCE = dir;
|
|
48
|
+
const { createServer } = await import("vite");
|
|
49
|
+
const server = await createServer({ root: VIEWER, configFile: join(VIEWER, "vite.config.ts"), server: { port: port ? Number(port) : 5173 } });
|
|
50
|
+
await server.listen();
|
|
51
|
+
const url = server.resolvedUrls?.local[0] ?? "http://localhost:5173/";
|
|
52
|
+
console.log(`${basename(dir)}: ${url}${page}`);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
if (["build", "play", "edit", "site"].includes(command ?? "") && !existsSync(join(dir, "scripts"))) {
|
|
56
|
+
throw new GameBuildError(`${shown(dir)}: no game here (no scripts/ folder); sci-ts new <dir> starts one`);
|
|
57
|
+
}
|
|
58
|
+
switch (command) {
|
|
59
|
+
case "new": {
|
|
60
|
+
if (!rest[0])
|
|
61
|
+
throw new GameBuildError(usage);
|
|
62
|
+
const target = newGame(rest[0]);
|
|
63
|
+
console.log(`${shown(target)}: a room (rooms/1.room.yaml, 1.yarn), the game (scripts/0.sc) and placeholder art (resources.ts)`);
|
|
64
|
+
console.log(shown(target) === "." ? " sci-ts play" : ` cd ${shown(target)} && sci-ts play`);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case "build":
|
|
68
|
+
await build(resolve(out ?? join(dir, "out/game")));
|
|
69
|
+
break;
|
|
70
|
+
case "play":
|
|
71
|
+
case "edit": {
|
|
72
|
+
const to = join(dir, "out/game");
|
|
73
|
+
await build(to);
|
|
74
|
+
await serve(to, command === "play" ? "play.html" : `editor.html?mod=${basename(dir)}`);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case "site": {
|
|
78
|
+
const to = resolve(out ?? join(dir, "out/site"));
|
|
79
|
+
const site = await buildSite(dir, to, { docs: dir, name: basename(dir) });
|
|
80
|
+
console.log(`${shown(to)}: ${site.pages} pages, the game in play/play.html${site.soundFont ? "" : " (no SoundFont: music is silent)"}`);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case "art": {
|
|
84
|
+
// The art tool keeps its own command line; run it with this process's loader (tsx).
|
|
85
|
+
const art = spawn(process.execPath, [...process.execArgv, join(import.meta.dirname, "art.ts"), ...rest], { stdio: "inherit" });
|
|
86
|
+
art.on("exit", (code) => process.exit(code ?? 1));
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
default:
|
|
90
|
+
console.error(usage);
|
|
91
|
+
process.exit(2);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
if (!(e instanceof GameBuildError))
|
|
96
|
+
throw e;
|
|
97
|
+
console.error(e.message);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
package/tools/cli.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sci-ts: the command for making games with sci-ts. Run it in a game's folder, or give it one.
|
|
3
|
+
*
|
|
4
|
+
* sci-ts new <dir> a new game: a room in YAML and Yarn, the game, placeholder art
|
|
5
|
+
* sci-ts build [dir] [--out <dir>] RESOURCE.MAP, RESOURCE.000 (and .SFX) in <dir>/out/game
|
|
6
|
+
* sci-ts play [dir] [--port <n>] build it, then serve the player for it
|
|
7
|
+
* sci-ts edit [dir] [--port <n>] build it, then serve the live editor for its rooms
|
|
8
|
+
* sci-ts site [dir] [--out <dir>] a static site: the game's README and docs, and the game
|
|
9
|
+
* in the player (in <dir>/out/site)
|
|
10
|
+
* sci-ts art check|build <art.json> [out] check or build editor-made art (tools/art.ts)
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
15
|
+
import { writeResourceArchive } from "../packages/sci/src/index.ts";
|
|
16
|
+
import { GameBuildError, buildGame } from "./game/build.ts";
|
|
17
|
+
import { newGame } from "./game/new.ts";
|
|
18
|
+
import { buildSite } from "./site/build.ts";
|
|
19
|
+
|
|
20
|
+
const VIEWER = resolve(import.meta.dirname, "../apps/viewer");
|
|
21
|
+
const usage = `usage: sci-ts new <dir>
|
|
22
|
+
sci-ts build|play|edit|site [dir] [--out <dir>] [--port <n>]
|
|
23
|
+
sci-ts art check|build <art.json> [out]`;
|
|
24
|
+
|
|
25
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
26
|
+
const option = (name: string) => {
|
|
27
|
+
const i = rest.indexOf(`--${name}`);
|
|
28
|
+
return i >= 0 ? rest.splice(i, 2)[1] : undefined;
|
|
29
|
+
};
|
|
30
|
+
const out = option("out"), port = option("port");
|
|
31
|
+
const dir = resolve(rest[0] ?? ".");
|
|
32
|
+
const shown = (p: string) => relative(process.cwd(), p) || ".";
|
|
33
|
+
|
|
34
|
+
/** Builds the game in `dir` into `to`; prints warnings and a summary. */
|
|
35
|
+
async function build(to: string) {
|
|
36
|
+
const game = await buildGame(dir);
|
|
37
|
+
const { map, volume } = writeResourceArchive(game.resources);
|
|
38
|
+
mkdirSync(to, { recursive: true });
|
|
39
|
+
writeFileSync(join(to, "RESOURCE.MAP"), map);
|
|
40
|
+
writeFileSync(join(to, "RESOURCE.000"), volume);
|
|
41
|
+
for (const [name, data] of Object.entries(game.files)) writeFileSync(join(to, name), data);
|
|
42
|
+
for (const w of game.warnings) console.warn(`warning: ${w}`);
|
|
43
|
+
console.log(`${shown(to)}: ${game.resources.length} resources, ${game.classes.size} classes`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Serves the viewer app for the game in `dir`, built into `to`; `page` is what to open. */
|
|
47
|
+
async function serve(to: string, page: string) {
|
|
48
|
+
process.env.SCI_GAME = to;
|
|
49
|
+
process.env.SCI_GAME_SOURCE = dir;
|
|
50
|
+
const { createServer } = await import("vite");
|
|
51
|
+
const server = await createServer({ root: VIEWER, configFile: join(VIEWER, "vite.config.ts"), server: { port: port ? Number(port) : 5173 } });
|
|
52
|
+
await server.listen();
|
|
53
|
+
const url = server.resolvedUrls?.local[0] ?? "http://localhost:5173/";
|
|
54
|
+
console.log(`${basename(dir)}: ${url}${page}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
if (["build", "play", "edit", "site"].includes(command ?? "") && !existsSync(join(dir, "scripts"))) {
|
|
59
|
+
throw new GameBuildError(`${shown(dir)}: no game here (no scripts/ folder); sci-ts new <dir> starts one`);
|
|
60
|
+
}
|
|
61
|
+
switch (command) {
|
|
62
|
+
case "new": {
|
|
63
|
+
if (!rest[0]) throw new GameBuildError(usage);
|
|
64
|
+
const target = newGame(rest[0]);
|
|
65
|
+
console.log(`${shown(target)}: a room (rooms/1.room.yaml, 1.yarn), the game (scripts/0.sc) and placeholder art (resources.ts)`);
|
|
66
|
+
console.log(shown(target) === "." ? " sci-ts play" : ` cd ${shown(target)} && sci-ts play`);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "build":
|
|
70
|
+
await build(resolve(out ?? join(dir, "out/game")));
|
|
71
|
+
break;
|
|
72
|
+
case "play":
|
|
73
|
+
case "edit": {
|
|
74
|
+
const to = join(dir, "out/game");
|
|
75
|
+
await build(to);
|
|
76
|
+
await serve(to, command === "play" ? "play.html" : `editor.html?mod=${basename(dir)}`);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "site": {
|
|
80
|
+
const to = resolve(out ?? join(dir, "out/site"));
|
|
81
|
+
const site = await buildSite(dir, to, { docs: dir, name: basename(dir) });
|
|
82
|
+
console.log(`${shown(to)}: ${site.pages} pages, the game in play/play.html${site.soundFont ? "" : " (no SoundFont: music is silent)"}`);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "art": {
|
|
86
|
+
// The art tool keeps its own command line; run it with this process's loader (tsx).
|
|
87
|
+
const art = spawn(process.execPath, [...process.execArgv, join(import.meta.dirname, "art.ts"), ...rest], { stdio: "inherit" });
|
|
88
|
+
art.on("exit", (code) => process.exit(code ?? 1));
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
console.error(usage);
|
|
93
|
+
process.exit(2);
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
if (!(e instanceof GameBuildError)) throw e;
|
|
97
|
+
console.error(e.message);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare function listMods(): {
|
|
2
|
+
name: string;
|
|
3
|
+
kind: "mod" | "game";
|
|
4
|
+
rooms: number[];
|
|
5
|
+
}[];
|
|
6
|
+
export declare function readFiles(name: string): Record<string, string>;
|
|
7
|
+
export interface BuildResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
/** Everything the mod builds, base64: the page swaps it into the running game. */
|
|
10
|
+
resources?: {
|
|
11
|
+
type: number;
|
|
12
|
+
number: number;
|
|
13
|
+
data: string;
|
|
14
|
+
}[];
|
|
15
|
+
/** The compiled .sca / .msg text, for reading. */
|
|
16
|
+
generated?: Record<string, string>;
|
|
17
|
+
/** flags.yaml after the build (new variables get numbers). */
|
|
18
|
+
flags?: string;
|
|
19
|
+
/** What changed can't be swapped into a running game (its selectors): reload the page. */
|
|
20
|
+
reload?: boolean;
|
|
21
|
+
ms?: number;
|
|
22
|
+
error?: {
|
|
23
|
+
message: string;
|
|
24
|
+
file?: string;
|
|
25
|
+
line?: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Writes `files` (only room files) into the mod, then builds it. One build at a time. */
|
|
29
|
+
export declare function save(mod: string, files: Record<string, string>): Promise<BuildResult>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live editor's server side (apps/viewer/editor.html, through the dev server's
|
|
3
|
+
* /__editor routes): lists what can be edited, reads its room files, and on save writes them
|
|
4
|
+
* and builds, returning the built resources for the page to swap into the running game.
|
|
5
|
+
*
|
|
6
|
+
* What's edited depends on what the dev server serves. A game of ours (SCI_GAME=
|
|
7
|
+
* out/games/<name>): its YAML rooms, rebuilt with `pnpm game build`; the page gets the
|
|
8
|
+
* resources that changed. Sierra's game: mods, built like `pnpm mod build`, with the game's
|
|
9
|
+
* files and class tables kept loaded between builds.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { basename, join, resolve } from "node:path";
|
|
13
|
+
import { ResourceManager, ResourceType, ScriptWorld, writeResourceArchive } from "../packages/sci/src/index.js";
|
|
14
|
+
import { buildGame } from "./game/build.js";
|
|
15
|
+
import { buildMod } from "./mod-build.js";
|
|
16
|
+
import { nodeFiles } from "./node-files.js";
|
|
17
|
+
import { writePatch } from "./patches.js";
|
|
18
|
+
const ROOT = resolve(import.meta.dirname, "..");
|
|
19
|
+
const MODS = join(ROOT, "mods");
|
|
20
|
+
const GAMES = join(ROOT, "games");
|
|
21
|
+
/** The files the editor shows and may write: rooms as data; for our games, scripts and messages too. */
|
|
22
|
+
const EDITABLE = /^(\d+\.room\.yaml|\d+\.yarn|flags\.yaml)$/;
|
|
23
|
+
const GAME_EDITABLE = /^(\d+\.room\.yaml|\d+\.yarn|flags\.yaml|\d+\.sc|\d+\.msg)$/;
|
|
24
|
+
/**
|
|
25
|
+
* The game of ours the dev server serves, if it is one: SCI_GAME_SOURCE names its folder
|
|
26
|
+
* (`sci-ts edit`), or SCI_GAME is out/games/<name> for a game in games/.
|
|
27
|
+
*/
|
|
28
|
+
const served = (() => {
|
|
29
|
+
const source = process.env.SCI_GAME_SOURCE;
|
|
30
|
+
if (source && existsSync(join(source, "scripts")))
|
|
31
|
+
return { name: basename(resolve(source)), dir: resolve(source), out: resolve(process.env.SCI_GAME ?? join(source, "out/game")) };
|
|
32
|
+
const name = /(?:^|\/)out\/games\/([\w-]+)\/?$/.exec(process.env.SCI_GAME ?? "")?.[1];
|
|
33
|
+
return name && existsSync(join(GAMES, name)) ? { name, dir: join(GAMES, name), out: join(ROOT, "out/games", name) } : undefined;
|
|
34
|
+
})();
|
|
35
|
+
const servedGame = served?.name;
|
|
36
|
+
/** Where an editable thing's files are: a mod's folder, or a game's rooms/ (flags.yaml beside it). */
|
|
37
|
+
function filesDir(name) {
|
|
38
|
+
if (served && name === served.name)
|
|
39
|
+
return { dir: served.dir, game: true };
|
|
40
|
+
return { dir: modDir(name), game: false };
|
|
41
|
+
}
|
|
42
|
+
const fileIn = (where, file) => join(where.dir, !where.game || file === "flags.yaml" ? "" : file.endsWith(".sc") ? "scripts" : file.endsWith(".msg") ? "messages" : "rooms", file);
|
|
43
|
+
let game;
|
|
44
|
+
const openGame = () => (game ??= (async () => {
|
|
45
|
+
const rm = await ResourceManager.open(nodeFiles(join(ROOT, "original")));
|
|
46
|
+
const world = await ScriptWorld.open(rm);
|
|
47
|
+
await world.loadAllClasses();
|
|
48
|
+
return { rm, world, ctx: world.asmContext() };
|
|
49
|
+
})());
|
|
50
|
+
const modDir = (mod) => {
|
|
51
|
+
if (!/^[\w-]+$/.test(mod) || !existsSync(join(MODS, mod)))
|
|
52
|
+
throw new Error(`no mod called "${mod}"`);
|
|
53
|
+
return join(MODS, mod);
|
|
54
|
+
};
|
|
55
|
+
const roomsIn = (dir) => existsSync(dir) ? readdirSync(dir).flatMap((f) => /^(\d+)\.room\.yaml$/.exec(f)?.[1] ?? []).map(Number).sort((a, b) => a - b) : [];
|
|
56
|
+
export function listMods() {
|
|
57
|
+
if (servedGame) {
|
|
58
|
+
// Rooms as data, and scripts (besides 0, the game) that may be rooms.
|
|
59
|
+
const scripts = existsSync(join(served.dir, "scripts"))
|
|
60
|
+
? readdirSync(join(served.dir, "scripts")).flatMap((f) => /^(\d+)\.sc$/.exec(f)?.[1] ?? []).map(Number).filter((n) => n !== 0)
|
|
61
|
+
: [];
|
|
62
|
+
const rooms = [...new Set([...roomsIn(join(served.dir, "rooms")), ...scripts])].sort((a, b) => a - b);
|
|
63
|
+
return [{ name: servedGame, kind: "game", rooms }];
|
|
64
|
+
}
|
|
65
|
+
return readdirSync(MODS)
|
|
66
|
+
.filter((d) => statSync(join(MODS, d)).isDirectory())
|
|
67
|
+
.map((name) => ({ name, kind: "mod", rooms: roomsIn(join(MODS, name)) }));
|
|
68
|
+
}
|
|
69
|
+
export function readFiles(name) {
|
|
70
|
+
const where = filesDir(name);
|
|
71
|
+
const list = (sub) => (existsSync(join(where.dir, sub)) ? readdirSync(join(where.dir, sub)) : []);
|
|
72
|
+
const names = where.game
|
|
73
|
+
? [...list("rooms"), ...list("scripts"), ...list("messages"), ...(existsSync(join(where.dir, "flags.yaml")) ? ["flags.yaml"] : [])]
|
|
74
|
+
: readdirSync(where.dir);
|
|
75
|
+
const editable = where.game ? GAME_EDITABLE : EDITABLE;
|
|
76
|
+
return Object.fromEntries(names.filter((f) => editable.test(f)).map((f) => [f, readFileSync(fileIn(where, f), "utf8")]));
|
|
77
|
+
}
|
|
78
|
+
let queue = Promise.resolve();
|
|
79
|
+
/** Writes `files` (only room files) into the mod, then builds it. One build at a time. */
|
|
80
|
+
export function save(mod, files) {
|
|
81
|
+
const run = async () => {
|
|
82
|
+
const started = Date.now();
|
|
83
|
+
try {
|
|
84
|
+
const where = filesDir(mod);
|
|
85
|
+
for (const [name, text] of Object.entries(files)) {
|
|
86
|
+
if (!(where.game ? GAME_EDITABLE : EDITABLE).test(name))
|
|
87
|
+
throw new Error(`the editor doesn't write ${name}`);
|
|
88
|
+
const path = fileIn(where, name);
|
|
89
|
+
if (!existsSync(path) || readFileSync(path, "utf8") !== text)
|
|
90
|
+
writeFileSync(path, text);
|
|
91
|
+
}
|
|
92
|
+
if (where.game)
|
|
93
|
+
return { ...(await buildServedGame()), ms: Date.now() - started };
|
|
94
|
+
const dir = where.dir;
|
|
95
|
+
const { rm, world, ctx } = await openGame();
|
|
96
|
+
const generated = {};
|
|
97
|
+
const built = await buildMod(dir, rm, world, ctx, (file, text) => (generated[file] = text));
|
|
98
|
+
const out = join(ROOT, "out/mods", mod);
|
|
99
|
+
for (const r of built)
|
|
100
|
+
writePatch(out, r.type, r.number, r.data);
|
|
101
|
+
const flagsFile = join(dir, "flags.yaml");
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
resources: built.map((r) => ({ type: r.type, number: r.number, data: Buffer.from(r.data).toString("base64") })),
|
|
105
|
+
generated,
|
|
106
|
+
flags: existsSync(flagsFile) ? readFileSync(flagsFile, "utf8") : undefined,
|
|
107
|
+
ms: Date.now() - started,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
const message = e.message;
|
|
112
|
+
// Compiler errors start with file:line.
|
|
113
|
+
const at = /^(?:.*\/)?([^/:\s]+):(\d+):/.exec(message);
|
|
114
|
+
// Paths in messages are relative to the dev server's folder (apps/viewer): from the repository's instead.
|
|
115
|
+
const shown = message.replaceAll(ROOT + "/", "").replace(/^(\.\.\/)+/, "");
|
|
116
|
+
return { ok: false, error: { message: shown, file: at?.[1], line: at ? Number(at[2]) : undefined }, ms: Date.now() - started };
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const result = queue.then(run, run);
|
|
120
|
+
queue = result;
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Rebuilds the game being served into its out folder, and returns what changed since the files
|
|
125
|
+
* there (which the page is running).
|
|
126
|
+
*/
|
|
127
|
+
async function buildServedGame() {
|
|
128
|
+
const game = await buildGame(served.dir);
|
|
129
|
+
const out = served.out;
|
|
130
|
+
const changed = [];
|
|
131
|
+
if (existsSync(join(out, "RESOURCE.MAP"))) {
|
|
132
|
+
const before = await ResourceManager.open(nodeFiles(out), []);
|
|
133
|
+
for (const r of game.resources) {
|
|
134
|
+
const old = before.has(r) ? (await before.load(r)).data : undefined;
|
|
135
|
+
if (!old || Buffer.compare(Buffer.from(old), Buffer.from(r.data)) !== 0)
|
|
136
|
+
changed.push(r);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else
|
|
140
|
+
changed.push(...game.resources);
|
|
141
|
+
const { map, volume } = writeResourceArchive(game.resources);
|
|
142
|
+
mkdirSync(out, { recursive: true });
|
|
143
|
+
writeFileSync(join(out, "RESOURCE.MAP"), map);
|
|
144
|
+
writeFileSync(join(out, "RESOURCE.000"), volume);
|
|
145
|
+
for (const [file, data] of Object.entries(game.files))
|
|
146
|
+
writeFileSync(join(out, file), data);
|
|
147
|
+
// The running VM read its selector and class tables when it started.
|
|
148
|
+
const reload = changed.some((r) => r.type === ResourceType.Vocab);
|
|
149
|
+
const generated = {};
|
|
150
|
+
for (const [n, text] of game.generated)
|
|
151
|
+
generated[`${n}.sca`] = text;
|
|
152
|
+
for (const [n, text] of game.roomMessages)
|
|
153
|
+
generated[`${n}.msg`] = text;
|
|
154
|
+
const flagsFile = join(served.dir, "flags.yaml");
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
resources: changed.map((r) => ({ type: r.type, number: r.number, data: Buffer.from(r.data).toString("base64") })),
|
|
158
|
+
generated,
|
|
159
|
+
flags: existsSync(flagsFile) ? readFileSync(flagsFile, "utf8") : undefined,
|
|
160
|
+
reload: reload || undefined,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live editor's server side (apps/viewer/editor.html, through the dev server's
|
|
3
|
+
* /__editor routes): lists what can be edited, reads its room files, and on save writes them
|
|
4
|
+
* and builds, returning the built resources for the page to swap into the running game.
|
|
5
|
+
*
|
|
6
|
+
* What's edited depends on what the dev server serves. A game of ours (SCI_GAME=
|
|
7
|
+
* out/games/<name>): its YAML rooms, rebuilt with `pnpm game build`; the page gets the
|
|
8
|
+
* resources that changed. Sierra's game: mods, built like `pnpm mod build`, with the game's
|
|
9
|
+
* files and class tables kept loaded between builds.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { basename, join, resolve } from "node:path";
|
|
13
|
+
import { ResourceManager, ResourceType, ScriptWorld, writeResourceArchive, type AsmContext } from "../packages/sci/src/index.ts";
|
|
14
|
+
import { buildGame } from "./game/build.ts";
|
|
15
|
+
import { buildMod, type BuiltResource } from "./mod-build.ts";
|
|
16
|
+
import { nodeFiles } from "./node-files.ts";
|
|
17
|
+
import { writePatch } from "./patches.ts";
|
|
18
|
+
|
|
19
|
+
const ROOT = resolve(import.meta.dirname, "..");
|
|
20
|
+
const MODS = join(ROOT, "mods");
|
|
21
|
+
const GAMES = join(ROOT, "games");
|
|
22
|
+
/** The files the editor shows and may write: rooms as data; for our games, scripts and messages too. */
|
|
23
|
+
const EDITABLE = /^(\d+\.room\.yaml|\d+\.yarn|flags\.yaml)$/;
|
|
24
|
+
const GAME_EDITABLE = /^(\d+\.room\.yaml|\d+\.yarn|flags\.yaml|\d+\.sc|\d+\.msg)$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The game of ours the dev server serves, if it is one: SCI_GAME_SOURCE names its folder
|
|
28
|
+
* (`sci-ts edit`), or SCI_GAME is out/games/<name> for a game in games/.
|
|
29
|
+
*/
|
|
30
|
+
const served = (() => {
|
|
31
|
+
const source = process.env.SCI_GAME_SOURCE;
|
|
32
|
+
if (source && existsSync(join(source, "scripts"))) return { name: basename(resolve(source)), dir: resolve(source), out: resolve(process.env.SCI_GAME ?? join(source, "out/game")) };
|
|
33
|
+
const name = /(?:^|\/)out\/games\/([\w-]+)\/?$/.exec(process.env.SCI_GAME ?? "")?.[1];
|
|
34
|
+
return name && existsSync(join(GAMES, name)) ? { name, dir: join(GAMES, name), out: join(ROOT, "out/games", name) } : undefined;
|
|
35
|
+
})();
|
|
36
|
+
const servedGame = served?.name;
|
|
37
|
+
|
|
38
|
+
/** Where an editable thing's files are: a mod's folder, or a game's rooms/ (flags.yaml beside it). */
|
|
39
|
+
function filesDir(name: string): { dir: string; game: boolean } {
|
|
40
|
+
if (served && name === served.name) return { dir: served.dir, game: true };
|
|
41
|
+
return { dir: modDir(name), game: false };
|
|
42
|
+
}
|
|
43
|
+
const fileIn = (where: { dir: string; game: boolean }, file: string) =>
|
|
44
|
+
join(where.dir, !where.game || file === "flags.yaml" ? "" : file.endsWith(".sc") ? "scripts" : file.endsWith(".msg") ? "messages" : "rooms", file);
|
|
45
|
+
|
|
46
|
+
let game: Promise<{ rm: ResourceManager; world: ScriptWorld; ctx: AsmContext }> | undefined;
|
|
47
|
+
const openGame = () =>
|
|
48
|
+
(game ??= (async () => {
|
|
49
|
+
const rm = await ResourceManager.open(nodeFiles(join(ROOT, "original")));
|
|
50
|
+
const world = await ScriptWorld.open(rm);
|
|
51
|
+
await world.loadAllClasses();
|
|
52
|
+
return { rm, world, ctx: world.asmContext() };
|
|
53
|
+
})());
|
|
54
|
+
|
|
55
|
+
const modDir = (mod: string) => {
|
|
56
|
+
if (!/^[\w-]+$/.test(mod) || !existsSync(join(MODS, mod))) throw new Error(`no mod called "${mod}"`);
|
|
57
|
+
return join(MODS, mod);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const roomsIn = (dir: string) =>
|
|
61
|
+
existsSync(dir) ? readdirSync(dir).flatMap((f) => /^(\d+)\.room\.yaml$/.exec(f)?.[1] ?? []).map(Number).sort((a, b) => a - b) : [];
|
|
62
|
+
|
|
63
|
+
export function listMods(): { name: string; kind: "mod" | "game"; rooms: number[] }[] {
|
|
64
|
+
if (servedGame) {
|
|
65
|
+
// Rooms as data, and scripts (besides 0, the game) that may be rooms.
|
|
66
|
+
const scripts = existsSync(join(served!.dir, "scripts"))
|
|
67
|
+
? readdirSync(join(served!.dir, "scripts")).flatMap((f) => /^(\d+)\.sc$/.exec(f)?.[1] ?? []).map(Number).filter((n) => n !== 0)
|
|
68
|
+
: [];
|
|
69
|
+
const rooms = [...new Set([...roomsIn(join(served!.dir, "rooms")), ...scripts])].sort((a, b) => a - b);
|
|
70
|
+
return [{ name: servedGame, kind: "game", rooms }];
|
|
71
|
+
}
|
|
72
|
+
return readdirSync(MODS)
|
|
73
|
+
.filter((d) => statSync(join(MODS, d)).isDirectory())
|
|
74
|
+
.map((name) => ({ name, kind: "mod" as const, rooms: roomsIn(join(MODS, name)) }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function readFiles(name: string): Record<string, string> {
|
|
78
|
+
const where = filesDir(name);
|
|
79
|
+
const list = (sub: string) => (existsSync(join(where.dir, sub)) ? readdirSync(join(where.dir, sub)) : []);
|
|
80
|
+
const names = where.game
|
|
81
|
+
? [...list("rooms"), ...list("scripts"), ...list("messages"), ...(existsSync(join(where.dir, "flags.yaml")) ? ["flags.yaml"] : [])]
|
|
82
|
+
: readdirSync(where.dir);
|
|
83
|
+
const editable = where.game ? GAME_EDITABLE : EDITABLE;
|
|
84
|
+
return Object.fromEntries(names.filter((f) => editable.test(f)).map((f) => [f, readFileSync(fileIn(where, f), "utf8")]));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface BuildResult {
|
|
88
|
+
ok: boolean;
|
|
89
|
+
/** Everything the mod builds, base64: the page swaps it into the running game. */
|
|
90
|
+
resources?: { type: number; number: number; data: string }[];
|
|
91
|
+
/** The compiled .sca / .msg text, for reading. */
|
|
92
|
+
generated?: Record<string, string>;
|
|
93
|
+
/** flags.yaml after the build (new variables get numbers). */
|
|
94
|
+
flags?: string;
|
|
95
|
+
/** What changed can't be swapped into a running game (its selectors): reload the page. */
|
|
96
|
+
reload?: boolean;
|
|
97
|
+
ms?: number;
|
|
98
|
+
error?: { message: string; file?: string; line?: number };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let queue: Promise<unknown> = Promise.resolve();
|
|
102
|
+
|
|
103
|
+
/** Writes `files` (only room files) into the mod, then builds it. One build at a time. */
|
|
104
|
+
export function save(mod: string, files: Record<string, string>): Promise<BuildResult> {
|
|
105
|
+
const run = async (): Promise<BuildResult> => {
|
|
106
|
+
const started = Date.now();
|
|
107
|
+
try {
|
|
108
|
+
const where = filesDir(mod);
|
|
109
|
+
for (const [name, text] of Object.entries(files)) {
|
|
110
|
+
if (!(where.game ? GAME_EDITABLE : EDITABLE).test(name)) throw new Error(`the editor doesn't write ${name}`);
|
|
111
|
+
const path = fileIn(where, name);
|
|
112
|
+
if (!existsSync(path) || readFileSync(path, "utf8") !== text) writeFileSync(path, text);
|
|
113
|
+
}
|
|
114
|
+
if (where.game) return { ...(await buildServedGame()), ms: Date.now() - started };
|
|
115
|
+
const dir = where.dir;
|
|
116
|
+
const { rm, world, ctx } = await openGame();
|
|
117
|
+
const generated: Record<string, string> = {};
|
|
118
|
+
const built: BuiltResource[] = await buildMod(dir, rm, world, ctx, (file, text) => (generated[file] = text));
|
|
119
|
+
const out = join(ROOT, "out/mods", mod);
|
|
120
|
+
for (const r of built) writePatch(out, r.type, r.number, r.data);
|
|
121
|
+
const flagsFile = join(dir, "flags.yaml");
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
resources: built.map((r) => ({ type: r.type, number: r.number, data: Buffer.from(r.data).toString("base64") })),
|
|
125
|
+
generated,
|
|
126
|
+
flags: existsSync(flagsFile) ? readFileSync(flagsFile, "utf8") : undefined,
|
|
127
|
+
ms: Date.now() - started,
|
|
128
|
+
};
|
|
129
|
+
} catch (e) {
|
|
130
|
+
const message = (e as Error).message;
|
|
131
|
+
// Compiler errors start with file:line.
|
|
132
|
+
const at = /^(?:.*\/)?([^/:\s]+):(\d+):/.exec(message);
|
|
133
|
+
// Paths in messages are relative to the dev server's folder (apps/viewer): from the repository's instead.
|
|
134
|
+
const shown = message.replaceAll(ROOT + "/", "").replace(/^(\.\.\/)+/, "");
|
|
135
|
+
return { ok: false, error: { message: shown, file: at?.[1], line: at ? Number(at[2]) : undefined }, ms: Date.now() - started };
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const result = queue.then(run, run);
|
|
139
|
+
queue = result;
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Rebuilds the game being served into its out folder, and returns what changed since the files
|
|
145
|
+
* there (which the page is running).
|
|
146
|
+
*/
|
|
147
|
+
async function buildServedGame(): Promise<BuildResult> {
|
|
148
|
+
const game = await buildGame(served!.dir);
|
|
149
|
+
const out = served!.out;
|
|
150
|
+
const changed: typeof game.resources = [];
|
|
151
|
+
if (existsSync(join(out, "RESOURCE.MAP"))) {
|
|
152
|
+
const before = await ResourceManager.open(nodeFiles(out), []);
|
|
153
|
+
for (const r of game.resources) {
|
|
154
|
+
const old = before.has(r) ? (await before.load(r)).data : undefined;
|
|
155
|
+
if (!old || Buffer.compare(Buffer.from(old), Buffer.from(r.data)) !== 0) changed.push(r);
|
|
156
|
+
}
|
|
157
|
+
} else changed.push(...game.resources);
|
|
158
|
+
const { map, volume } = writeResourceArchive(game.resources);
|
|
159
|
+
mkdirSync(out, { recursive: true });
|
|
160
|
+
writeFileSync(join(out, "RESOURCE.MAP"), map);
|
|
161
|
+
writeFileSync(join(out, "RESOURCE.000"), volume);
|
|
162
|
+
for (const [file, data] of Object.entries(game.files)) writeFileSync(join(out, file), data);
|
|
163
|
+
// The running VM read its selector and class tables when it started.
|
|
164
|
+
const reload = changed.some((r) => r.type === ResourceType.Vocab);
|
|
165
|
+
const generated: Record<string, string> = {};
|
|
166
|
+
for (const [n, text] of game.generated) generated[`${n}.sca`] = text;
|
|
167
|
+
for (const [n, text] of game.roomMessages) generated[`${n}.msg`] = text;
|
|
168
|
+
const flagsFile = join(served!.dir, "flags.yaml");
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
resources: changed.map((r) => ({ type: r.type, number: r.number, data: Buffer.from(r.data).toString("base64") })),
|
|
172
|
+
generated,
|
|
173
|
+
flags: existsSync(flagsFile) ? readFileSync(flagsFile, "utf8") : undefined,
|
|
174
|
+
reload: reload || undefined,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|