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/explore.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless exploration from a checkpoint.
|
|
3
|
+
* pnpm tsx tools/explore.ts <checkpoint> [actions...] [--frames N] [--png-every K]
|
|
4
|
+
*
|
|
5
|
+
* Checkpoints live in out/checkpoints/<name>.bin (a VM snapshot). "cave-start" (a new Wizard in
|
|
6
|
+
* the first cave) and "cave-exit" (the cave solved, room 720) are built on first use by
|
|
7
|
+
* playing the opening. SCI_MODS=out/mods/<name> loads built mods (see tools/mod.ts); checkpoints
|
|
8
|
+
* then are rebuilt each run, since a snapshot only fits the scripts it was made with.
|
|
9
|
+
*
|
|
10
|
+
* Actions run in order, each after the previous one's wait:
|
|
11
|
+
* click:x,y move:x,y key:text enter wait:N (frames) icon:walk|look|do|talk|cast
|
|
12
|
+
* save:<name> (writes a new checkpoint from the current state)
|
|
13
|
+
* props:<object>,<prop>,... planes items snap / diff (frame index + palette diff)
|
|
14
|
+
* room:<n> (jump there, as the player's #room/<n>) alt:<letter> (Alt+key; --debug enables
|
|
15
|
+
* Sierra's debug room: alt:t teleports, alt:h sets the hour) fkey:<n> (F1-F10; F7 restore)
|
|
16
|
+
* objs:<name>,<prop>,... (every instance, full references) fakesaves:<n> (fill the save list)
|
|
17
|
+
* loadsaves:<file.json> (saves from the player "Export saves" button; restores the newest)
|
|
18
|
+
* WATCH=<object>.<prop> logs every change to a property with the send that was running.
|
|
19
|
+
* Coordinates are screen coordinates; room features are local to the room plane (y - 10).
|
|
20
|
+
* KTRACE=<regex> logs matching kernel calls; --trace-sends <regex> logs method calls.
|
|
21
|
+
* Every message the game shows is printed with its text.
|
|
22
|
+
*/
|
|
23
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { relative } from "node:path";
|
|
25
|
+
import { deserialize, serialize } from "node:v8";
|
|
26
|
+
import { EventType, ResourceManager, ResourceType, Vm, allKernels, audio, findMessage, hostFiles, saves, readingTime, requestRoom, graphics, input, parseMessages, restore, restoreGame, savesFromJson, snapshot, stringHelpers, } from "../packages/sci/src/index.js";
|
|
27
|
+
import { nodeFiles } from "./node-files.js";
|
|
28
|
+
import { framePng } from "./png.js";
|
|
29
|
+
const argv = process.argv.slice(2);
|
|
30
|
+
const opt = (name) => (argv.includes(name) ? argv[argv.indexOf(name) + 1] : undefined);
|
|
31
|
+
const checkpoint = argv[0] ?? "cave-exit";
|
|
32
|
+
const valued = new Set(["--frames", "--png-every", "--trace-sends"]);
|
|
33
|
+
const actions = argv.slice(1).filter((a, i, all) => !a.startsWith("--") && !valued.has(all[i]) && !valued.has(all[i - 1]));
|
|
34
|
+
const pngEvery = Number(opt("--png-every") ?? 0);
|
|
35
|
+
const tail = Number(opt("--frames") ?? 300);
|
|
36
|
+
// SCI_MODS=out/mods/<name>[,...] loads built mods over the game (see tools/mod.ts).
|
|
37
|
+
const gameDir = process.env.SCI_GAME ?? "original";
|
|
38
|
+
const mods = (process.env.SCI_MODS ?? "").split(",").filter(Boolean).map((dir) => relative(gameDir, dir));
|
|
39
|
+
const rm = await ResourceManager.open(nodeFiles(gameDir), ["", "PATCHES", ...mods]);
|
|
40
|
+
await rm.preload();
|
|
41
|
+
const vm = new Vm(rm);
|
|
42
|
+
vm.registerKernels(allKernels);
|
|
43
|
+
// --debug: Sierra's debug room (it loads when "18.scr" exists), as the player's ?debug.
|
|
44
|
+
if (argv.includes("--debug"))
|
|
45
|
+
hostFiles(vm).add("18.scr");
|
|
46
|
+
// As in the browser: with mods, lines without recordings stay up long enough to read.
|
|
47
|
+
if (mods.length)
|
|
48
|
+
audio(vm).missingSpeechTicks = readingTime(vm);
|
|
49
|
+
const g = graphics(vm);
|
|
50
|
+
vm.clock = () => (g.frames * 1000) / 60;
|
|
51
|
+
const inp = input(vm);
|
|
52
|
+
const files = new Map();
|
|
53
|
+
const text = (m, noun, verb, cond, seq) => {
|
|
54
|
+
if (!files.has(m))
|
|
55
|
+
files.set(m, parseMessages(rm.loadSync({ type: ResourceType.Message, number: m }).data));
|
|
56
|
+
return findMessage(files.get(m), { noun, verb, cond, seq })?.text ?? "(no text)";
|
|
57
|
+
};
|
|
58
|
+
const kernelStats = new Map();
|
|
59
|
+
vm.onKernelCall = (name, args, result) => {
|
|
60
|
+
// KDUMP=<kernel>: after each call, show what its reference arguments point at (debugging).
|
|
61
|
+
if (process.env.KDUMP === name)
|
|
62
|
+
for (const a of args) {
|
|
63
|
+
const arr = vm.memory.array(a), o = vm.memory.object(a);
|
|
64
|
+
if (arr)
|
|
65
|
+
console.log(` ${a.toString(16)}: ${arr.type}[${arr.data.length}] ${arr.data.slice(0, 12).join(",")}`);
|
|
66
|
+
else if (o)
|
|
67
|
+
console.log(` ${a.toString(16)}: object ${o.name} data=${(vm.getProp(o, "data") ?? 0).toString(16)}`);
|
|
68
|
+
}
|
|
69
|
+
if (process.env.KSTATS)
|
|
70
|
+
kernelStats.set(name, (kernelStats.get(name) ?? 0) + 1);
|
|
71
|
+
if (process.env.KTRACE && new RegExp(process.env.KTRACE).test(name))
|
|
72
|
+
console.log(` [${g.frames}] ${name}(${args.map((a) => a.toString(16)).join(", ")}) = ${result === undefined ? "-" : Number(result).toString(16)}`);
|
|
73
|
+
if (name === "Message" && args[0] === 0 && args.length === 7) {
|
|
74
|
+
const [, m = 0, n = 0, v = 0, c = 0, s = 0] = args;
|
|
75
|
+
console.log(`[${g.frames}] ${m}/${n}/${v}/${c}/${s}: ${text(m, n, v, c, s)}`);
|
|
76
|
+
}
|
|
77
|
+
// Subop 1: the next message of the current sequence, written into the buffer.
|
|
78
|
+
if (name === "Message" && args[0] === 1 && args[1] && result)
|
|
79
|
+
console.log(`[${g.frames}] ...next: ${stringHelpers.str(vm, args[1])}`);
|
|
80
|
+
};
|
|
81
|
+
// --trace-sends <regex>: log method calls on objects whose name matches, during actions.
|
|
82
|
+
const traceSends = opt("--trace-sends");
|
|
83
|
+
let tracing = false;
|
|
84
|
+
if (traceSends) {
|
|
85
|
+
const re = new RegExp(traceSends);
|
|
86
|
+
vm.onSend = (obj, sel, argc) => {
|
|
87
|
+
if (tracing && re.test(obj.name))
|
|
88
|
+
console.log(` [${g.frames}] ${obj.name} ${sel}${argc ? `(${argc})` : ""}`);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// WATCH=object.property: log every change to it, with the send that was running (debugging).
|
|
92
|
+
if (process.env.WATCH) {
|
|
93
|
+
const [objName, propName] = process.env.WATCH.split(".");
|
|
94
|
+
let last;
|
|
95
|
+
const previous = vm.onSend;
|
|
96
|
+
vm.onSend = (obj, sel, argc) => {
|
|
97
|
+
previous?.(obj, sel, argc);
|
|
98
|
+
const target = [...vm.memory.objects.values()].find((o) => o.name === objName);
|
|
99
|
+
const v = target ? vm.getProp(target.address, propName) : undefined;
|
|
100
|
+
if (v !== last)
|
|
101
|
+
console.log(` [${g.frames}] ${objName}.${propName}: ${last?.toString(16)} → ${v?.toString(16)} (before ${obj.name} ${sel})`);
|
|
102
|
+
last = v;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
mkdirSync("out/checkpoints", { recursive: true });
|
|
106
|
+
mkdirSync("out/explore", { recursive: true });
|
|
107
|
+
const path = (name) => `out/checkpoints/${name}.bin`;
|
|
108
|
+
const move = (x, y) => ([inp.x, inp.y] = [x, y]);
|
|
109
|
+
const click = (x, y) => {
|
|
110
|
+
move(x, y);
|
|
111
|
+
inp.push({ type: EventType.MouseDown, message: 0 });
|
|
112
|
+
inp.push({ type: EventType.MouseUp, message: 0 });
|
|
113
|
+
};
|
|
114
|
+
// A modal loop (a dialog waiting for a click) may poll events without drawing frames, so
|
|
115
|
+
// run in small slices and count five slices without a frame as one, letting input through.
|
|
116
|
+
let idle = 0;
|
|
117
|
+
const run = (frames) => {
|
|
118
|
+
const end = g.frames + frames;
|
|
119
|
+
while (g.frames < end && vm.running) {
|
|
120
|
+
const before = g.frames;
|
|
121
|
+
vm.run(200_000);
|
|
122
|
+
if (g.frames !== before)
|
|
123
|
+
idle = 0;
|
|
124
|
+
else if (++idle >= 5) {
|
|
125
|
+
idle = 0;
|
|
126
|
+
g.frames++;
|
|
127
|
+
if (process.env.KSTATS)
|
|
128
|
+
console.log(" (no frame drawn: modal loop?)", Object.fromEntries(kernelStats));
|
|
129
|
+
}
|
|
130
|
+
if (pngEvery && g.frames % pngEvery === 0)
|
|
131
|
+
writeFileSync(`out/explore/${String(g.frames).padStart(6, "0")}.png`, framePng(g.compose()));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
if (existsSync(path(checkpoint)) && !(mods.length && checkpoint.startsWith("cave-"))) {
|
|
135
|
+
vm.start(vm.exportAddress(0, 0), "play");
|
|
136
|
+
vm.run(); // initialise the game object before replacing memory
|
|
137
|
+
restore(vm, deserialize(readFileSync(path(checkpoint))));
|
|
138
|
+
vm.resetExecution();
|
|
139
|
+
vm.start(vm.exportAddress(0, 0), "replay");
|
|
140
|
+
run(60);
|
|
141
|
+
}
|
|
142
|
+
else if (checkpoint === "cave-exit" || checkpoint === "cave-start") {
|
|
143
|
+
// cave-start: just created a Wizard, standing in the first cave. cave-exit: solved it.
|
|
144
|
+
// Not saved when mods are loaded: a snapshot only fits the scripts it was made with.
|
|
145
|
+
const until = checkpoint === "cave-start" ? 5300 : 10200;
|
|
146
|
+
console.log(`building checkpoint ${checkpoint} (plays the opening route)...`);
|
|
147
|
+
const type = (t) => {
|
|
148
|
+
for (const ch of t)
|
|
149
|
+
inp.push({ type: EventType.KeyDown, message: ch.charCodeAt(0) });
|
|
150
|
+
inp.push({ type: EventType.KeyDown, message: 13 });
|
|
151
|
+
};
|
|
152
|
+
const script = {
|
|
153
|
+
1401: () => click(160, 60), 1800: () => click(185, 110), 2000: () => click(100, 185), 2100: () => type("Hero"),
|
|
154
|
+
2300: () => click(30, 185), 2600: () => click(170, 112), 5300: () => move(35, 2), 5360: () => click(35, 14),
|
|
155
|
+
5400: () => click(250, 140), 6400: () => move(172, 2), 6460: () => click(172, 14), 6600: () => click(122, 98),
|
|
156
|
+
6900: () => click(253, 80), 7200: () => click(253, 80), 7800: () => move(89, 2), 7860: () => click(89, 14),
|
|
157
|
+
7950: () => click(253, 80), 8600: () => move(89, 2), 8660: () => click(89, 14), 8800: () => click(156, 57),
|
|
158
|
+
};
|
|
159
|
+
vm.start(vm.exportAddress(0, 0), "play");
|
|
160
|
+
while (g.frames < until) {
|
|
161
|
+
script[g.frames]?.();
|
|
162
|
+
vm.run();
|
|
163
|
+
}
|
|
164
|
+
if (!mods.length)
|
|
165
|
+
writeFileSync(path(checkpoint), serialize(snapshot(vm)));
|
|
166
|
+
}
|
|
167
|
+
else
|
|
168
|
+
throw new Error(`no checkpoint ${checkpoint}`);
|
|
169
|
+
tracing = true;
|
|
170
|
+
let snapFrame;
|
|
171
|
+
const ALT_SCAN = Object.fromEntries([..."QWERTYUIOP"].map((k, i) => [k, 0x10 + i]).concat([..."ASDFGHJKL"].map((k, i) => [k, 0x1e + i]), [..."ZXCVBNM"].map((k, i) => [k, 0x2c + i])));
|
|
172
|
+
const ICON_X = { walk: 35, look: 60, do: 89, talk: 117, actions: 143, cast: 172 };
|
|
173
|
+
for (const a of actions) {
|
|
174
|
+
const [kind, arg = ""] = a.split(/:(.*)/);
|
|
175
|
+
const [x = 0, y = 0] = arg.split(",").map(Number);
|
|
176
|
+
switch (kind) {
|
|
177
|
+
// Hover first, like a real mouse: some UIs (the spell window) select what's highlighted.
|
|
178
|
+
case "click":
|
|
179
|
+
move(x, y);
|
|
180
|
+
run(6);
|
|
181
|
+
click(x, y);
|
|
182
|
+
run(90);
|
|
183
|
+
break;
|
|
184
|
+
case "move":
|
|
185
|
+
move(x, y);
|
|
186
|
+
run(30);
|
|
187
|
+
break;
|
|
188
|
+
case "wait":
|
|
189
|
+
run(x);
|
|
190
|
+
break;
|
|
191
|
+
case "enter":
|
|
192
|
+
inp.push({ type: EventType.KeyDown, message: 13 });
|
|
193
|
+
run(30);
|
|
194
|
+
break;
|
|
195
|
+
case "key":
|
|
196
|
+
for (const ch of arg)
|
|
197
|
+
inp.push({ type: EventType.KeyDown, message: ch.charCodeAt(0) });
|
|
198
|
+
run(30);
|
|
199
|
+
break;
|
|
200
|
+
case "icon":
|
|
201
|
+
move(ICON_X[arg], 2);
|
|
202
|
+
run(40);
|
|
203
|
+
click(ICON_X[arg], 14);
|
|
204
|
+
run(40);
|
|
205
|
+
break;
|
|
206
|
+
case "planes":
|
|
207
|
+
for (const pl of g.planes) {
|
|
208
|
+
const props = ["priority", "picture", "left", "top", "right", "bottom", "inLeft", "inTop", "inRight", "inBottom", "vanishingX", "vanishingY"].map((k) => `${k}=${g.prop(pl, k)}`);
|
|
209
|
+
console.log(` plane ${vm.memory.object(pl)?.name} ${pl.toString(16)} ${props.join(" ")}`);
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
case "items":
|
|
213
|
+
for (const it of g.items) {
|
|
214
|
+
const props = ["plane", "view", "loop", "cel", "x", "y", "z", "priority", "fixPriority", "scaleSignal", "scaleX", "scaleY", "bitmap"].map((k) => `${k}=${(g.prop(it, k) >>> 0).toString(16)}`);
|
|
215
|
+
console.log(` item ${vm.memory.object(it)?.name} ${props.join(" ")}`);
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
case "room":
|
|
219
|
+
requestRoom(vm, x);
|
|
220
|
+
run(1);
|
|
221
|
+
break; // as the player's #room/N does
|
|
222
|
+
case "png":
|
|
223
|
+
writeFileSync(`out/explore/${arg}.png`, framePng(g.compose()));
|
|
224
|
+
break; // snapshot mid-script
|
|
225
|
+
case "addsave": { // a checkpoint file (an earlier session's snapshot) as a saved game
|
|
226
|
+
const version = saves(vm).store.list()[0]?.version ?? "";
|
|
227
|
+
saves(vm).store.put({ id: 90, description: `checkpoint ${arg}`, version, date: Date.now() + 1000 }, deserialize(readFileSync(path(arg))));
|
|
228
|
+
console.log(` added checkpoint ${arg} as a save (version ${JSON.stringify(version)})`);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case "loadsaves": { // saves exported from the player (Export saves); restores the newest, as #room/N does
|
|
232
|
+
const list = savesFromJson(readFileSync(arg, "utf8"));
|
|
233
|
+
for (const s of list)
|
|
234
|
+
saves(vm).store.put(s.info, s.snapshot);
|
|
235
|
+
const newest = saves(vm).store.list()[0];
|
|
236
|
+
console.log(` loaded ${list.length} saves; restoring "${newest?.description}" (id ${newest?.id})`);
|
|
237
|
+
if (newest)
|
|
238
|
+
restoreGame(vm, saves(vm).store.get(newest.id).snapshot);
|
|
239
|
+
run(120);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "fakesaves": { // n saved games (copies of the current state), for testing the Restore dialog
|
|
243
|
+
const snap = snapshot(vm);
|
|
244
|
+
const odd = process.env.ODD_SAVES ? JSON.parse(process.env.ODD_SAVES) : [];
|
|
245
|
+
for (let i = 0; i < x; i++) {
|
|
246
|
+
const o = odd[i];
|
|
247
|
+
saves(vm).store.put({ id: 100 + i, description: o?.description ?? `Save number ${i + 1} ${"x".repeat(i % 12)}`, version: o?.version ?? "", date: Date.now() - i }, snap);
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case "fkey":
|
|
252
|
+
inp.push({ type: EventType.KeyDown, message: (0x3a + x) << 8 });
|
|
253
|
+
run(30);
|
|
254
|
+
break; // F1-F10
|
|
255
|
+
case "alt":
|
|
256
|
+
inp.push({ type: EventType.KeyDown, message: ALT_SCAN[arg.toUpperCase()] << 8, modifiers: 8 });
|
|
257
|
+
run(30);
|
|
258
|
+
break;
|
|
259
|
+
case "objs": { // every object with this name, full references: objs:Scaler,client,lastY
|
|
260
|
+
const [name, ...keys] = arg.split(",");
|
|
261
|
+
for (const o of vm.memory.objects.values()) {
|
|
262
|
+
if (o.name !== name || o.isClass)
|
|
263
|
+
continue;
|
|
264
|
+
const show = (v) => { const t = vm.memory.object(v); return t ? `${t.name}@${v.toString(16)}` : String(v >= 0x8000 && v < 0x10000 ? v - 0x10000 : v); };
|
|
265
|
+
console.log(` ${name}@${o.address.toString(16)} ${keys.map((k) => `${k}=${show(vm.getProp(o.address, k) ?? 0)}`).join(" ")}`);
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case "props": {
|
|
270
|
+
const [name, ...keys] = arg.split(",");
|
|
271
|
+
for (const o of vm.memory.objects.values()) {
|
|
272
|
+
if (o.name !== name)
|
|
273
|
+
continue;
|
|
274
|
+
console.log(` ${name}${o.isClass ? " (class)" : ""} ${keys.map((k) => `${k}=${(vm.getProp(o.address, k) ?? NaN) << 16 >> 16}`).join(" ")}`);
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case "snap":
|
|
279
|
+
snapFrame = g.compose();
|
|
280
|
+
break;
|
|
281
|
+
case "diff": {
|
|
282
|
+
const now = g.compose(), before = snapFrame;
|
|
283
|
+
let idx = 0;
|
|
284
|
+
for (let i = 160 * 320; i < 199 * 320; i++)
|
|
285
|
+
if (now.pixels[i] !== before.pixels[i])
|
|
286
|
+
idx++;
|
|
287
|
+
const changed = [];
|
|
288
|
+
for (let c = 0; c < 256; c++)
|
|
289
|
+
if ([0, 1, 2].some((k) => now.palette.rgb[c * 3 + k] !== before.palette.rgb[c * 3 + k]))
|
|
290
|
+
changed.push(c);
|
|
291
|
+
console.log(` diff: ${idx} index changes in rows 160-198; palette entries changed: ${changed.length} [${changed.slice(0, 40).join(",")}]`);
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case "save":
|
|
295
|
+
writeFileSync(path(arg), serialize(snapshot(vm)));
|
|
296
|
+
console.log(`checkpoint ${arg} saved at frame ${g.frames}`);
|
|
297
|
+
break;
|
|
298
|
+
default: throw new Error(`unknown action ${a}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
run(tail);
|
|
302
|
+
writeFileSync("out/explore/last.png", framePng(g.compose()));
|
|
303
|
+
const ego = vm.object(vm.loadedScripts.find((s) => s.number === 0).locals[0]);
|
|
304
|
+
const room = vm.loadedScripts.find((s) => s.number === 0).locals[11]; // curRoomNum
|
|
305
|
+
console.log(`done at frame ${g.frames} · room ${room} · ego ${g.prop(ego.address, "x")},${g.prop(ego.address, "y")} · missing kernels:`, Object.fromEntries(vm.missingKernels));
|
package/tools/explore.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless exploration from a checkpoint.
|
|
3
|
+
* pnpm tsx tools/explore.ts <checkpoint> [actions...] [--frames N] [--png-every K]
|
|
4
|
+
*
|
|
5
|
+
* Checkpoints live in out/checkpoints/<name>.bin (a VM snapshot). "cave-start" (a new Wizard in
|
|
6
|
+
* the first cave) and "cave-exit" (the cave solved, room 720) are built on first use by
|
|
7
|
+
* playing the opening. SCI_MODS=out/mods/<name> loads built mods (see tools/mod.ts); checkpoints
|
|
8
|
+
* then are rebuilt each run, since a snapshot only fits the scripts it was made with.
|
|
9
|
+
*
|
|
10
|
+
* Actions run in order, each after the previous one's wait:
|
|
11
|
+
* click:x,y move:x,y key:text enter wait:N (frames) icon:walk|look|do|talk|cast
|
|
12
|
+
* save:<name> (writes a new checkpoint from the current state)
|
|
13
|
+
* props:<object>,<prop>,... planes items snap / diff (frame index + palette diff)
|
|
14
|
+
* room:<n> (jump there, as the player's #room/<n>) alt:<letter> (Alt+key; --debug enables
|
|
15
|
+
* Sierra's debug room: alt:t teleports, alt:h sets the hour) fkey:<n> (F1-F10; F7 restore)
|
|
16
|
+
* objs:<name>,<prop>,... (every instance, full references) fakesaves:<n> (fill the save list)
|
|
17
|
+
* loadsaves:<file.json> (saves from the player "Export saves" button; restores the newest)
|
|
18
|
+
* WATCH=<object>.<prop> logs every change to a property with the send that was running.
|
|
19
|
+
* Coordinates are screen coordinates; room features are local to the room plane (y - 10).
|
|
20
|
+
* KTRACE=<regex> logs matching kernel calls; --trace-sends <regex> logs method calls.
|
|
21
|
+
* Every message the game shows is printed with its text.
|
|
22
|
+
*/
|
|
23
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { relative } from "node:path";
|
|
25
|
+
import { deserialize, serialize } from "node:v8";
|
|
26
|
+
import {
|
|
27
|
+
EventType, ResourceManager, ResourceType, Vm, allKernels, audio, findMessage, hostFiles, saves, readingTime, requestRoom, graphics, input, parseMessages, restore, restoreGame, savesFromJson, snapshot, stringHelpers,
|
|
28
|
+
type MessageFile, type SaveInfo, type VmSnapshot,
|
|
29
|
+
} from "../packages/sci/src/index.ts";
|
|
30
|
+
import { nodeFiles } from "./node-files.ts";
|
|
31
|
+
import { framePng } from "./png.ts";
|
|
32
|
+
|
|
33
|
+
const argv = process.argv.slice(2);
|
|
34
|
+
const opt = (name: string) => (argv.includes(name) ? argv[argv.indexOf(name) + 1] : undefined);
|
|
35
|
+
const checkpoint = argv[0] ?? "cave-exit";
|
|
36
|
+
const valued = new Set(["--frames", "--png-every", "--trace-sends"]);
|
|
37
|
+
const actions = argv.slice(1).filter((a, i, all) => !a.startsWith("--") && !valued.has(all[i]!) && !valued.has(all[i - 1]!));
|
|
38
|
+
const pngEvery = Number(opt("--png-every") ?? 0);
|
|
39
|
+
const tail = Number(opt("--frames") ?? 300);
|
|
40
|
+
|
|
41
|
+
// SCI_MODS=out/mods/<name>[,...] loads built mods over the game (see tools/mod.ts).
|
|
42
|
+
const gameDir = process.env.SCI_GAME ?? "original";
|
|
43
|
+
const mods = (process.env.SCI_MODS ?? "").split(",").filter(Boolean).map((dir) => relative(gameDir, dir));
|
|
44
|
+
const rm = await ResourceManager.open(nodeFiles(gameDir), ["", "PATCHES", ...mods]);
|
|
45
|
+
await rm.preload();
|
|
46
|
+
const vm = new Vm(rm);
|
|
47
|
+
vm.registerKernels(allKernels);
|
|
48
|
+
// --debug: Sierra's debug room (it loads when "18.scr" exists), as the player's ?debug.
|
|
49
|
+
if (argv.includes("--debug")) hostFiles(vm).add("18.scr");
|
|
50
|
+
// As in the browser: with mods, lines without recordings stay up long enough to read.
|
|
51
|
+
if (mods.length) audio(vm).missingSpeechTicks = readingTime(vm);
|
|
52
|
+
const g = graphics(vm);
|
|
53
|
+
vm.clock = () => (g.frames * 1000) / 60;
|
|
54
|
+
const inp = input(vm);
|
|
55
|
+
|
|
56
|
+
const files = new Map<number, MessageFile>();
|
|
57
|
+
const text = (m: number, noun: number, verb: number, cond: number, seq: number) => {
|
|
58
|
+
if (!files.has(m)) files.set(m, parseMessages(rm.loadSync({ type: ResourceType.Message, number: m }).data));
|
|
59
|
+
return findMessage(files.get(m)!, { noun, verb, cond, seq })?.text ?? "(no text)";
|
|
60
|
+
};
|
|
61
|
+
const kernelStats = new Map<string, number>();
|
|
62
|
+
vm.onKernelCall = (name, args, result) => {
|
|
63
|
+
// KDUMP=<kernel>: after each call, show what its reference arguments point at (debugging).
|
|
64
|
+
if (process.env.KDUMP === name) for (const a of args) {
|
|
65
|
+
const arr = vm.memory.array(a), o = vm.memory.object(a);
|
|
66
|
+
if (arr) console.log(` ${a.toString(16)}: ${arr.type}[${arr.data.length}] ${arr.data.slice(0, 12).join(",")}`);
|
|
67
|
+
else if (o) console.log(` ${a.toString(16)}: object ${o.name} data=${(vm.getProp(o, "data") ?? 0).toString(16)}`);
|
|
68
|
+
}
|
|
69
|
+
if (process.env.KSTATS) kernelStats.set(name, (kernelStats.get(name) ?? 0) + 1);
|
|
70
|
+
if (process.env.KTRACE && new RegExp(process.env.KTRACE).test(name)) console.log(` [${g.frames}] ${name}(${args.map((a) => a.toString(16)).join(", ")}) = ${result === undefined ? "-" : Number(result).toString(16)}`);
|
|
71
|
+
if (name === "Message" && args[0] === 0 && args.length === 7) {
|
|
72
|
+
const [, m = 0, n = 0, v = 0, c = 0, s = 0] = args;
|
|
73
|
+
console.log(`[${g.frames}] ${m}/${n}/${v}/${c}/${s}: ${text(m, n, v, c, s)}`);
|
|
74
|
+
}
|
|
75
|
+
// Subop 1: the next message of the current sequence, written into the buffer.
|
|
76
|
+
if (name === "Message" && args[0] === 1 && args[1] && result) console.log(`[${g.frames}] ...next: ${stringHelpers.str(vm, args[1])}`);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// --trace-sends <regex>: log method calls on objects whose name matches, during actions.
|
|
80
|
+
const traceSends = opt("--trace-sends");
|
|
81
|
+
let tracing = false;
|
|
82
|
+
if (traceSends) {
|
|
83
|
+
const re = new RegExp(traceSends);
|
|
84
|
+
vm.onSend = (obj, sel, argc) => {
|
|
85
|
+
if (tracing && re.test(obj.name)) console.log(` [${g.frames}] ${obj.name} ${sel}${argc ? `(${argc})` : ""}`);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// WATCH=object.property: log every change to it, with the send that was running (debugging).
|
|
90
|
+
if (process.env.WATCH) {
|
|
91
|
+
const [objName, propName] = process.env.WATCH.split(".") as [string, string];
|
|
92
|
+
let last: number | undefined;
|
|
93
|
+
const previous = vm.onSend;
|
|
94
|
+
vm.onSend = (obj, sel, argc) => {
|
|
95
|
+
previous?.(obj, sel, argc);
|
|
96
|
+
const target = [...vm.memory.objects.values()].find((o) => o.name === objName);
|
|
97
|
+
const v = target ? vm.getProp(target.address, propName) : undefined;
|
|
98
|
+
if (v !== last) console.log(` [${g.frames}] ${objName}.${propName}: ${last?.toString(16)} → ${v?.toString(16)} (before ${obj.name} ${sel})`);
|
|
99
|
+
last = v;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
mkdirSync("out/checkpoints", { recursive: true });
|
|
103
|
+
mkdirSync("out/explore", { recursive: true });
|
|
104
|
+
const path = (name: string) => `out/checkpoints/${name}.bin`;
|
|
105
|
+
const move = (x: number, y: number) => ([inp.x, inp.y] = [x, y]);
|
|
106
|
+
const click = (x: number, y: number) => {
|
|
107
|
+
move(x, y);
|
|
108
|
+
inp.push({ type: EventType.MouseDown, message: 0 });
|
|
109
|
+
inp.push({ type: EventType.MouseUp, message: 0 });
|
|
110
|
+
};
|
|
111
|
+
// A modal loop (a dialog waiting for a click) may poll events without drawing frames, so
|
|
112
|
+
// run in small slices and count five slices without a frame as one, letting input through.
|
|
113
|
+
let idle = 0;
|
|
114
|
+
const run = (frames: number) => {
|
|
115
|
+
const end = g.frames + frames;
|
|
116
|
+
while (g.frames < end && vm.running) {
|
|
117
|
+
const before = g.frames;
|
|
118
|
+
vm.run(200_000);
|
|
119
|
+
if (g.frames !== before) idle = 0;
|
|
120
|
+
else if (++idle >= 5) {
|
|
121
|
+
idle = 0;
|
|
122
|
+
g.frames++;
|
|
123
|
+
if (process.env.KSTATS) console.log(" (no frame drawn: modal loop?)", Object.fromEntries(kernelStats));
|
|
124
|
+
}
|
|
125
|
+
if (pngEvery && g.frames % pngEvery === 0) writeFileSync(`out/explore/${String(g.frames).padStart(6, "0")}.png`, framePng(g.compose()));
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (existsSync(path(checkpoint)) && !(mods.length && checkpoint.startsWith("cave-"))) {
|
|
130
|
+
vm.start(vm.exportAddress(0, 0), "play");
|
|
131
|
+
vm.run(); // initialise the game object before replacing memory
|
|
132
|
+
restore(vm, deserialize(readFileSync(path(checkpoint))) as VmSnapshot);
|
|
133
|
+
vm.resetExecution();
|
|
134
|
+
vm.start(vm.exportAddress(0, 0), "replay");
|
|
135
|
+
run(60);
|
|
136
|
+
} else if (checkpoint === "cave-exit" || checkpoint === "cave-start") {
|
|
137
|
+
// cave-start: just created a Wizard, standing in the first cave. cave-exit: solved it.
|
|
138
|
+
// Not saved when mods are loaded: a snapshot only fits the scripts it was made with.
|
|
139
|
+
const until = checkpoint === "cave-start" ? 5300 : 10200;
|
|
140
|
+
console.log(`building checkpoint ${checkpoint} (plays the opening route)...`);
|
|
141
|
+
const type = (t: string) => {
|
|
142
|
+
for (const ch of t) inp.push({ type: EventType.KeyDown, message: ch.charCodeAt(0) });
|
|
143
|
+
inp.push({ type: EventType.KeyDown, message: 13 });
|
|
144
|
+
};
|
|
145
|
+
const script: Record<number, () => void> = {
|
|
146
|
+
1401: () => click(160, 60), 1800: () => click(185, 110), 2000: () => click(100, 185), 2100: () => type("Hero"),
|
|
147
|
+
2300: () => click(30, 185), 2600: () => click(170, 112), 5300: () => move(35, 2), 5360: () => click(35, 14),
|
|
148
|
+
5400: () => click(250, 140), 6400: () => move(172, 2), 6460: () => click(172, 14), 6600: () => click(122, 98),
|
|
149
|
+
6900: () => click(253, 80), 7200: () => click(253, 80), 7800: () => move(89, 2), 7860: () => click(89, 14),
|
|
150
|
+
7950: () => click(253, 80), 8600: () => move(89, 2), 8660: () => click(89, 14), 8800: () => click(156, 57),
|
|
151
|
+
};
|
|
152
|
+
vm.start(vm.exportAddress(0, 0), "play");
|
|
153
|
+
while (g.frames < until) {
|
|
154
|
+
script[g.frames]?.();
|
|
155
|
+
vm.run();
|
|
156
|
+
}
|
|
157
|
+
if (!mods.length) writeFileSync(path(checkpoint), serialize(snapshot(vm)));
|
|
158
|
+
} else throw new Error(`no checkpoint ${checkpoint}`);
|
|
159
|
+
|
|
160
|
+
tracing = true;
|
|
161
|
+
let snapFrame: ReturnType<typeof g.compose> | undefined;
|
|
162
|
+
const ALT_SCAN: Record<string, number> = Object.fromEntries([..."QWERTYUIOP"].map((k, i) => [k, 0x10 + i]).concat([..."ASDFGHJKL"].map((k, i) => [k, 0x1e + i]), [..."ZXCVBNM"].map((k, i) => [k, 0x2c + i])));
|
|
163
|
+
const ICON_X: Record<string, number> = { walk: 35, look: 60, do: 89, talk: 117, actions: 143, cast: 172 };
|
|
164
|
+
for (const a of actions) {
|
|
165
|
+
const [kind, arg = ""] = a.split(/:(.*)/);
|
|
166
|
+
const [x = 0, y = 0] = arg.split(",").map(Number);
|
|
167
|
+
switch (kind) {
|
|
168
|
+
// Hover first, like a real mouse: some UIs (the spell window) select what's highlighted.
|
|
169
|
+
case "click": move(x, y); run(6); click(x, y); run(90); break;
|
|
170
|
+
case "move": move(x, y); run(30); break;
|
|
171
|
+
case "wait": run(x); break;
|
|
172
|
+
case "enter": inp.push({ type: EventType.KeyDown, message: 13 }); run(30); break;
|
|
173
|
+
case "key": for (const ch of arg) inp.push({ type: EventType.KeyDown, message: ch.charCodeAt(0) }); run(30); break;
|
|
174
|
+
case "icon": move(ICON_X[arg]!, 2); run(40); click(ICON_X[arg]!, 14); run(40); break;
|
|
175
|
+
case "planes":
|
|
176
|
+
for (const pl of g.planes) {
|
|
177
|
+
const props = ["priority", "picture", "left", "top", "right", "bottom", "inLeft", "inTop", "inRight", "inBottom", "vanishingX", "vanishingY"].map((k) => `${k}=${g.prop(pl, k)}`);
|
|
178
|
+
console.log(` plane ${vm.memory.object(pl)?.name} ${pl.toString(16)} ${props.join(" ")}`);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
case "items":
|
|
182
|
+
for (const it of g.items) {
|
|
183
|
+
const props = ["plane", "view", "loop", "cel", "x", "y", "z", "priority", "fixPriority", "scaleSignal", "scaleX", "scaleY", "bitmap"].map((k) => `${k}=${(g.prop(it, k) >>> 0).toString(16)}`);
|
|
184
|
+
console.log(` item ${vm.memory.object(it)?.name} ${props.join(" ")}`);
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
case "room": requestRoom(vm, x); run(1); break; // as the player's #room/N does
|
|
188
|
+
case "png": writeFileSync(`out/explore/${arg}.png`, framePng(g.compose())); break; // snapshot mid-script
|
|
189
|
+
case "addsave": { // a checkpoint file (an earlier session's snapshot) as a saved game
|
|
190
|
+
const version = saves(vm).store.list()[0]?.version ?? "";
|
|
191
|
+
saves(vm).store.put({ id: 90, description: `checkpoint ${arg}`, version, date: Date.now() + 1000 }, deserialize(readFileSync(path(arg))) as VmSnapshot);
|
|
192
|
+
console.log(` added checkpoint ${arg} as a save (version ${JSON.stringify(version)})`);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case "loadsaves": { // saves exported from the player (Export saves); restores the newest, as #room/N does
|
|
196
|
+
const list = savesFromJson<SaveInfo>(readFileSync(arg, "utf8"));
|
|
197
|
+
for (const s of list) saves(vm).store.put(s.info, s.snapshot);
|
|
198
|
+
const newest = saves(vm).store.list()[0];
|
|
199
|
+
console.log(` loaded ${list.length} saves; restoring "${newest?.description}" (id ${newest?.id})`);
|
|
200
|
+
if (newest) restoreGame(vm, saves(vm).store.get(newest.id)!.snapshot);
|
|
201
|
+
run(120);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "fakesaves": { // n saved games (copies of the current state), for testing the Restore dialog
|
|
205
|
+
const snap = snapshot(vm);
|
|
206
|
+
const odd = process.env.ODD_SAVES ? JSON.parse(process.env.ODD_SAVES) as { description: string; version?: string }[] : [];
|
|
207
|
+
for (let i = 0; i < x; i++) {
|
|
208
|
+
const o = odd[i];
|
|
209
|
+
saves(vm).store.put({ id: 100 + i, description: o?.description ?? `Save number ${i + 1} ${"x".repeat(i % 12)}`, version: o?.version ?? "", date: Date.now() - i }, snap);
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case "fkey": inp.push({ type: EventType.KeyDown, message: (0x3a + x) << 8 }); run(30); break; // F1-F10
|
|
214
|
+
case "alt": inp.push({ type: EventType.KeyDown, message: ALT_SCAN[arg.toUpperCase()]! << 8, modifiers: 8 }); run(30); break;
|
|
215
|
+
case "objs": { // every object with this name, full references: objs:Scaler,client,lastY
|
|
216
|
+
const [name, ...keys] = arg.split(",");
|
|
217
|
+
for (const o of vm.memory.objects.values()) {
|
|
218
|
+
if (o.name !== name || o.isClass) continue;
|
|
219
|
+
const show = (v: number) => { const t = vm.memory.object(v); return t ? `${t.name}@${v.toString(16)}` : String(v >= 0x8000 && v < 0x10000 ? v - 0x10000 : v); };
|
|
220
|
+
console.log(` ${name}@${o.address.toString(16)} ${keys.map((k) => `${k}=${show(vm.getProp(o.address, k) ?? 0)}`).join(" ")}`);
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "props": {
|
|
225
|
+
const [name, ...keys] = arg.split(",");
|
|
226
|
+
for (const o of vm.memory.objects.values()) {
|
|
227
|
+
if (o.name !== name) continue;
|
|
228
|
+
console.log(` ${name}${o.isClass ? " (class)" : ""} ${keys.map((k) => `${k}=${(vm.getProp(o.address, k) ?? NaN) << 16 >> 16}`).join(" ")}`);
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case "snap": snapFrame = g.compose(); break;
|
|
233
|
+
case "diff": {
|
|
234
|
+
const now = g.compose(), before = snapFrame!;
|
|
235
|
+
let idx = 0;
|
|
236
|
+
for (let i = 160 * 320; i < 199 * 320; i++) if (now.pixels[i] !== before.pixels[i]) idx++;
|
|
237
|
+
const changed: number[] = [];
|
|
238
|
+
for (let c = 0; c < 256; c++) if ([0, 1, 2].some((k) => now.palette.rgb[c * 3 + k] !== before.palette.rgb[c * 3 + k])) changed.push(c);
|
|
239
|
+
console.log(` diff: ${idx} index changes in rows 160-198; palette entries changed: ${changed.length} [${changed.slice(0, 40).join(",")}]`);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "save": writeFileSync(path(arg), serialize(snapshot(vm))); console.log(`checkpoint ${arg} saved at frame ${g.frames}`); break;
|
|
243
|
+
default: throw new Error(`unknown action ${a}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
run(tail);
|
|
247
|
+
writeFileSync("out/explore/last.png", framePng(g.compose()));
|
|
248
|
+
const ego = vm.object(vm.loadedScripts.find((s) => s.number === 0)!.locals[0]!);
|
|
249
|
+
const room = vm.loadedScripts.find((s) => s.number === 0)!.locals[11]; // curRoomNum
|
|
250
|
+
console.log(`done at frame ${g.frames} · room ${room} · ego ${g.prop(ego.address, "x")},${g.prop(ego.address, "y")} · missing kernels:`, Object.fromEntries(vm.missingKernels));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type SoundSpec } from "../../packages/sci/src/index.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Music and sound for games, from the files people make them with: Standard MIDI Files
|
|
4
|
+
* (.mid, from any sequencer) become sound resources; WAV files (PCM) become digital effects.
|
|
5
|
+
*/
|
|
6
|
+
export declare class AudioFileError extends Error {
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A Standard MIDI File (format 0 or 1) as a sound resource for General MIDI: its tempo
|
|
10
|
+
* changes turned into SCI's 1/60 s ticks, a channel stream per MIDI channel it uses. SCI
|
|
11
|
+
* keeps channel 16 (15 counting from 0) for signals to the scripts, so music there moves to
|
|
12
|
+
* a free channel.
|
|
13
|
+
*/
|
|
14
|
+
export declare function readMidiFile(data: Uint8Array): SoundSpec;
|
|
15
|
+
/** A PCM WAV file (8- or 16-bit, mono or stereo) as mono 16-bit samples. */
|
|
16
|
+
export declare function readWav(data: Uint8Array): {
|
|
17
|
+
samples: Int16Array;
|
|
18
|
+
rate: number;
|
|
19
|
+
};
|