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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a song through the AdLib path (sequencer → Sierra AdLib driver → OPL2 emulator)
|
|
3
|
+
* to a WAV file, for listening and for checking the emulator headlessly.
|
|
4
|
+
* pnpm tsx tools/adlib-render.ts <sound number> [seconds] [--gm-track]
|
|
5
|
+
*/
|
|
6
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { AdLibDriver, OPL_RATE, Opl2, ResourceManager, ResourceType, SongPlayer, SoundDevice, parseAdLibBank, parseSound } from "../packages/sci/src/index.js";
|
|
8
|
+
import { nodeFiles } from "./node-files.js";
|
|
9
|
+
const [number = "130", seconds = "30"] = process.argv.slice(2);
|
|
10
|
+
const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
|
|
11
|
+
const bank = parseAdLibBank((await rm.load({ type: ResourceType.Patch, number: 3 })).data);
|
|
12
|
+
const song = parseSound((await rm.load({ type: ResourceType.Sound, number: Number(number) })).data, SoundDevice.AdLib);
|
|
13
|
+
if (!song)
|
|
14
|
+
throw new Error(`sound ${number} has no AdLib track`);
|
|
15
|
+
const chip = new Opl2();
|
|
16
|
+
const driver = new AdLibDriver(chip, bank);
|
|
17
|
+
const player = new SongPlayer(song, driver, false, 127, 0);
|
|
18
|
+
const ticks = Number(seconds) * 60;
|
|
19
|
+
const out = new Float32Array(Math.ceil((ticks * OPL_RATE) / 60));
|
|
20
|
+
let written = 0;
|
|
21
|
+
for (let t = 0; t < ticks && !player.finished; t++) {
|
|
22
|
+
player.update(t);
|
|
23
|
+
const end = Math.round(((t + 1) * OPL_RATE) / 60);
|
|
24
|
+
chip.generate(out, written, end - written);
|
|
25
|
+
written = end;
|
|
26
|
+
}
|
|
27
|
+
let peak = 0, sum = 0;
|
|
28
|
+
for (let i = 0; i < written; i++)
|
|
29
|
+
(peak = Math.max(peak, Math.abs(out[i]))), (sum += out[i] * out[i]);
|
|
30
|
+
console.log({ sound: Number(number), seconds: +(written / OPL_RATE).toFixed(1), peak: +peak.toFixed(3), rms: +Math.sqrt(sum / written).toFixed(4), finished: player.finished });
|
|
31
|
+
const wav = Buffer.alloc(44 + written * 2);
|
|
32
|
+
wav.write("RIFF", 0);
|
|
33
|
+
wav.writeUInt32LE(36 + written * 2, 4);
|
|
34
|
+
wav.write("WAVE", 8);
|
|
35
|
+
wav.write("fmt ", 12);
|
|
36
|
+
wav.writeUInt32LE(16, 16);
|
|
37
|
+
wav.writeUInt16LE(1, 20);
|
|
38
|
+
wav.writeUInt16LE(1, 22);
|
|
39
|
+
wav.writeUInt32LE(OPL_RATE, 24);
|
|
40
|
+
wav.writeUInt32LE(OPL_RATE * 2, 28);
|
|
41
|
+
wav.writeUInt16LE(2, 32);
|
|
42
|
+
wav.writeUInt16LE(16, 34);
|
|
43
|
+
wav.write("data", 36);
|
|
44
|
+
wav.writeUInt32LE(written * 2, 40);
|
|
45
|
+
for (let i = 0; i < written; i++)
|
|
46
|
+
wav.writeInt16LE(Math.round(out[i] * 32767), 44 + i * 2);
|
|
47
|
+
mkdirSync("out/audio", { recursive: true });
|
|
48
|
+
writeFileSync(`out/audio/adlib-${number}.wav`, wav);
|
|
49
|
+
console.log(`wrote out/audio/adlib-${number}.wav`);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a song through the AdLib path (sequencer → Sierra AdLib driver → OPL2 emulator)
|
|
3
|
+
* to a WAV file, for listening and for checking the emulator headlessly.
|
|
4
|
+
* pnpm tsx tools/adlib-render.ts <sound number> [seconds] [--gm-track]
|
|
5
|
+
*/
|
|
6
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { AdLibDriver, OPL_RATE, Opl2, ResourceManager, ResourceType, SongPlayer, SoundDevice, parseAdLibBank, parseSound } from "../packages/sci/src/index.ts";
|
|
8
|
+
import { nodeFiles } from "./node-files.ts";
|
|
9
|
+
|
|
10
|
+
const [number = "130", seconds = "30"] = process.argv.slice(2);
|
|
11
|
+
const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
|
|
12
|
+
const bank = parseAdLibBank((await rm.load({ type: ResourceType.Patch, number: 3 })).data);
|
|
13
|
+
const song = parseSound((await rm.load({ type: ResourceType.Sound, number: Number(number) })).data, SoundDevice.AdLib);
|
|
14
|
+
if (!song) throw new Error(`sound ${number} has no AdLib track`);
|
|
15
|
+
|
|
16
|
+
const chip = new Opl2();
|
|
17
|
+
const driver = new AdLibDriver(chip, bank);
|
|
18
|
+
const player = new SongPlayer(song, driver, false, 127, 0);
|
|
19
|
+
|
|
20
|
+
const ticks = Number(seconds) * 60;
|
|
21
|
+
const out = new Float32Array(Math.ceil((ticks * OPL_RATE) / 60));
|
|
22
|
+
let written = 0;
|
|
23
|
+
for (let t = 0; t < ticks && !player.finished; t++) {
|
|
24
|
+
player.update(t);
|
|
25
|
+
const end = Math.round(((t + 1) * OPL_RATE) / 60);
|
|
26
|
+
chip.generate(out, written, end - written);
|
|
27
|
+
written = end;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let peak = 0, sum = 0;
|
|
31
|
+
for (let i = 0; i < written; i++) (peak = Math.max(peak, Math.abs(out[i]!))), (sum += out[i]! * out[i]!);
|
|
32
|
+
console.log({ sound: Number(number), seconds: +(written / OPL_RATE).toFixed(1), peak: +peak.toFixed(3), rms: +Math.sqrt(sum / written).toFixed(4), finished: player.finished });
|
|
33
|
+
|
|
34
|
+
const wav = Buffer.alloc(44 + written * 2);
|
|
35
|
+
wav.write("RIFF", 0); wav.writeUInt32LE(36 + written * 2, 4); wav.write("WAVE", 8);
|
|
36
|
+
wav.write("fmt ", 12); wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22);
|
|
37
|
+
wav.writeUInt32LE(OPL_RATE, 24); wav.writeUInt32LE(OPL_RATE * 2, 28); wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34);
|
|
38
|
+
wav.write("data", 36); wav.writeUInt32LE(written * 2, 40);
|
|
39
|
+
for (let i = 0; i < written; i++) wav.writeInt16LE(Math.round(out[i]! * 32767), 44 + i * 2);
|
|
40
|
+
mkdirSync("out/audio", { recursive: true });
|
|
41
|
+
writeFileSync(`out/audio/adlib-${number}.wav`, wav);
|
|
42
|
+
console.log(`wrote out/audio/adlib-${number}.wav`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/tools/analyze.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static game index (rooms, exits, classes, objects) as JSON.
|
|
3
|
+
* pnpm tsx tools/analyze.ts [--summary]
|
|
4
|
+
*/
|
|
5
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { ResourceManager, analyzeGame } from "../packages/sci/src/index.js";
|
|
7
|
+
import { nodeFiles } from "./node-files.js";
|
|
8
|
+
const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
|
|
9
|
+
const t = performance.now();
|
|
10
|
+
const index = await analyzeGame(rm);
|
|
11
|
+
const ms = Math.round(performance.now() - t);
|
|
12
|
+
mkdirSync("out", { recursive: true });
|
|
13
|
+
writeFileSync("out/game-index.json", JSON.stringify(index));
|
|
14
|
+
const byKind = {};
|
|
15
|
+
for (const e of index.exits)
|
|
16
|
+
byKind[e.kind] = (byKind[e.kind] ?? 0) + 1;
|
|
17
|
+
console.log(JSON.stringify({
|
|
18
|
+
ms,
|
|
19
|
+
rooms: index.rooms.length,
|
|
20
|
+
classes: index.classes.length,
|
|
21
|
+
objects: index.scripts.reduce((n, s) => n + s.objects.length, 0),
|
|
22
|
+
exits: byKind,
|
|
23
|
+
isolatedRooms: index.rooms.filter((r) => !index.exits.some((e) => e.from === r.number || e.to === r.number)).map((r) => r.number),
|
|
24
|
+
unresolved: index.unresolved.length,
|
|
25
|
+
}, null, 1));
|
package/tools/analyze.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static game index (rooms, exits, classes, objects) as JSON.
|
|
3
|
+
* pnpm tsx tools/analyze.ts [--summary]
|
|
4
|
+
*/
|
|
5
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { ResourceManager, analyzeGame } from "../packages/sci/src/index.ts";
|
|
7
|
+
import { nodeFiles } from "./node-files.ts";
|
|
8
|
+
|
|
9
|
+
const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
|
|
10
|
+
const t = performance.now();
|
|
11
|
+
const index = await analyzeGame(rm);
|
|
12
|
+
const ms = Math.round(performance.now() - t);
|
|
13
|
+
mkdirSync("out", { recursive: true });
|
|
14
|
+
writeFileSync("out/game-index.json", JSON.stringify(index));
|
|
15
|
+
const byKind: Record<string, number> = {};
|
|
16
|
+
for (const e of index.exits) byKind[e.kind] = (byKind[e.kind] ?? 0) + 1;
|
|
17
|
+
console.log(JSON.stringify({
|
|
18
|
+
ms,
|
|
19
|
+
rooms: index.rooms.length,
|
|
20
|
+
classes: index.classes.length,
|
|
21
|
+
objects: index.scripts.reduce((n, s) => n + s.objects.length, 0),
|
|
22
|
+
exits: byKind,
|
|
23
|
+
isolatedRooms: index.rooms.filter((r) => !index.exits.some((e) => e.from === r.number || e.to === r.number)).map((r) => r.number),
|
|
24
|
+
unresolved: index.unresolved.length,
|
|
25
|
+
}, null, 1));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ResourceData } from "../../packages/sci/src/index.ts";
|
|
2
|
+
/** Editor-neutral, exact-colour art for standalone games. See docs/art-workflow.md. */
|
|
3
|
+
export interface ArtManifest {
|
|
4
|
+
version: 1;
|
|
5
|
+
palette: string;
|
|
6
|
+
/** Optional project art budget, including black and white but excluding transparency. */
|
|
7
|
+
maxColours?: number;
|
|
8
|
+
pictures: {
|
|
9
|
+
number: number;
|
|
10
|
+
layers: {
|
|
11
|
+
png: string;
|
|
12
|
+
priority: number;
|
|
13
|
+
}[];
|
|
14
|
+
}[];
|
|
15
|
+
views: {
|
|
16
|
+
number: number;
|
|
17
|
+
loops: ({
|
|
18
|
+
cels: {
|
|
19
|
+
png: string;
|
|
20
|
+
anchor: [number, number];
|
|
21
|
+
}[];
|
|
22
|
+
} | {
|
|
23
|
+
link: number;
|
|
24
|
+
mirror: boolean;
|
|
25
|
+
})[];
|
|
26
|
+
}[];
|
|
27
|
+
}
|
|
28
|
+
export declare class ArtError extends Error {
|
|
29
|
+
}
|
|
30
|
+
export declare function buildArt(file: string): {
|
|
31
|
+
resources: ResourceData[];
|
|
32
|
+
colours: string[];
|
|
33
|
+
images: number;
|
|
34
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { ResourceType, writeHunkPalette, writePic, writeView } from "../../packages/sci/src/index.js";
|
|
4
|
+
import { basePalette } from "../game/defaults.js";
|
|
5
|
+
import { decodePng } from "../png.js";
|
|
6
|
+
export class ArtError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export function buildArt(file) {
|
|
9
|
+
const fail = (where, message) => { throw new ArtError(`${file}: ${where}: ${message}`); };
|
|
10
|
+
const json = (path) => {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
return fail(path, e.message);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const object = (value, where, keys) => {
|
|
19
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
20
|
+
return fail(where, "expected an object");
|
|
21
|
+
for (const key of Object.keys(value))
|
|
22
|
+
if (!keys.includes(key))
|
|
23
|
+
fail(`${where}.${key}`, "unknown key");
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
const list = (value, where, min, max) => {
|
|
27
|
+
if (!Array.isArray(value) || value.length < min || value.length > max)
|
|
28
|
+
return fail(where, `expected ${min}..${max} entries`);
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
31
|
+
const int = (value, where, min, max) => {
|
|
32
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max)
|
|
33
|
+
return fail(where, `expected an integer in ${min}..${max}`);
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
36
|
+
const path = (value, where) => {
|
|
37
|
+
if (typeof value !== "string" || !value.length)
|
|
38
|
+
return fail(where, "expected a file path");
|
|
39
|
+
return resolve(dirname(file), value);
|
|
40
|
+
};
|
|
41
|
+
const m = object(json(file), "manifest", ["version", "palette", "maxColours", "pictures", "views"]);
|
|
42
|
+
if (m.version !== 1)
|
|
43
|
+
fail("version", "expected 1");
|
|
44
|
+
const maxColours = m.maxColours === undefined ? 254 : int(m.maxColours, "maxColours", 2, 254);
|
|
45
|
+
const paletteInput = json(path(m.palette, "palette"));
|
|
46
|
+
if (Array.isArray(paletteInput) && paletteInput.length > maxColours)
|
|
47
|
+
fail("palette", `${paletteInput.length} colours exceeds the project budget of ${maxColours}`);
|
|
48
|
+
const colours = list(paletteInput, "palette", 2, 254).map((c, i) => {
|
|
49
|
+
if (typeof c !== "string" || !/^#[0-9a-f]{6}$/i.test(c))
|
|
50
|
+
return fail(`palette[${i}]`, "expected #RRGGBB");
|
|
51
|
+
return c.toLowerCase();
|
|
52
|
+
});
|
|
53
|
+
if (colours[0] !== "#000000" || colours.at(-1) !== "#ffffff")
|
|
54
|
+
fail("palette", "first colour must be black, last white (interface colours)");
|
|
55
|
+
if (new Set(colours).size !== colours.length)
|
|
56
|
+
fail("palette", "duplicate colours");
|
|
57
|
+
// Preserve the library's black=0, white=255 and transparent=254 conventions.
|
|
58
|
+
// Entries 1..252 are ours; 253 is reserved. No room quantization or alpha remaps.
|
|
59
|
+
const palette = basePalette();
|
|
60
|
+
const indices = new Map();
|
|
61
|
+
colours.forEach((c, i) => {
|
|
62
|
+
const index = i === colours.length - 1 ? 255 : i;
|
|
63
|
+
palette.rgb[index] = [1, 3, 5].map((at) => parseInt(c.slice(at, at + 2), 16));
|
|
64
|
+
indices.set(c, index);
|
|
65
|
+
});
|
|
66
|
+
let images = 0;
|
|
67
|
+
const image = (value, where, opaque) => {
|
|
68
|
+
const filename = path(value, where);
|
|
69
|
+
let img;
|
|
70
|
+
try {
|
|
71
|
+
img = decodePng(readFileSync(filename));
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return fail(where, `${filename}: ${e.message}`);
|
|
75
|
+
}
|
|
76
|
+
int(img.width, `${where}.width`, 1, 320);
|
|
77
|
+
int(img.height, `${where}.height`, 1, 200);
|
|
78
|
+
const pixels = new Uint8Array(img.width * img.height);
|
|
79
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
80
|
+
const [r, g, b, a] = img.data.subarray(i * 4, i * 4 + 4);
|
|
81
|
+
const at = `${where} (${i % img.width},${Math.floor(i / img.width)})`;
|
|
82
|
+
if (a !== 0 && a !== 255)
|
|
83
|
+
fail(at, `alpha ${a}; export without antialiasing or partial opacity`);
|
|
84
|
+
if (a === 0) {
|
|
85
|
+
if (opaque)
|
|
86
|
+
fail(at, "background must be opaque");
|
|
87
|
+
pixels[i] = 254;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const hex = `#${[r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
91
|
+
const index = indices.get(hex);
|
|
92
|
+
if (index === undefined)
|
|
93
|
+
fail(at, `${hex} is outside the project palette`);
|
|
94
|
+
pixels[i] = index;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
images++;
|
|
98
|
+
return { width: img.width, height: img.height, pixels, skipColor: 254 };
|
|
99
|
+
};
|
|
100
|
+
const resources = [{ type: ResourceType.Palette, number: 999, data: writeHunkPalette(palette) }];
|
|
101
|
+
const numbers = new Set();
|
|
102
|
+
const number = (value, type, where) => {
|
|
103
|
+
const n = int(value, where, 0, 65535);
|
|
104
|
+
if (numbers.has(`${type}:${n}`))
|
|
105
|
+
fail(where, `duplicate ${type} ${n}`);
|
|
106
|
+
numbers.add(`${type}:${n}`);
|
|
107
|
+
return n;
|
|
108
|
+
};
|
|
109
|
+
list(m.pictures, "pictures", 0, 65536).forEach((value, i) => {
|
|
110
|
+
const where = `pictures[${i}]`;
|
|
111
|
+
const pic = object(value, where, ["number", "layers"]);
|
|
112
|
+
const n = number(pic.number, "picture", `${where}.number`);
|
|
113
|
+
const priorities = new Set();
|
|
114
|
+
const cels = list(pic.layers, `${where}.layers`, 1, 255).map((value, j) => {
|
|
115
|
+
const at = `${where}.layers[${j}]`;
|
|
116
|
+
const layer = object(value, at, ["png", "priority"]);
|
|
117
|
+
const priority = int(layer.priority, `${at}.priority`, -32768, 32767);
|
|
118
|
+
if (j === 0 ? priority !== -1000 : priority < 0 || priority > 199)
|
|
119
|
+
fail(at, "background priority is -1000; foreground priorities are 0..199");
|
|
120
|
+
if (priorities.has(priority))
|
|
121
|
+
fail(at, "duplicate layer priority; combine these layers in the editor");
|
|
122
|
+
priorities.add(priority);
|
|
123
|
+
const img = image(layer.png, `${at}.png`, j === 0);
|
|
124
|
+
if (img.width !== 320 || img.height !== 200)
|
|
125
|
+
fail(at, "picture layers must be 320x200");
|
|
126
|
+
return { ...img, displaceX: 0, displaceY: 0, priority, x: 0, y: 0, unknown16: 0 };
|
|
127
|
+
});
|
|
128
|
+
resources.push({ type: ResourceType.Pic, number: n, data: writePic({ resolution: [320, 200], cels, palette }) });
|
|
129
|
+
});
|
|
130
|
+
list(m.views, "views", 0, 65536).forEach((value, i) => {
|
|
131
|
+
const where = `views[${i}]`;
|
|
132
|
+
const view = object(value, where, ["number", "loops"]);
|
|
133
|
+
const n = number(view.number, "view", `${where}.number`);
|
|
134
|
+
const loops = [];
|
|
135
|
+
let total = 0;
|
|
136
|
+
list(view.loops, `${where}.loops`, 1, 128).forEach((value, j) => {
|
|
137
|
+
const at = `${where}.loops[${j}]`;
|
|
138
|
+
const loop = object(value, at, ["cels", "link", "mirror"]);
|
|
139
|
+
if ("link" in loop) {
|
|
140
|
+
if ("cels" in loop)
|
|
141
|
+
fail(at, "linked loops cannot also have cels");
|
|
142
|
+
const link = int(loop.link, `${at}.link`, 0, j - 1);
|
|
143
|
+
if (loops[link].link !== -1)
|
|
144
|
+
fail(at, "link must refer to an earlier loop with its own cels");
|
|
145
|
+
if (typeof loop.mirror !== "boolean")
|
|
146
|
+
fail(at, "mirror must be a boolean");
|
|
147
|
+
loops.push({ link, mirror: loop.mirror, cels: [] });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if ("mirror" in loop)
|
|
151
|
+
fail(at, "mirror requires link");
|
|
152
|
+
let size = "";
|
|
153
|
+
const cels = list(loop.cels, `${at}.cels`, 1, 255).map((value, k) => {
|
|
154
|
+
const pos = `${at}.cels[${k}]`;
|
|
155
|
+
const cel = object(value, pos, ["png", "anchor"]);
|
|
156
|
+
const img = image(cel.png, `${pos}.png`, false);
|
|
157
|
+
const dimensions = `${img.width}x${img.height}`;
|
|
158
|
+
if (size && size !== dimensions)
|
|
159
|
+
fail(pos, "keep the same canvas size throughout a loop (disable trimming)");
|
|
160
|
+
size = dimensions;
|
|
161
|
+
const anchor = list(cel.anchor, `${pos}.anchor`, 2, 2);
|
|
162
|
+
const x = int(anchor[0], `${pos}.anchor[0]`, 0, img.width - 1);
|
|
163
|
+
const y = int(anchor[1], `${pos}.anchor[1]`, 0, img.height - 1);
|
|
164
|
+
total++;
|
|
165
|
+
return { ...img, displaceX: (img.width >> 1) - x, displaceY: img.height - 1 - y };
|
|
166
|
+
});
|
|
167
|
+
loops.push({ link: -1, mirror: false, cels });
|
|
168
|
+
});
|
|
169
|
+
if (total > 255)
|
|
170
|
+
fail(where, "a view supports at most 255 stored cels");
|
|
171
|
+
resources.push({ type: ResourceType.View, number: n, data: writeView({ flags: 1, loops, palette }) });
|
|
172
|
+
});
|
|
173
|
+
resources.sort((a, b) => a.type - b.type || a.number - b.number);
|
|
174
|
+
return { resources, colours, images };
|
|
175
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { ResourceType, writeHunkPalette, writePic, writeView, type ResourceData, type ViewLoop } from "../../packages/sci/src/index.ts";
|
|
4
|
+
import { basePalette } from "../game/defaults.ts";
|
|
5
|
+
import { decodePng } from "../png.ts";
|
|
6
|
+
|
|
7
|
+
/** Editor-neutral, exact-colour art for standalone games. See docs/art-workflow.md. */
|
|
8
|
+
export interface ArtManifest {
|
|
9
|
+
version: 1;
|
|
10
|
+
palette: string;
|
|
11
|
+
/** Optional project art budget, including black and white but excluding transparency. */
|
|
12
|
+
maxColours?: number;
|
|
13
|
+
pictures: { number: number; layers: { png: string; priority: number }[] }[];
|
|
14
|
+
views: { number: number; loops: ({ cels: { png: string; anchor: [number, number] }[] } | { link: number; mirror: boolean })[] }[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ArtError extends Error {}
|
|
18
|
+
|
|
19
|
+
export function buildArt(file: string): { resources: ResourceData[]; colours: string[]; images: number } {
|
|
20
|
+
const fail = (where: string, message: string): never => { throw new ArtError(`${file}: ${where}: ${message}`); };
|
|
21
|
+
const json = (path: string): unknown => {
|
|
22
|
+
try { return JSON.parse(readFileSync(path, "utf8")); }
|
|
23
|
+
catch (e) { return fail(path, (e as Error).message); }
|
|
24
|
+
};
|
|
25
|
+
const object = (value: unknown, where: string, keys: string[]): Record<string, unknown> => {
|
|
26
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return fail(where, "expected an object");
|
|
27
|
+
for (const key of Object.keys(value)) if (!keys.includes(key)) fail(`${where}.${key}`, "unknown key");
|
|
28
|
+
return value as Record<string, unknown>;
|
|
29
|
+
};
|
|
30
|
+
const list = (value: unknown, where: string, min: number, max: number): unknown[] => {
|
|
31
|
+
if (!Array.isArray(value) || value.length < min || value.length > max) return fail(where, `expected ${min}..${max} entries`);
|
|
32
|
+
return value;
|
|
33
|
+
};
|
|
34
|
+
const int = (value: unknown, where: string, min: number, max: number): number => {
|
|
35
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) return fail(where, `expected an integer in ${min}..${max}`);
|
|
36
|
+
return value;
|
|
37
|
+
};
|
|
38
|
+
const path = (value: unknown, where: string): string => {
|
|
39
|
+
if (typeof value !== "string" || !value.length) return fail(where, "expected a file path");
|
|
40
|
+
return resolve(dirname(file), value);
|
|
41
|
+
};
|
|
42
|
+
const m = object(json(file), "manifest", ["version", "palette", "maxColours", "pictures", "views"]);
|
|
43
|
+
if (m.version !== 1) fail("version", "expected 1");
|
|
44
|
+
const maxColours = m.maxColours === undefined ? 254 : int(m.maxColours, "maxColours", 2, 254);
|
|
45
|
+
const paletteInput = json(path(m.palette, "palette"));
|
|
46
|
+
if (Array.isArray(paletteInput) && paletteInput.length > maxColours) fail("palette", `${paletteInput.length} colours exceeds the project budget of ${maxColours}`);
|
|
47
|
+
const colours = list(paletteInput, "palette", 2, 254).map((c, i) => {
|
|
48
|
+
if (typeof c !== "string" || !/^#[0-9a-f]{6}$/i.test(c)) return fail(`palette[${i}]`, "expected #RRGGBB");
|
|
49
|
+
return c.toLowerCase();
|
|
50
|
+
});
|
|
51
|
+
if (colours[0] !== "#000000" || colours.at(-1) !== "#ffffff") fail("palette", "first colour must be black, last white (interface colours)");
|
|
52
|
+
if (new Set(colours).size !== colours.length) fail("palette", "duplicate colours");
|
|
53
|
+
// Preserve the library's black=0, white=255 and transparent=254 conventions.
|
|
54
|
+
// Entries 1..252 are ours; 253 is reserved. No room quantization or alpha remaps.
|
|
55
|
+
const palette = basePalette();
|
|
56
|
+
const indices = new Map<string, number>();
|
|
57
|
+
colours.forEach((c, i) => {
|
|
58
|
+
const index = i === colours.length - 1 ? 255 : i;
|
|
59
|
+
palette.rgb[index] = [1, 3, 5].map((at) => parseInt(c.slice(at, at + 2), 16)) as [number, number, number];
|
|
60
|
+
indices.set(c, index);
|
|
61
|
+
});
|
|
62
|
+
let images = 0;
|
|
63
|
+
const image = (value: unknown, where: string, opaque: boolean) => {
|
|
64
|
+
const filename = path(value, where);
|
|
65
|
+
let img: ReturnType<typeof decodePng>;
|
|
66
|
+
try { img = decodePng(readFileSync(filename)); }
|
|
67
|
+
catch (e) { return fail(where, `${filename}: ${(e as Error).message}`); }
|
|
68
|
+
int(img.width, `${where}.width`, 1, 320);
|
|
69
|
+
int(img.height, `${where}.height`, 1, 200);
|
|
70
|
+
const pixels = new Uint8Array(img.width * img.height);
|
|
71
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
72
|
+
const [r, g, b, a] = img.data.subarray(i * 4, i * 4 + 4);
|
|
73
|
+
const at = `${where} (${i % img.width},${Math.floor(i / img.width)})`;
|
|
74
|
+
if (a !== 0 && a !== 255) fail(at, `alpha ${a}; export without antialiasing or partial opacity`);
|
|
75
|
+
if (a === 0) {
|
|
76
|
+
if (opaque) fail(at, "background must be opaque");
|
|
77
|
+
pixels[i] = 254;
|
|
78
|
+
} else {
|
|
79
|
+
const hex = `#${[r, g, b].map((c) => c!.toString(16).padStart(2, "0")).join("")}`;
|
|
80
|
+
const index = indices.get(hex);
|
|
81
|
+
if (index === undefined) fail(at, `${hex} is outside the project palette`);
|
|
82
|
+
pixels[i] = index!;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
images++;
|
|
86
|
+
return { width: img.width, height: img.height, pixels, skipColor: 254 };
|
|
87
|
+
};
|
|
88
|
+
const resources: ResourceData[] = [{ type: ResourceType.Palette, number: 999, data: writeHunkPalette(palette) }];
|
|
89
|
+
const numbers = new Set<string>();
|
|
90
|
+
const number = (value: unknown, type: string, where: string) => {
|
|
91
|
+
const n = int(value, where, 0, 65535);
|
|
92
|
+
if (numbers.has(`${type}:${n}`)) fail(where, `duplicate ${type} ${n}`);
|
|
93
|
+
numbers.add(`${type}:${n}`);
|
|
94
|
+
return n;
|
|
95
|
+
};
|
|
96
|
+
list(m.pictures, "pictures", 0, 65536).forEach((value, i) => {
|
|
97
|
+
const where = `pictures[${i}]`;
|
|
98
|
+
const pic = object(value, where, ["number", "layers"]);
|
|
99
|
+
const n = number(pic.number, "picture", `${where}.number`);
|
|
100
|
+
const priorities = new Set<number>();
|
|
101
|
+
const cels = list(pic.layers, `${where}.layers`, 1, 255).map((value, j) => {
|
|
102
|
+
const at = `${where}.layers[${j}]`;
|
|
103
|
+
const layer = object(value, at, ["png", "priority"]);
|
|
104
|
+
const priority = int(layer.priority, `${at}.priority`, -32768, 32767);
|
|
105
|
+
if (j === 0 ? priority !== -1000 : priority < 0 || priority > 199) fail(at, "background priority is -1000; foreground priorities are 0..199");
|
|
106
|
+
if (priorities.has(priority)) fail(at, "duplicate layer priority; combine these layers in the editor");
|
|
107
|
+
priorities.add(priority);
|
|
108
|
+
const img = image(layer.png, `${at}.png`, j === 0);
|
|
109
|
+
if (img.width !== 320 || img.height !== 200) fail(at, "picture layers must be 320x200");
|
|
110
|
+
return { ...img, displaceX: 0, displaceY: 0, priority, x: 0, y: 0, unknown16: 0 };
|
|
111
|
+
});
|
|
112
|
+
resources.push({ type: ResourceType.Pic, number: n, data: writePic({ resolution: [320, 200], cels, palette }) });
|
|
113
|
+
});
|
|
114
|
+
list(m.views, "views", 0, 65536).forEach((value, i) => {
|
|
115
|
+
const where = `views[${i}]`;
|
|
116
|
+
const view = object(value, where, ["number", "loops"]);
|
|
117
|
+
const n = number(view.number, "view", `${where}.number`);
|
|
118
|
+
const loops: ViewLoop[] = [];
|
|
119
|
+
let total = 0;
|
|
120
|
+
list(view.loops, `${where}.loops`, 1, 128).forEach((value, j) => {
|
|
121
|
+
const at = `${where}.loops[${j}]`;
|
|
122
|
+
const loop = object(value, at, ["cels", "link", "mirror"]);
|
|
123
|
+
if ("link" in loop) {
|
|
124
|
+
if ("cels" in loop) fail(at, "linked loops cannot also have cels");
|
|
125
|
+
const link = int(loop.link, `${at}.link`, 0, j - 1);
|
|
126
|
+
if (loops[link]!.link !== -1) fail(at, "link must refer to an earlier loop with its own cels");
|
|
127
|
+
if (typeof loop.mirror !== "boolean") fail(at, "mirror must be a boolean");
|
|
128
|
+
loops.push({ link, mirror: loop.mirror as boolean, cels: [] });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if ("mirror" in loop) fail(at, "mirror requires link");
|
|
132
|
+
let size = "";
|
|
133
|
+
const cels = list(loop.cels, `${at}.cels`, 1, 255).map((value, k) => {
|
|
134
|
+
const pos = `${at}.cels[${k}]`;
|
|
135
|
+
const cel = object(value, pos, ["png", "anchor"]);
|
|
136
|
+
const img = image(cel.png, `${pos}.png`, false);
|
|
137
|
+
const dimensions = `${img.width}x${img.height}`;
|
|
138
|
+
if (size && size !== dimensions) fail(pos, "keep the same canvas size throughout a loop (disable trimming)");
|
|
139
|
+
size = dimensions;
|
|
140
|
+
const anchor = list(cel.anchor, `${pos}.anchor`, 2, 2);
|
|
141
|
+
const x = int(anchor[0], `${pos}.anchor[0]`, 0, img.width - 1);
|
|
142
|
+
const y = int(anchor[1], `${pos}.anchor[1]`, 0, img.height - 1);
|
|
143
|
+
total++;
|
|
144
|
+
return { ...img, displaceX: (img.width >> 1) - x, displaceY: img.height - 1 - y };
|
|
145
|
+
});
|
|
146
|
+
loops.push({ link: -1, mirror: false, cels });
|
|
147
|
+
});
|
|
148
|
+
if (total > 255) fail(where, "a view supports at most 255 stored cels");
|
|
149
|
+
resources.push({ type: ResourceType.View, number: n, data: writeView({ flags: 1, loops, palette }) });
|
|
150
|
+
});
|
|
151
|
+
resources.sort((a, b) => a.type - b.type || a.number - b.number);
|
|
152
|
+
return { resources, colours, images };
|
|
153
|
+
}
|
package/tools/art.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/tools/art.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { writeResourceArchive } from "../packages/sci/src/index.js";
|
|
4
|
+
import { ArtError, buildArt } from "./art/build.js";
|
|
5
|
+
const [command, manifest, outArg] = process.argv.slice(2);
|
|
6
|
+
if (!manifest || (command !== "check" && command !== "build")) {
|
|
7
|
+
console.error("usage: pnpm art <check|build> games/<name>/art/art.json [out dir]");
|
|
8
|
+
process.exit(2);
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const art = buildArt(manifest);
|
|
12
|
+
if (command === "build") {
|
|
13
|
+
const out = outArg ?? join("out/art", basename(dirname(dirname(manifest))));
|
|
14
|
+
const { map, volume } = writeResourceArchive(art.resources);
|
|
15
|
+
mkdirSync(out, { recursive: true });
|
|
16
|
+
writeFileSync(join(out, "RESOURCE.MAP"), map);
|
|
17
|
+
writeFileSync(join(out, "RESOURCE.000"), volume);
|
|
18
|
+
const gpl = ["GIMP Palette", "Name: SCI project palette", "Columns: 8", "#"];
|
|
19
|
+
for (const hex of art.colours)
|
|
20
|
+
gpl.push(`${[1, 3, 5].map((at) => parseInt(hex.slice(at, at + 2), 16)).join(" ")} ${hex}`);
|
|
21
|
+
writeFileSync(join(out, "palette.gpl"), `${gpl.join("\n")}\n`);
|
|
22
|
+
console.log(`${out}: art-only archive (no game scripts), palette.gpl`);
|
|
23
|
+
}
|
|
24
|
+
console.log(`${manifest}: ${art.images} PNGs checked, ${art.colours.length} colours, ${art.resources.length} resources`);
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
if (!(e instanceof ArtError))
|
|
28
|
+
throw e;
|
|
29
|
+
console.error(e.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
package/tools/art.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { writeResourceArchive } from "../packages/sci/src/index.ts";
|
|
4
|
+
import { ArtError, buildArt } from "./art/build.ts";
|
|
5
|
+
|
|
6
|
+
const [command, manifest, outArg] = process.argv.slice(2);
|
|
7
|
+
if (!manifest || (command !== "check" && command !== "build")) {
|
|
8
|
+
console.error("usage: pnpm art <check|build> games/<name>/art/art.json [out dir]");
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const art = buildArt(manifest);
|
|
13
|
+
if (command === "build") {
|
|
14
|
+
const out = outArg ?? join("out/art", basename(dirname(dirname(manifest))));
|
|
15
|
+
const { map, volume } = writeResourceArchive(art.resources);
|
|
16
|
+
mkdirSync(out, { recursive: true });
|
|
17
|
+
writeFileSync(join(out, "RESOURCE.MAP"), map);
|
|
18
|
+
writeFileSync(join(out, "RESOURCE.000"), volume);
|
|
19
|
+
const gpl = ["GIMP Palette", "Name: SCI project palette", "Columns: 8", "#"];
|
|
20
|
+
for (const hex of art.colours) gpl.push(`${[1, 3, 5].map((at) => parseInt(hex.slice(at, at + 2), 16)).join(" ")} ${hex}`);
|
|
21
|
+
writeFileSync(join(out, "palette.gpl"), `${gpl.join("\n")}\n`);
|
|
22
|
+
console.log(`${out}: art-only archive (no game scripts), palette.gpl`);
|
|
23
|
+
}
|
|
24
|
+
console.log(`${manifest}: ${art.images} PNGs checked, ${art.colours.length} colours, ${art.resources.length} resources`);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (!(e instanceof ArtError)) throw e;
|
|
27
|
+
console.error(e.message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
package/tools/asm.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/tools/asm.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
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.js";
|
|
11
|
+
import { nodeFiles } from "./node-files.js";
|
|
12
|
+
import { writePatch } from "./patches.js";
|
|
13
|
+
const [command = "roundtrip", ...args] = process.argv.slice(2);
|
|
14
|
+
const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
|
|
15
|
+
const world = await ScriptWorld.open(rm);
|
|
16
|
+
await world.loadAllClasses();
|
|
17
|
+
const ctx = world.asmContext();
|
|
18
|
+
const firstDiff = (a, b) => {
|
|
19
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++)
|
|
20
|
+
if (a[i] !== b[i])
|
|
21
|
+
return i;
|
|
22
|
+
return -1;
|
|
23
|
+
};
|
|
24
|
+
if (command === "dump") {
|
|
25
|
+
const text = toAssembly(await world.script(Number(args[0])), ctx);
|
|
26
|
+
if (args[1])
|
|
27
|
+
writeFileSync(args[1], text);
|
|
28
|
+
else
|
|
29
|
+
process.stdout.write(text);
|
|
30
|
+
}
|
|
31
|
+
else if (command === "build") {
|
|
32
|
+
build(readFileSync(args[0], "utf8"), args[1] ?? "out/mods");
|
|
33
|
+
}
|
|
34
|
+
else if (command === "roundtrip") {
|
|
35
|
+
const numbers = args.length ? args.map(Number) : rm.list(ResourceType.Script).map((r) => r.number).filter((n) => rm.has({ type: ResourceType.Heap, number: n }));
|
|
36
|
+
let ok = 0;
|
|
37
|
+
const failures = [];
|
|
38
|
+
for (const n of numbers) {
|
|
39
|
+
const s = await world.script(n);
|
|
40
|
+
try {
|
|
41
|
+
const out = assemble(toAssembly(s, ctx), ctx);
|
|
42
|
+
const dc = firstDiff(s.code, out.code), dh = firstDiff(s.heap, out.heap);
|
|
43
|
+
if (dc < 0 && dh < 0)
|
|
44
|
+
ok++;
|
|
45
|
+
else
|
|
46
|
+
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)` : ""}`);
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
failures.push(`${n}: ${e.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
console.log(`${ok}/${numbers.length} scripts round-trip byte-exact`);
|
|
53
|
+
if (failures.length)
|
|
54
|
+
console.log(failures.slice(0, 20).join("\n"));
|
|
55
|
+
process.exitCode = failures.length ? 1 : 0;
|
|
56
|
+
}
|
|
57
|
+
function build(text, dir) {
|
|
58
|
+
const out = assemble(text, ctx);
|
|
59
|
+
writePatch(dir, ResourceType.Script, out.number, out.code);
|
|
60
|
+
writePatch(dir, ResourceType.Heap, out.number, out.heap);
|
|
61
|
+
}
|