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,108 @@
|
|
|
1
|
+
import type { MidiEvent } from "./midi.ts";
|
|
2
|
+
import { SolFlag } from "./sol.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Writers for sound: sound resources (music, in the layout parseSound reads), SOL clips, and
|
|
6
|
+
* the effects file RESOURCE.SFX with its index, map 65535 (the layout AudioIndex reads).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface SoundChannel {
|
|
10
|
+
/** 0-15; 15 is the control channel (signals and cues to the scripts), not music. */
|
|
11
|
+
midiChannel: number;
|
|
12
|
+
/** Voices it wants from a voice-limited device (AdLib); others ignore it. */
|
|
13
|
+
voices: number;
|
|
14
|
+
/** Absolute ticks (1/60 s); bytes are MIDI messages (their channel nibble is replaced). */
|
|
15
|
+
events: MidiEvent[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SoundSpec {
|
|
19
|
+
priority?: number;
|
|
20
|
+
/** One per sound device (SoundDevice): each device plays its own arrangement. */
|
|
21
|
+
tracks: { device: number; channels: SoundChannel[] }[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const dataBytes = (status: number) => ((status & 0xf0) === 0xc0 || (status & 0xf0) === 0xd0 ? 1 : 2);
|
|
25
|
+
|
|
26
|
+
/** A channel stream: channel, voices, then delta-timed events with running status, then 0xFC. */
|
|
27
|
+
function channelStream(c: SoundChannel): number[] {
|
|
28
|
+
if (c.midiChannel < 0 || c.midiChannel > 15) throw new Error(`MIDI channel ${c.midiChannel} isn't 0-15`);
|
|
29
|
+
const out = [c.midiChannel, c.voices & 0x0f];
|
|
30
|
+
const events = [...c.events].sort((a, b) => a.tick - b.tick);
|
|
31
|
+
let tick = 0, running = -1;
|
|
32
|
+
for (const e of events) {
|
|
33
|
+
if (e.tick < tick) throw new Error(`event at tick ${e.tick} before ${tick}`);
|
|
34
|
+
let delta = Math.round(e.tick - tick);
|
|
35
|
+
tick += delta;
|
|
36
|
+
// A delta of 240 or more is 0xF8 bytes of 240 each, then the rest.
|
|
37
|
+
for (; delta >= 240; delta -= 240) out.push(0xf8);
|
|
38
|
+
out.push(delta);
|
|
39
|
+
const status = (e.bytes[0]! & 0xf0) | c.midiChannel;
|
|
40
|
+
if (status < 0x80 || status >= 0xf0) throw new Error(`not a channel message: ${e.bytes.map((b) => b.toString(16)).join(" ")}`);
|
|
41
|
+
if (status !== running) out.push(status);
|
|
42
|
+
running = status;
|
|
43
|
+
const n = dataBytes(status);
|
|
44
|
+
for (let i = 1; i <= n; i++) out.push((e.bytes[i] ?? 0) & 0x7f);
|
|
45
|
+
}
|
|
46
|
+
out.push(0, 0xfc);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function writeSound(spec: SoundSpec): Uint8Array {
|
|
51
|
+
const header = [0xf0, spec.priority ?? 0, 0, 0, 0, 0, 0, 0];
|
|
52
|
+
const streams = spec.tracks.map((t) => t.channels.map(channelStream));
|
|
53
|
+
// Track table: device, 6-byte entries (u16 0, u16 offset, u16 size), 0xFF; then 0xFF.
|
|
54
|
+
const tableSize = spec.tracks.reduce((n, t) => n + 1 + t.channels.length * 6 + 1, 0) + 1;
|
|
55
|
+
let at = header.length + tableSize;
|
|
56
|
+
const table: number[] = [];
|
|
57
|
+
const body: number[] = [];
|
|
58
|
+
spec.tracks.forEach((t, i) => {
|
|
59
|
+
table.push(t.device);
|
|
60
|
+
for (const s of streams[i]!) {
|
|
61
|
+
if (at + s.length > 0xffff) throw new Error("sound too large: offsets are 16-bit (64 KB)");
|
|
62
|
+
table.push(0, 0, at & 0xff, at >> 8, s.length & 0xff, s.length >> 8);
|
|
63
|
+
body.push(...s);
|
|
64
|
+
at += s.length;
|
|
65
|
+
}
|
|
66
|
+
table.push(0xff);
|
|
67
|
+
});
|
|
68
|
+
table.push(0xff);
|
|
69
|
+
return Uint8Array.from([...header, ...table, ...body]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A SOL clip: 16-bit samples at `rate`, uncompressed. */
|
|
73
|
+
export function writeSol(samples: Int16Array, rate: number): Uint8Array {
|
|
74
|
+
const out = new Uint8Array(13 + samples.length * 2);
|
|
75
|
+
const v = new DataView(out.buffer);
|
|
76
|
+
out[0] = 0x8d; // audio
|
|
77
|
+
out[1] = 11; // header bytes after these two
|
|
78
|
+
out.set([0x53, 0x4f, 0x4c, 0], 2); // "SOL\0"
|
|
79
|
+
v.setUint16(6, rate, true);
|
|
80
|
+
out[8] = SolFlag.Bits16 | SolFlag.Signed;
|
|
81
|
+
v.setUint32(9, samples.length * 2, true);
|
|
82
|
+
samples.forEach((s, i) => v.setInt16(13 + i * 2, s, true));
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* RESOURCE.SFX (the SOL clips, one after another) and its index, map 65535: per clip, u16
|
|
88
|
+
* sound number and u24 offset from the previous clip; 0xFFFF ends it.
|
|
89
|
+
*/
|
|
90
|
+
export function writeEffects(clips: readonly { number: number; sol: Uint8Array }[]): { sfx: Uint8Array; map: Uint8Array } {
|
|
91
|
+
const sorted = [...clips].sort((a, b) => a.number - b.number);
|
|
92
|
+
const sfx = new Uint8Array(sorted.reduce((n, c) => n + c.sol.length, 0));
|
|
93
|
+
const map = new Uint8Array(sorted.length * 5 + 5);
|
|
94
|
+
const v = new DataView(map.buffer);
|
|
95
|
+
let at = 0, prev = 0;
|
|
96
|
+
sorted.forEach((c, i) => {
|
|
97
|
+
if (i && c.number === sorted[i - 1]!.number) throw new Error(`sound effect ${c.number} is there twice`);
|
|
98
|
+
if (at - prev > 0xffffff) throw new Error("sound effect too large");
|
|
99
|
+
sfx.set(c.sol, at);
|
|
100
|
+
v.setUint16(i * 5, c.number, true);
|
|
101
|
+
const delta = at - prev;
|
|
102
|
+
map.set([delta & 0xff, (delta >> 8) & 0xff, delta >> 16], i * 5 + 2);
|
|
103
|
+
prev = at;
|
|
104
|
+
at += c.sol.length;
|
|
105
|
+
});
|
|
106
|
+
v.setUint16(sorted.length * 5, 0xffff, true);
|
|
107
|
+
return { sfx, map };
|
|
108
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Palette } from "./palette.ts";
|
|
2
|
+
/** A single decoded image: 8-bit palette indices, row-major. */
|
|
3
|
+
export interface Cel {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
/** Signed displacement from the anchor point, as stored in the header. */
|
|
7
|
+
displaceX: number;
|
|
8
|
+
displaceY: number;
|
|
9
|
+
/** Palette index that is transparent. */
|
|
10
|
+
skipColor: number;
|
|
11
|
+
pixels: Uint8Array;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Decodes an SCI32 cel. The header layout is shared by views and pics:
|
|
15
|
+
*
|
|
16
|
+
* +0 u16 width +2 u16 height
|
|
17
|
+
* +4 i16 displaceX +6 i16 displaceY
|
|
18
|
+
* +8 u8 skip colour +9 u8 compression (0 = raw, 0x8A = RLE)
|
|
19
|
+
* +24 u32 data offset (RLE control stream, or raw pixels)
|
|
20
|
+
* +28 u32 literal offset (RLE literal pixels)
|
|
21
|
+
* +32 u32 row table offset (RLE: height u32 control offsets, then height u32 literal offsets)
|
|
22
|
+
*
|
|
23
|
+
* All offsets are relative to the start of the resource.
|
|
24
|
+
*/
|
|
25
|
+
export declare function decodeCel(data: Uint8Array, headerOffset: number): Cel;
|
|
26
|
+
export interface RgbaOptions {
|
|
27
|
+
mirror?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Remap colours: indices that aren't real colours but tell the engine to transform
|
|
30
|
+
* whatever is underneath (e.g. 253/254 for shadows). Scripts configure the real
|
|
31
|
+
* transform at runtime; until the VM exists they're approximated as translucent black.
|
|
32
|
+
*/
|
|
33
|
+
remap?: ReadonlyMap<number, number>;
|
|
34
|
+
}
|
|
35
|
+
/** Converts a cel to RGBA using `palette`, making the skip colour transparent. */
|
|
36
|
+
export declare function celToRgba(cel: Cel, palette: Palette, { mirror, remap }?: RgbaOptions): Uint8ClampedArray<ArrayBuffer>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const CelCompression = { None: 0, Rle: 0x8a };
|
|
2
|
+
/**
|
|
3
|
+
* Decodes an SCI32 cel. The header layout is shared by views and pics:
|
|
4
|
+
*
|
|
5
|
+
* +0 u16 width +2 u16 height
|
|
6
|
+
* +4 i16 displaceX +6 i16 displaceY
|
|
7
|
+
* +8 u8 skip colour +9 u8 compression (0 = raw, 0x8A = RLE)
|
|
8
|
+
* +24 u32 data offset (RLE control stream, or raw pixels)
|
|
9
|
+
* +28 u32 literal offset (RLE literal pixels)
|
|
10
|
+
* +32 u32 row table offset (RLE: height u32 control offsets, then height u32 literal offsets)
|
|
11
|
+
*
|
|
12
|
+
* All offsets are relative to the start of the resource.
|
|
13
|
+
*/
|
|
14
|
+
export function decodeCel(data, headerOffset) {
|
|
15
|
+
const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
16
|
+
const h = headerOffset;
|
|
17
|
+
const width = v.getUint16(h, true);
|
|
18
|
+
const height = v.getUint16(h + 2, true);
|
|
19
|
+
const cel = {
|
|
20
|
+
width,
|
|
21
|
+
height,
|
|
22
|
+
displaceX: v.getInt16(h + 4, true),
|
|
23
|
+
displaceY: v.getInt16(h + 6, true),
|
|
24
|
+
skipColor: data[h + 8],
|
|
25
|
+
pixels: new Uint8Array(width * height),
|
|
26
|
+
};
|
|
27
|
+
const compression = data[h + 9];
|
|
28
|
+
const dataOffset = v.getUint32(h + 24, true);
|
|
29
|
+
if (compression === CelCompression.None) {
|
|
30
|
+
cel.pixels.set(data.subarray(dataOffset, dataOffset + width * height));
|
|
31
|
+
return cel;
|
|
32
|
+
}
|
|
33
|
+
if (compression !== CelCompression.Rle) {
|
|
34
|
+
throw new Error(`Unknown cel compression 0x${compression.toString(16)}`);
|
|
35
|
+
}
|
|
36
|
+
const literalBase = v.getUint32(h + 28, true);
|
|
37
|
+
const rowTable = v.getUint32(h + 32, true);
|
|
38
|
+
for (let y = 0; y < height; y++) {
|
|
39
|
+
let ctrl = dataOffset + v.getUint32(rowTable + y * 4, true);
|
|
40
|
+
let lit = literalBase + v.getUint32(rowTable + (height + y) * 4, true);
|
|
41
|
+
const row = y * width;
|
|
42
|
+
for (let x = 0; x < width;) {
|
|
43
|
+
const op = data[ctrl++];
|
|
44
|
+
if (op & 0x80) {
|
|
45
|
+
const len = op & 0x3f;
|
|
46
|
+
// 11xxxxxx = run of transparent; 10xxxxxx = run of the next literal byte
|
|
47
|
+
const color = op & 0x40 ? cel.skipColor : data[lit++];
|
|
48
|
+
cel.pixels.fill(color, row + x, row + x + len);
|
|
49
|
+
x += len;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// 0xxxxxxx = copy that many literal bytes
|
|
53
|
+
cel.pixels.set(data.subarray(lit, lit + op), row + x);
|
|
54
|
+
lit += op;
|
|
55
|
+
x += op;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return cel;
|
|
60
|
+
}
|
|
61
|
+
/** Converts a cel to RGBA using `palette`, making the skip colour transparent. */
|
|
62
|
+
export function celToRgba(cel, palette, { mirror = false, remap } = {}) {
|
|
63
|
+
const out = new Uint8ClampedArray(cel.width * cel.height * 4);
|
|
64
|
+
for (let y = 0; y < cel.height; y++) {
|
|
65
|
+
for (let x = 0; x < cel.width; x++) {
|
|
66
|
+
const src = cel.pixels[y * cel.width + (mirror ? cel.width - 1 - x : x)];
|
|
67
|
+
if (src === cel.skipColor)
|
|
68
|
+
continue;
|
|
69
|
+
const o = (y * cel.width + x) * 4;
|
|
70
|
+
const remapAlpha = remap?.get(src);
|
|
71
|
+
if (remapAlpha !== undefined) {
|
|
72
|
+
out[o + 3] = remapAlpha;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
out[o] = palette.rgb[src * 3];
|
|
76
|
+
out[o + 1] = palette.rgb[src * 3 + 1];
|
|
77
|
+
out[o + 2] = palette.rgb[src * 3 + 2];
|
|
78
|
+
out[o + 3] = 255;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Palette } from "./palette.ts";
|
|
2
|
+
|
|
3
|
+
/** A single decoded image: 8-bit palette indices, row-major. */
|
|
4
|
+
export interface Cel {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
/** Signed displacement from the anchor point, as stored in the header. */
|
|
8
|
+
displaceX: number;
|
|
9
|
+
displaceY: number;
|
|
10
|
+
/** Palette index that is transparent. */
|
|
11
|
+
skipColor: number;
|
|
12
|
+
pixels: Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CelCompression = { None: 0, Rle: 0x8a } as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Decodes an SCI32 cel. The header layout is shared by views and pics:
|
|
19
|
+
*
|
|
20
|
+
* +0 u16 width +2 u16 height
|
|
21
|
+
* +4 i16 displaceX +6 i16 displaceY
|
|
22
|
+
* +8 u8 skip colour +9 u8 compression (0 = raw, 0x8A = RLE)
|
|
23
|
+
* +24 u32 data offset (RLE control stream, or raw pixels)
|
|
24
|
+
* +28 u32 literal offset (RLE literal pixels)
|
|
25
|
+
* +32 u32 row table offset (RLE: height u32 control offsets, then height u32 literal offsets)
|
|
26
|
+
*
|
|
27
|
+
* All offsets are relative to the start of the resource.
|
|
28
|
+
*/
|
|
29
|
+
export function decodeCel(data: Uint8Array, headerOffset: number): Cel {
|
|
30
|
+
const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
31
|
+
const h = headerOffset;
|
|
32
|
+
const width = v.getUint16(h, true);
|
|
33
|
+
const height = v.getUint16(h + 2, true);
|
|
34
|
+
const cel: Cel = {
|
|
35
|
+
width,
|
|
36
|
+
height,
|
|
37
|
+
displaceX: v.getInt16(h + 4, true),
|
|
38
|
+
displaceY: v.getInt16(h + 6, true),
|
|
39
|
+
skipColor: data[h + 8]!,
|
|
40
|
+
pixels: new Uint8Array(width * height),
|
|
41
|
+
};
|
|
42
|
+
const compression = data[h + 9]!;
|
|
43
|
+
const dataOffset = v.getUint32(h + 24, true);
|
|
44
|
+
|
|
45
|
+
if (compression === CelCompression.None) {
|
|
46
|
+
cel.pixels.set(data.subarray(dataOffset, dataOffset + width * height));
|
|
47
|
+
return cel;
|
|
48
|
+
}
|
|
49
|
+
if (compression !== CelCompression.Rle) {
|
|
50
|
+
throw new Error(`Unknown cel compression 0x${compression.toString(16)}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const literalBase = v.getUint32(h + 28, true);
|
|
54
|
+
const rowTable = v.getUint32(h + 32, true);
|
|
55
|
+
|
|
56
|
+
for (let y = 0; y < height; y++) {
|
|
57
|
+
let ctrl = dataOffset + v.getUint32(rowTable + y * 4, true);
|
|
58
|
+
let lit = literalBase + v.getUint32(rowTable + (height + y) * 4, true);
|
|
59
|
+
const row = y * width;
|
|
60
|
+
|
|
61
|
+
for (let x = 0; x < width; ) {
|
|
62
|
+
const op = data[ctrl++]!;
|
|
63
|
+
if (op & 0x80) {
|
|
64
|
+
const len = op & 0x3f;
|
|
65
|
+
// 11xxxxxx = run of transparent; 10xxxxxx = run of the next literal byte
|
|
66
|
+
const color = op & 0x40 ? cel.skipColor : data[lit++]!;
|
|
67
|
+
cel.pixels.fill(color, row + x, row + x + len);
|
|
68
|
+
x += len;
|
|
69
|
+
} else {
|
|
70
|
+
// 0xxxxxxx = copy that many literal bytes
|
|
71
|
+
cel.pixels.set(data.subarray(lit, lit + op), row + x);
|
|
72
|
+
lit += op;
|
|
73
|
+
x += op;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return cel;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface RgbaOptions {
|
|
81
|
+
mirror?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Remap colours: indices that aren't real colours but tell the engine to transform
|
|
84
|
+
* whatever is underneath (e.g. 253/254 for shadows). Scripts configure the real
|
|
85
|
+
* transform at runtime; until the VM exists they're approximated as translucent black.
|
|
86
|
+
*/
|
|
87
|
+
remap?: ReadonlyMap<number, number>; // index -> alpha 0..255
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Converts a cel to RGBA using `palette`, making the skip colour transparent. */
|
|
91
|
+
export function celToRgba(cel: Cel, palette: Palette, { mirror = false, remap }: RgbaOptions = {}): Uint8ClampedArray<ArrayBuffer> {
|
|
92
|
+
const out = new Uint8ClampedArray(cel.width * cel.height * 4);
|
|
93
|
+
for (let y = 0; y < cel.height; y++) {
|
|
94
|
+
for (let x = 0; x < cel.width; x++) {
|
|
95
|
+
const src = cel.pixels[y * cel.width + (mirror ? cel.width - 1 - x : x)]!;
|
|
96
|
+
if (src === cel.skipColor) continue;
|
|
97
|
+
const o = (y * cel.width + x) * 4;
|
|
98
|
+
const remapAlpha = remap?.get(src);
|
|
99
|
+
if (remapAlpha !== undefined) {
|
|
100
|
+
out[o + 3] = remapAlpha;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
out[o] = palette.rgb[src * 3]!;
|
|
104
|
+
out[o + 1] = palette.rgb[src * 3 + 1]!;
|
|
105
|
+
out[o + 2] = palette.rgb[src * 3 + 2]!;
|
|
106
|
+
out[o + 3] = 255;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** 256-colour VGA palette: 8-bit RGB triplets plus a per-entry "used" flag. */
|
|
2
|
+
export interface Palette {
|
|
3
|
+
rgb: Uint8Array;
|
|
4
|
+
used: Uint8Array;
|
|
5
|
+
}
|
|
6
|
+
export declare const emptyPalette: () => Palette;
|
|
7
|
+
/**
|
|
8
|
+
* Parses a "hunk palette", the SCI32 palette format used by palette resources and
|
|
9
|
+
* embedded in pics and views.
|
|
10
|
+
*
|
|
11
|
+
* header (13 bytes): byte 10 = number of palettes
|
|
12
|
+
* u16 per palette (skipped)
|
|
13
|
+
* entry header (22 bytes):
|
|
14
|
+
* +10 u8 first colour index
|
|
15
|
+
* +14 u16 colour count
|
|
16
|
+
* +16 u8 default "used" flag
|
|
17
|
+
* +17 u8 shared-used: 1 = entries are RGB (3 bytes), 0 = used+RGB (4 bytes)
|
|
18
|
+
* colours
|
|
19
|
+
*
|
|
20
|
+
* Returns a palette with only the entries this hunk defines marked used.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseHunkPalette(data: Uint8Array, offset?: number): Palette | undefined;
|
|
23
|
+
/** Returns `base` with every used entry of `over` applied on top. */
|
|
24
|
+
export declare function mergePalette(base: Palette, over: Palette | undefined): Palette;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const emptyPalette = () => ({ rgb: new Uint8Array(768), used: new Uint8Array(256) });
|
|
2
|
+
/**
|
|
3
|
+
* Parses a "hunk palette", the SCI32 palette format used by palette resources and
|
|
4
|
+
* embedded in pics and views.
|
|
5
|
+
*
|
|
6
|
+
* header (13 bytes): byte 10 = number of palettes
|
|
7
|
+
* u16 per palette (skipped)
|
|
8
|
+
* entry header (22 bytes):
|
|
9
|
+
* +10 u8 first colour index
|
|
10
|
+
* +14 u16 colour count
|
|
11
|
+
* +16 u8 default "used" flag
|
|
12
|
+
* +17 u8 shared-used: 1 = entries are RGB (3 bytes), 0 = used+RGB (4 bytes)
|
|
13
|
+
* colours
|
|
14
|
+
*
|
|
15
|
+
* Returns a palette with only the entries this hunk defines marked used.
|
|
16
|
+
*/
|
|
17
|
+
export function parseHunkPalette(data, offset = 0) {
|
|
18
|
+
const count = data[offset + 10] ?? 0;
|
|
19
|
+
if (count === 0)
|
|
20
|
+
return undefined;
|
|
21
|
+
const entry = offset + 13 + 2 * count;
|
|
22
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
23
|
+
const start = data[entry + 10];
|
|
24
|
+
const numColors = view.getUint16(entry + 14, true);
|
|
25
|
+
const defaultUsed = data[entry + 16];
|
|
26
|
+
const sharedUsed = data[entry + 17] !== 0;
|
|
27
|
+
const pal = emptyPalette();
|
|
28
|
+
let pos = entry + 22;
|
|
29
|
+
for (let i = start; i < start + numColors && i < 256; i++) {
|
|
30
|
+
pal.used[i] = sharedUsed ? defaultUsed : data[pos++];
|
|
31
|
+
pal.rgb[i * 3] = data[pos++];
|
|
32
|
+
pal.rgb[i * 3 + 1] = data[pos++];
|
|
33
|
+
pal.rgb[i * 3 + 2] = data[pos++];
|
|
34
|
+
}
|
|
35
|
+
return pal;
|
|
36
|
+
}
|
|
37
|
+
/** Returns `base` with every used entry of `over` applied on top. */
|
|
38
|
+
export function mergePalette(base, over) {
|
|
39
|
+
if (!over)
|
|
40
|
+
return base;
|
|
41
|
+
const out = { rgb: base.rgb.slice(), used: base.used.slice() };
|
|
42
|
+
for (let i = 0; i < 256; i++) {
|
|
43
|
+
if (!over.used[i])
|
|
44
|
+
continue;
|
|
45
|
+
out.used[i] = 1;
|
|
46
|
+
out.rgb.set(over.rgb.subarray(i * 3, i * 3 + 3), i * 3);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** 256-colour VGA palette: 8-bit RGB triplets plus a per-entry "used" flag. */
|
|
2
|
+
export interface Palette {
|
|
3
|
+
rgb: Uint8Array; // 256 * 3
|
|
4
|
+
used: Uint8Array; // 256
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const emptyPalette = (): Palette => ({ rgb: new Uint8Array(768), used: new Uint8Array(256) });
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parses a "hunk palette", the SCI32 palette format used by palette resources and
|
|
11
|
+
* embedded in pics and views.
|
|
12
|
+
*
|
|
13
|
+
* header (13 bytes): byte 10 = number of palettes
|
|
14
|
+
* u16 per palette (skipped)
|
|
15
|
+
* entry header (22 bytes):
|
|
16
|
+
* +10 u8 first colour index
|
|
17
|
+
* +14 u16 colour count
|
|
18
|
+
* +16 u8 default "used" flag
|
|
19
|
+
* +17 u8 shared-used: 1 = entries are RGB (3 bytes), 0 = used+RGB (4 bytes)
|
|
20
|
+
* colours
|
|
21
|
+
*
|
|
22
|
+
* Returns a palette with only the entries this hunk defines marked used.
|
|
23
|
+
*/
|
|
24
|
+
export function parseHunkPalette(data: Uint8Array, offset = 0): Palette | undefined {
|
|
25
|
+
const count = data[offset + 10] ?? 0;
|
|
26
|
+
if (count === 0) return undefined;
|
|
27
|
+
|
|
28
|
+
const entry = offset + 13 + 2 * count;
|
|
29
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
30
|
+
const start = data[entry + 10]!;
|
|
31
|
+
const numColors = view.getUint16(entry + 14, true);
|
|
32
|
+
const defaultUsed = data[entry + 16]!;
|
|
33
|
+
const sharedUsed = data[entry + 17]! !== 0;
|
|
34
|
+
|
|
35
|
+
const pal = emptyPalette();
|
|
36
|
+
let pos = entry + 22;
|
|
37
|
+
for (let i = start; i < start + numColors && i < 256; i++) {
|
|
38
|
+
pal.used[i] = sharedUsed ? defaultUsed : data[pos++]!;
|
|
39
|
+
pal.rgb[i * 3] = data[pos++]!;
|
|
40
|
+
pal.rgb[i * 3 + 1] = data[pos++]!;
|
|
41
|
+
pal.rgb[i * 3 + 2] = data[pos++]!;
|
|
42
|
+
}
|
|
43
|
+
return pal;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Returns `base` with every used entry of `over` applied on top. */
|
|
47
|
+
export function mergePalette(base: Palette, over: Palette | undefined): Palette {
|
|
48
|
+
if (!over) return base;
|
|
49
|
+
const out: Palette = { rgb: base.rgb.slice(), used: base.used.slice() };
|
|
50
|
+
for (let i = 0; i < 256; i++) {
|
|
51
|
+
if (!over.used[i]) continue;
|
|
52
|
+
out.used[i] = 1;
|
|
53
|
+
out.rgb.set(over.rgb.subarray(i * 3, i * 3 + 3), i * 3);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Palette } from "./palette.ts";
|
|
2
|
+
import type { PicCel } from "./pic.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A pic resource with everything needed to write it back byte for byte. Measured on all 59
|
|
5
|
+
* pics of an SCI2 game (every one has this exact layout; all cels are uncompressed):
|
|
6
|
+
*
|
|
7
|
+
* +0 u16 header size (14) +2 u8 cel count +3 u8 0 +4 u16 cel header size (42)
|
|
8
|
+
* +6 u32 palette offset +10 u16/u16 resolution (320,200, or 0,0 meaning the default)
|
|
9
|
+
* cel headers, 42 bytes each:
|
|
10
|
+
* +0 u16 width +2 u16 height +4 i16 displaceX +6 i16 displaceY +8 u8 skip colour
|
|
11
|
+
* +9 u8 compression (0 = raw) +10 u16 0 +12 u32 pixel bytes (w*h)
|
|
12
|
+
* +16 u32 `unknown16` (2768 in most pics, 6 in a few; the engine ignores it)
|
|
13
|
+
* +20 u32 0 +24 u32 pixel offset +28 u32 0 +32 u32 0
|
|
14
|
+
* +36 i16 priority +38 i16 x +40 i16 y
|
|
15
|
+
* 00 03 u32 palette size | hunk palette
|
|
16
|
+
* 00 04 u32 pixel bytes | each cel's pixels, in cel order
|
|
17
|
+
*/
|
|
18
|
+
export interface PicFile {
|
|
19
|
+
resolution: [number, number];
|
|
20
|
+
cels: (PicCel & {
|
|
21
|
+
unknown16: number;
|
|
22
|
+
})[];
|
|
23
|
+
palette: HunkPaletteFile;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A hunk palette with its exact header bytes. Pics use one entry: `first` and
|
|
27
|
+
* `count` colours, each with its own "used" flag (4 bytes per colour) unless `shared`.
|
|
28
|
+
*/
|
|
29
|
+
export interface HunkPaletteFile {
|
|
30
|
+
/** The 13-byte hunk header (byte 0 = 14, byte 10 = entry count = 1). */
|
|
31
|
+
header: Uint8Array;
|
|
32
|
+
/** The first 10 bytes of the entry header (editor data; varies, never read). */
|
|
33
|
+
entryPrefix: Uint8Array;
|
|
34
|
+
first: number;
|
|
35
|
+
used: number[];
|
|
36
|
+
rgb: [number, number, number][];
|
|
37
|
+
/** Shared "used" flag: every colour is 3 bytes and takes `defaultUsed`. */
|
|
38
|
+
shared: boolean;
|
|
39
|
+
defaultUsed: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function parseHunkPaletteFile(data: Uint8Array, offset: number): HunkPaletteFile;
|
|
42
|
+
export declare function writeHunkPalette(p: HunkPaletteFile): Uint8Array;
|
|
43
|
+
export declare function parsePicFile(data: Uint8Array): PicFile;
|
|
44
|
+
export declare function writePic(pic: PicFile): Uint8Array;
|
|
45
|
+
/** The palette a pic installs, as the engine sees it. */
|
|
46
|
+
export declare function hunkToPalette(p: HunkPaletteFile): Palette;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const PIC_HEADER = 14;
|
|
2
|
+
const CEL_HEADER = 42;
|
|
3
|
+
export function parseHunkPaletteFile(data, offset) {
|
|
4
|
+
const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
5
|
+
if (data[offset + 10] !== 1)
|
|
6
|
+
throw new Error(`Hunk palette with ${data[offset + 10]} entries: only 1 is supported`);
|
|
7
|
+
const entry = offset + 13 + 2;
|
|
8
|
+
const first = data[entry + 10];
|
|
9
|
+
const count = v.getUint16(entry + 14, true);
|
|
10
|
+
const defaultUsed = data[entry + 16];
|
|
11
|
+
const shared = data[entry + 17] !== 0;
|
|
12
|
+
const used = [], rgb = [];
|
|
13
|
+
let p = entry + 22;
|
|
14
|
+
for (let i = 0; i < count; i++) {
|
|
15
|
+
used.push(shared ? defaultUsed : data[p++]);
|
|
16
|
+
rgb.push([data[p], data[p + 1], data[p + 2]]);
|
|
17
|
+
p += 3;
|
|
18
|
+
}
|
|
19
|
+
return { header: data.slice(offset, offset + 13), entryPrefix: data.slice(entry, entry + 10), first, used, rgb, shared, defaultUsed };
|
|
20
|
+
}
|
|
21
|
+
export function writeHunkPalette(p) {
|
|
22
|
+
const colors = p.rgb.length * (p.shared ? 3 : 4);
|
|
23
|
+
const out = new Uint8Array(13 + 2 + 22 + colors);
|
|
24
|
+
const v = new DataView(out.buffer);
|
|
25
|
+
out.set(p.header, 0);
|
|
26
|
+
out[10] = 1;
|
|
27
|
+
v.setUint16(13, 22 + colors, true);
|
|
28
|
+
const entry = 15;
|
|
29
|
+
out.set(p.entryPrefix, entry);
|
|
30
|
+
out[entry + 10] = p.first;
|
|
31
|
+
v.setUint16(entry + 14, p.rgb.length, true);
|
|
32
|
+
out[entry + 16] = p.defaultUsed;
|
|
33
|
+
out[entry + 17] = p.shared ? 1 : 0;
|
|
34
|
+
let o = entry + 22;
|
|
35
|
+
p.rgb.forEach((c, i) => {
|
|
36
|
+
if (!p.shared)
|
|
37
|
+
out[o++] = p.used[i];
|
|
38
|
+
out.set(c, o);
|
|
39
|
+
o += 3;
|
|
40
|
+
});
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
export function parsePicFile(data) {
|
|
44
|
+
const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
45
|
+
const count = data[2];
|
|
46
|
+
const cels = Array.from({ length: count }, (_, i) => {
|
|
47
|
+
const h = PIC_HEADER + CEL_HEADER * i;
|
|
48
|
+
if (data[h + 9] !== 0)
|
|
49
|
+
throw new Error(`Compressed pic cels aren't supported (cel ${i})`);
|
|
50
|
+
const width = v.getUint16(h, true), height = v.getUint16(h + 2, true);
|
|
51
|
+
const at = v.getUint32(h + 24, true);
|
|
52
|
+
return {
|
|
53
|
+
width, height,
|
|
54
|
+
displaceX: v.getInt16(h + 4, true), displaceY: v.getInt16(h + 6, true),
|
|
55
|
+
skipColor: data[h + 8],
|
|
56
|
+
pixels: data.slice(at, at + width * height),
|
|
57
|
+
unknown16: v.getUint32(h + 16, true),
|
|
58
|
+
priority: v.getInt16(h + 36, true), x: v.getInt16(h + 38, true), y: v.getInt16(h + 40, true),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
return { resolution: [v.getUint16(10, true), v.getUint16(12, true)], cels, palette: parseHunkPaletteFile(data, v.getUint32(6, true)) };
|
|
62
|
+
}
|
|
63
|
+
export function writePic(pic) {
|
|
64
|
+
const palette = writeHunkPalette(pic.palette);
|
|
65
|
+
const headers = PIC_HEADER + CEL_HEADER * pic.cels.length;
|
|
66
|
+
const paletteAt = headers + 6;
|
|
67
|
+
const pixelsAt = paletteAt + palette.length + 6;
|
|
68
|
+
const pixelBytes = pic.cels.reduce((n, c) => n + c.width * c.height, 0);
|
|
69
|
+
const out = new Uint8Array(pixelsAt + pixelBytes);
|
|
70
|
+
const v = new DataView(out.buffer);
|
|
71
|
+
v.setUint16(0, PIC_HEADER, true);
|
|
72
|
+
out[2] = pic.cels.length;
|
|
73
|
+
v.setUint16(4, CEL_HEADER, true);
|
|
74
|
+
v.setUint32(6, paletteAt, true);
|
|
75
|
+
v.setUint16(10, pic.resolution[0], true);
|
|
76
|
+
v.setUint16(12, pic.resolution[1], true);
|
|
77
|
+
let at = pixelsAt;
|
|
78
|
+
pic.cels.forEach((c, i) => {
|
|
79
|
+
const h = PIC_HEADER + CEL_HEADER * i;
|
|
80
|
+
if (c.pixels.length !== c.width * c.height)
|
|
81
|
+
throw new Error(`cel ${i}: ${c.pixels.length} pixels for ${c.width}x${c.height}`);
|
|
82
|
+
v.setUint16(h, c.width, true);
|
|
83
|
+
v.setUint16(h + 2, c.height, true);
|
|
84
|
+
v.setInt16(h + 4, c.displaceX, true);
|
|
85
|
+
v.setInt16(h + 6, c.displaceY, true);
|
|
86
|
+
out[h + 8] = c.skipColor;
|
|
87
|
+
v.setUint32(h + 12, c.width * c.height, true);
|
|
88
|
+
v.setUint32(h + 16, c.unknown16, true);
|
|
89
|
+
v.setUint32(h + 24, at, true);
|
|
90
|
+
v.setInt16(h + 36, c.priority, true);
|
|
91
|
+
v.setInt16(h + 38, c.x, true);
|
|
92
|
+
v.setInt16(h + 40, c.y, true);
|
|
93
|
+
out.set(c.pixels, at);
|
|
94
|
+
at += c.pixels.length;
|
|
95
|
+
});
|
|
96
|
+
out.set([0, 3], headers);
|
|
97
|
+
v.setUint32(headers + 2, palette.length, true);
|
|
98
|
+
out.set(palette, paletteAt);
|
|
99
|
+
out.set([0, 4], paletteAt + palette.length);
|
|
100
|
+
v.setUint32(paletteAt + palette.length + 2, pixelBytes, true);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** The palette a pic installs, as the engine sees it. */
|
|
104
|
+
export function hunkToPalette(p) {
|
|
105
|
+
const pal = { rgb: new Uint8Array(768), used: new Uint8Array(256) };
|
|
106
|
+
p.rgb.forEach((c, i) => {
|
|
107
|
+
const idx = p.first + i;
|
|
108
|
+
if (idx > 255)
|
|
109
|
+
return;
|
|
110
|
+
pal.used[idx] = p.used[i];
|
|
111
|
+
pal.rgb.set(c, idx * 3);
|
|
112
|
+
});
|
|
113
|
+
return pal;
|
|
114
|
+
}
|