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,349 @@
|
|
|
1
|
+
import { AudioIndex, speechKey, type AudioClip, type AudioOutput } from "../../audio/index.ts";
|
|
2
|
+
import { SongPlayer, SoundDevice, parseSound, type MidiOutput, type Song } from "../../audio/midi.ts";
|
|
3
|
+
import { ResourceType } from "../../resource/types.ts";
|
|
4
|
+
import { findMessage, parseMessages, type MessageFile } from "../../text/message.ts";
|
|
5
|
+
import type { KernelFn, Vm } from "../vm.ts";
|
|
6
|
+
import { toSigned, type Value } from "../value.ts";
|
|
7
|
+
|
|
8
|
+
const NOT_PLAYING = 0xffff;
|
|
9
|
+
const MAX_VOLUME = 127;
|
|
10
|
+
|
|
11
|
+
interface Channel {
|
|
12
|
+
clip: AudioClip;
|
|
13
|
+
/** VM clock (ms) when playback started; undefined while waiting to start. */
|
|
14
|
+
start: number | undefined;
|
|
15
|
+
pausedAt: number | undefined;
|
|
16
|
+
ticks: number;
|
|
17
|
+
loop: boolean;
|
|
18
|
+
volume: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Digital audio state. Durations are known synchronously from the index, so the scripts'
|
|
23
|
+
* position polling works off the VM clock; the host's AudioOutput does the actual playback
|
|
24
|
+
* (fetching and decoding clips asynchronously).
|
|
25
|
+
*/
|
|
26
|
+
export class AudioState {
|
|
27
|
+
readonly index: AudioIndex;
|
|
28
|
+
readonly channels = new Map<string, Channel>();
|
|
29
|
+
output: AudioOutput | undefined;
|
|
30
|
+
/** MIDI music goes here (a SoundFont synth or the AdLib driver). */
|
|
31
|
+
midi: MidiOutput | undefined;
|
|
32
|
+
/** Which track of each sound resource to play: General MIDI or AdLib. */
|
|
33
|
+
device: number = SoundDevice.GeneralMidi;
|
|
34
|
+
readonly songs = new Map<Value, SongPlayer>();
|
|
35
|
+
private readonly songNumbers = new Map<Value, number>();
|
|
36
|
+
private readonly songCache = new Map<string, Song | null>();
|
|
37
|
+
masterVolume = 15;
|
|
38
|
+
/**
|
|
39
|
+
* Ticks to report when a speech clip doesn't exist (new dialogue in a mod). The game times
|
|
40
|
+
* a spoken line by its clip, so a missing clip would flash the text for a second; hosts
|
|
41
|
+
* that load mods set this (see `readingTime`). Unset, missing clips report 0 like SSCI.
|
|
42
|
+
*/
|
|
43
|
+
missingSpeechTicks?: (module: number, noun: number, verb: number, cond: number, seq: number) => number;
|
|
44
|
+
|
|
45
|
+
constructor(private readonly vm: Vm) {
|
|
46
|
+
this.index = AudioIndex.build(vm.resources);
|
|
47
|
+
vm.frameHooks.push(() => {
|
|
48
|
+
const now = this.ticks();
|
|
49
|
+
for (const player of this.songs.values()) player.update(now);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ticks(): number {
|
|
54
|
+
return Math.floor((this.vm.clock() * 60) / 1000);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
song(number: number, device = this.device): Song | undefined {
|
|
58
|
+
const key = `${number}:${device}`;
|
|
59
|
+
if (!this.songCache.has(key)) {
|
|
60
|
+
const id = { type: ResourceType.Sound, number };
|
|
61
|
+
this.songCache.set(key, this.vm.resources.has(id) ? (parseSound(this.vm.resources.loadSync(id).data, device) ?? null) : null);
|
|
62
|
+
}
|
|
63
|
+
return this.songCache.get(key) ?? undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
startSong(obj: Value, number: number, loop: boolean, volume: number, position = 0): void {
|
|
67
|
+
const song = this.song(number);
|
|
68
|
+
if (!song) return;
|
|
69
|
+
const player = new SongPlayer(song, this.midi, loop, volume, this.ticks());
|
|
70
|
+
if (position) player.seek(position % Math.max(song.length, 1), this.ticks());
|
|
71
|
+
this.songs.set(obj, player);
|
|
72
|
+
this.songNumbers.set(obj, number);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
playingSongs(): { object: Value; number: number; player: SongPlayer }[] {
|
|
76
|
+
return [...this.songs].map(([object, player]) => ({ object, number: this.songNumbers.get(object)!, player }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Silences all music, effects and speech (restart / restore). */
|
|
80
|
+
stopEverything(): void {
|
|
81
|
+
for (const obj of [...this.songs.keys()]) this.stopSong(obj);
|
|
82
|
+
for (const key of [...this.channels.keys()]) this.stop(key);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
stopSong(obj: Value): void {
|
|
86
|
+
this.songs.get(obj)?.stop();
|
|
87
|
+
this.songs.delete(obj);
|
|
88
|
+
this.songNumbers.delete(obj);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Switches music to another device (track) and output, carrying every playing song over
|
|
93
|
+
* at its current position.
|
|
94
|
+
*/
|
|
95
|
+
setMusicDevice(device: number, output: MidiOutput | undefined): void {
|
|
96
|
+
const now = this.ticks();
|
|
97
|
+
const playing = [...this.songs].map(([obj, player]) => ({ obj, player, pos: player.position(now) }));
|
|
98
|
+
for (const { player } of playing) player.stop();
|
|
99
|
+
this.device = device;
|
|
100
|
+
this.midi = output;
|
|
101
|
+
output?.setMasterVolume?.(this.masterVolume);
|
|
102
|
+
for (const { obj, player, pos } of playing) {
|
|
103
|
+
const song = this.song(this.songNumbers.get(obj)!);
|
|
104
|
+
if (!song) {
|
|
105
|
+
this.songs.delete(obj);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const next = new SongPlayer(song, output, player.loop, player.volume, now);
|
|
109
|
+
next.seek(song.length ? pos % Math.max(song.length, 1) : 0, now);
|
|
110
|
+
this.songs.set(obj, next);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
play(key: string, clip: AudioClip, loop: boolean, volume: number, autoStart = true): number {
|
|
115
|
+
this.stop(key);
|
|
116
|
+
const ticks = AudioIndex.ticks(clip);
|
|
117
|
+
this.channels.set(key, { clip, start: undefined, pausedAt: undefined, ticks, loop, volume });
|
|
118
|
+
if (autoStart) this.start(key);
|
|
119
|
+
return ticks;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
start(key: string): void {
|
|
123
|
+
const ch = this.channels.get(key);
|
|
124
|
+
if (!ch || ch.start !== undefined) return;
|
|
125
|
+
ch.start = this.vm.clock();
|
|
126
|
+
this.output?.play(key, ch.clip, { loop: ch.loop, volume: ch.volume / MAX_VOLUME });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
stop(key: string): void {
|
|
130
|
+
if (!this.channels.delete(key)) return;
|
|
131
|
+
this.output?.stop(key);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
stopAll(prefix = ""): void {
|
|
135
|
+
for (const key of [...this.channels.keys()]) if (key.startsWith(prefix)) this.stop(key);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Elapsed ticks, or undefined once the clip has finished (non-looping). */
|
|
139
|
+
position(key: string): number | undefined {
|
|
140
|
+
const ch = this.channels.get(key);
|
|
141
|
+
if (!ch) return undefined;
|
|
142
|
+
if (ch.start === undefined) return 0;
|
|
143
|
+
const now = ch.pausedAt ?? this.vm.clock();
|
|
144
|
+
const elapsed = Math.floor(((now - ch.start) * 60) / 1000);
|
|
145
|
+
if (!ch.loop && elapsed >= ch.ticks) {
|
|
146
|
+
this.stop(key);
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
return ch.loop ? elapsed % ch.ticks : elapsed;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
pause(key: string, paused: boolean): void {
|
|
153
|
+
const ch = this.channels.get(key);
|
|
154
|
+
if (!ch || ch.start === undefined) return;
|
|
155
|
+
if (paused && ch.pausedAt === undefined) {
|
|
156
|
+
ch.pausedAt = this.vm.clock();
|
|
157
|
+
this.output?.pause(key);
|
|
158
|
+
} else if (!paused && ch.pausedAt !== undefined) {
|
|
159
|
+
ch.start += this.vm.clock() - ch.pausedAt;
|
|
160
|
+
ch.pausedAt = undefined;
|
|
161
|
+
this.output?.resume(key);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const stateOf = new WeakMap<Vm, AudioState>();
|
|
167
|
+
export const audio = (vm: Vm): AudioState => {
|
|
168
|
+
let a = stateOf.get(vm);
|
|
169
|
+
if (!a) {
|
|
170
|
+
const created = (a = new AudioState(vm));
|
|
171
|
+
stateOf.set(vm, created);
|
|
172
|
+
vm.hostRoots.push(() => created.songs.keys());
|
|
173
|
+
}
|
|
174
|
+
return a;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* DoAudio arguments name a clip either as an audio36 (speech) tuple
|
|
179
|
+
* `module, noun, verb, cond, seq, [loop], [volume]` or as a plain audio number
|
|
180
|
+
* `number, [loop], [volume]`.
|
|
181
|
+
*/
|
|
182
|
+
function resolveArgs(a: AudioState, args: Value[]): { key: string; clip?: AudioClip; loop: boolean; volume: number } | undefined {
|
|
183
|
+
if (args.length >= 5) {
|
|
184
|
+
const [module = 0, noun = 0, verb = 0, cond = 0, seq = 0, loop, volume] = args;
|
|
185
|
+
return {
|
|
186
|
+
key: `aud:${speechKey(module, noun, verb, cond, seq)}`,
|
|
187
|
+
clip: a.index.speech.get(speechKey(module, noun, verb, cond, seq)),
|
|
188
|
+
loop: loop !== undefined && toSigned(loop) === -1,
|
|
189
|
+
volume: volume !== undefined && toSigned(volume) >= 0 ? Math.min(toSigned(volume), MAX_VOLUME) : MAX_VOLUME,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (args.length >= 1) {
|
|
193
|
+
const [number = 0, loop, volume] = args;
|
|
194
|
+
return {
|
|
195
|
+
key: `sfx:${number}`,
|
|
196
|
+
clip: a.index.effects.get(number),
|
|
197
|
+
loop: loop !== undefined && toSigned(loop) === -1,
|
|
198
|
+
volume: volume !== undefined && toSigned(volume) >= 0 ? Math.min(toSigned(volume), MAX_VOLUME) : MAX_VOLUME,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const soundKey = (obj: Value) => `snd:${obj.toString(16)}`;
|
|
205
|
+
const prop = (vm: Vm, obj: Value, name: string) => toSigned(vm.getProp(obj, name) ?? 0);
|
|
206
|
+
|
|
207
|
+
export const audioKernels: Record<string, KernelFn> = {
|
|
208
|
+
DoAudio: (vm, [subop = 0, ...args]) => {
|
|
209
|
+
const a = audio(vm);
|
|
210
|
+
const target = resolveArgs(a, args);
|
|
211
|
+
switch (subop) {
|
|
212
|
+
case 0: return 0; // init
|
|
213
|
+
case 1: // wait-for-play: prepare without starting, return length
|
|
214
|
+
case 2: { // play
|
|
215
|
+
if (!target) return a.channels.size;
|
|
216
|
+
if (!target.clip) {
|
|
217
|
+
const [module = 0, noun = 0, verb = 0, cond = 0, seq = 0] = args;
|
|
218
|
+
return args.length >= 5 && a.missingSpeechTicks ? a.missingSpeechTicks(module, noun, verb, cond, seq) & 0xffff : 0;
|
|
219
|
+
}
|
|
220
|
+
return a.play(target.key, target.clip, target.loop, target.volume, subop === 2);
|
|
221
|
+
}
|
|
222
|
+
case 3: // stop
|
|
223
|
+
if (target) a.stop(target.key);
|
|
224
|
+
else a.stopAll("aud:"), a.stopAll("sfx:");
|
|
225
|
+
return 0;
|
|
226
|
+
case 4: case 5: // pause / resume
|
|
227
|
+
for (const key of target ? [target.key] : [...a.channels.keys()]) a.pause(key, subop === 4);
|
|
228
|
+
return 0;
|
|
229
|
+
case 6: { // position in ticks, -1 when not playing
|
|
230
|
+
const key = target?.key ?? [...a.channels.keys()].find((k) => k.startsWith("aud:"));
|
|
231
|
+
const pos = key === undefined ? undefined : a.position(key);
|
|
232
|
+
return pos === undefined ? NOT_PLAYING : pos & 0xffff;
|
|
233
|
+
}
|
|
234
|
+
case 8: return 0; // volume
|
|
235
|
+
case 9: return 1; // capability: audio present
|
|
236
|
+
case 10: return 16; // bit depth
|
|
237
|
+
case 12: return 1; // mixing
|
|
238
|
+
case 13: return 2; // channels
|
|
239
|
+
default: return 0;
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
// Sound objects. Numbers with a digital effect in RESOURCE.SFX play as samples;
|
|
244
|
+
// everything else is MIDI, sequenced here and sent to `midi` (a SoundFont synth).
|
|
245
|
+
DoSound: (vm, [subop = 0, obj = 0, arg = 0, , , stopAfter = 0]) => {
|
|
246
|
+
const a = audio(vm);
|
|
247
|
+
switch (subop) {
|
|
248
|
+
case 0: // master volume (get, or set to arg)
|
|
249
|
+
if (obj !== 0 || arg) {
|
|
250
|
+
a.masterVolume = obj & 0xf;
|
|
251
|
+
a.midi?.setMasterVolume?.(a.masterVolume);
|
|
252
|
+
}
|
|
253
|
+
return a.masterVolume;
|
|
254
|
+
case 3: return 32; // polyphony
|
|
255
|
+
case 4: return 1; // digital audio available
|
|
256
|
+
case 6: // init
|
|
257
|
+
vm.setProp(obj, "nodePtr", obj);
|
|
258
|
+
return 0;
|
|
259
|
+
case 7: // dispose
|
|
260
|
+
a.stop(soundKey(obj));
|
|
261
|
+
a.stopSong(obj);
|
|
262
|
+
vm.setProp(obj, "nodePtr", 0);
|
|
263
|
+
return 0;
|
|
264
|
+
case 8: { // play: a digital effect if RESOURCE.SFX has this number, else MIDI
|
|
265
|
+
const number = prop(vm, obj, "number");
|
|
266
|
+
const loop = prop(vm, obj, "loop") === -1;
|
|
267
|
+
const volume = vm.getProp(obj, "vol") === undefined ? MAX_VOLUME : prop(vm, obj, "vol");
|
|
268
|
+
a.stop(soundKey(obj));
|
|
269
|
+
a.stopSong(obj);
|
|
270
|
+
vm.setProp(obj, "signal", 0);
|
|
271
|
+
// SCI2 Sounds ask for updates (Sound::check -> updateCues, which reports the end and
|
|
272
|
+
// cues the client) only while `handle` is set; playing sets it, stopping clears it.
|
|
273
|
+
vm.setProp(obj, "handle", obj);
|
|
274
|
+
const clip = a.index.effects.get(number);
|
|
275
|
+
if (clip) a.play(soundKey(obj), clip, loop, volume);
|
|
276
|
+
else a.startSong(obj, number, loop, volume);
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
case 9: // stop
|
|
280
|
+
a.stop(soundKey(obj));
|
|
281
|
+
a.stopSong(obj);
|
|
282
|
+
vm.setProp(obj, "signal", NOT_PLAYING);
|
|
283
|
+
vm.setProp(obj, "handle", 0);
|
|
284
|
+
return 0;
|
|
285
|
+
case 10: { // pause (obj or 0 = all, arg = pause?)
|
|
286
|
+
for (const key of obj ? [soundKey(obj)] : [...a.channels.keys()].filter((k) => k.startsWith("snd:"))) a.pause(key, arg !== 0);
|
|
287
|
+
for (const player of obj ? [a.songs.get(obj)] : [...a.songs.values()]) {
|
|
288
|
+
if (arg !== 0) player?.pause(a.ticks());
|
|
289
|
+
else player?.resume(a.ticks());
|
|
290
|
+
}
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
case 11: { // fade(obj, targetVolume, ticks, step, stopAfter): fading out → stop
|
|
294
|
+
if (arg === 0 || stopAfter) {
|
|
295
|
+
a.stop(soundKey(obj));
|
|
296
|
+
a.stopSong(obj);
|
|
297
|
+
vm.setProp(obj, "signal", NOT_PLAYING);
|
|
298
|
+
} else a.songs.get(obj)?.setVolume(arg);
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
case 14: // set volume
|
|
302
|
+
a.output?.setVolume(soundKey(obj), arg / MAX_VOLUME);
|
|
303
|
+
a.songs.get(obj)?.setVolume(arg);
|
|
304
|
+
vm.setProp(obj, "vol", arg);
|
|
305
|
+
return 0;
|
|
306
|
+
case 16: { // set loop
|
|
307
|
+
vm.setProp(obj, "loop", arg);
|
|
308
|
+
const player = a.songs.get(obj);
|
|
309
|
+
if (player) player.loop = toSigned(arg) === -1;
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
case 17: { // update cues: report song signals/cues and finished sounds to the script
|
|
313
|
+
if (a.channels.has(soundKey(obj)) && a.position(soundKey(obj)) === undefined) vm.setProp(obj, "signal", NOT_PLAYING);
|
|
314
|
+
const player = a.songs.get(obj);
|
|
315
|
+
if (player) {
|
|
316
|
+
player.update(a.ticks());
|
|
317
|
+
if (player.signal) {
|
|
318
|
+
vm.setProp(obj, "signal", player.signal);
|
|
319
|
+
vm.setProp(obj, "dataInc", player.dataInc);
|
|
320
|
+
player.signal = 0;
|
|
321
|
+
}
|
|
322
|
+
if (player.finished) a.stopSong(obj);
|
|
323
|
+
}
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
default:
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
DoSync: () => 0,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* A `missingSpeechTicks` that gives a line without a recording as long as it takes to read:
|
|
336
|
+
* ~15 characters a second, at least two seconds. Reads the text from the message resources.
|
|
337
|
+
*/
|
|
338
|
+
export function readingTime(vm: Vm): (module: number, noun: number, verb: number, cond: number, seq: number) => number {
|
|
339
|
+
const files = new Map<number, MessageFile | undefined>();
|
|
340
|
+
return (module, noun, verb, cond, seq) => {
|
|
341
|
+
if (!files.has(module)) {
|
|
342
|
+
const id = { type: ResourceType.Message, number: module };
|
|
343
|
+
files.set(module, vm.resources.has(id) ? parseMessages(vm.resources.loadSync(id).data) : undefined);
|
|
344
|
+
}
|
|
345
|
+
const file = files.get(module);
|
|
346
|
+
const text = file ? findMessage(file, { noun, verb, cond, seq })?.text ?? "" : "";
|
|
347
|
+
return Math.max(120, text.length * 4);
|
|
348
|
+
};
|
|
349
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ResourceType } from "../../resource/types.js";
|
|
2
|
+
import { NULL, bool, fromInt, toSigned } from "../value.js";
|
|
3
|
+
/** Script, object and resource management. */
|
|
4
|
+
export const coreKernels = {
|
|
5
|
+
// Resource loading is eager in this engine, so these only need to answer sensibly.
|
|
6
|
+
Load: (_vm, [, number = 0]) => number,
|
|
7
|
+
Unload: () => 0,
|
|
8
|
+
Lock: () => 0,
|
|
9
|
+
Purge: () => 0,
|
|
10
|
+
ResCheck: (vm, [type = 0, number = 0]) => bool(vm.resources.has({ type: (type & 0x7f), number })),
|
|
11
|
+
ScriptID: (vm, [script = 0, index = 0]) => {
|
|
12
|
+
if (!vm.resources.has({ type: ResourceType.Script, number: script }))
|
|
13
|
+
return NULL;
|
|
14
|
+
return vm.exportAddress(script, index);
|
|
15
|
+
},
|
|
16
|
+
// Unloads the script (see Vm.disposeScript) and returns its optional second argument:
|
|
17
|
+
// scripts use it to hand a value back from code that's about to be unloaded (SRDialog
|
|
18
|
+
// returns the chosen save id this way).
|
|
19
|
+
DisposeScript: (vm, args) => {
|
|
20
|
+
vm.disposeScript(args[0] & 0xffff);
|
|
21
|
+
return args.length >= 2 ? args[1] : undefined;
|
|
22
|
+
},
|
|
23
|
+
Clone: (vm, [obj = 0]) => vm.memory.clone(vm.object(obj)).address,
|
|
24
|
+
DisposeClone: (vm, [obj = 0]) => {
|
|
25
|
+
const o = vm.memory.object(obj);
|
|
26
|
+
if (o)
|
|
27
|
+
vm.memory.disposeClone(o);
|
|
28
|
+
},
|
|
29
|
+
RespondsTo: (vm, [obj = 0, selector = 0]) => {
|
|
30
|
+
const o = vm.memory.object(obj);
|
|
31
|
+
return bool(!!o && vm.respondsTo(o, selector));
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
let seed = 0x1234;
|
|
35
|
+
const bootDate = Date.now();
|
|
36
|
+
/** Maths, time and the host platform. */
|
|
37
|
+
export const mathKernels = {
|
|
38
|
+
Random: (_vm, args) => {
|
|
39
|
+
if (args.length === 1) {
|
|
40
|
+
seed = args[0]; // seed the generator
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
|
44
|
+
if (args.length < 2)
|
|
45
|
+
return (seed >> 8) & 0xffff;
|
|
46
|
+
const lo = toSigned(args[0]), hi = toSigned(args[1]);
|
|
47
|
+
return fromInt(lo + ((seed >> 8) % (hi - lo + 1)));
|
|
48
|
+
},
|
|
49
|
+
Abs: (_vm, [n = 0]) => fromInt(Math.abs(toSigned(n))),
|
|
50
|
+
Sqrt: (_vm, [n = 0]) => Math.floor(Math.sqrt(Math.abs(toSigned(n)))),
|
|
51
|
+
GetAngle: (_vm, [x1 = 0, y1 = 0, x2 = 0, y2 = 0]) => sciAngle(toSigned(x2) - toSigned(x1), toSigned(y2) - toSigned(y1)),
|
|
52
|
+
GetDistance: (_vm, [x1 = 0, y1 = 0, x2 = 0, y2 = 0, perspective = 0]) => {
|
|
53
|
+
const dx = toSigned(x2) - toSigned(x1);
|
|
54
|
+
let dy = toSigned(y2) - toSigned(y1);
|
|
55
|
+
if (perspective)
|
|
56
|
+
dy = Math.trunc((dy * 3) / 2);
|
|
57
|
+
return Math.round(Math.sqrt(dx * dx + dy * dy)) & 0xffff;
|
|
58
|
+
},
|
|
59
|
+
ATan: (_vm, [x1 = 0, y1 = 0, x2 = 0, y2 = 0]) => sciAngle(toSigned(x2) - toSigned(x1), toSigned(y2) - toSigned(y1)),
|
|
60
|
+
SinMult: (_vm, [angle = 0, n = 0]) => fromInt(Math.round(Math.sin(rad(angle)) * toSigned(n))),
|
|
61
|
+
CosMult: (_vm, [angle = 0, n = 0]) => fromInt(Math.round(Math.cos(rad(angle)) * toSigned(n))),
|
|
62
|
+
SinDiv: (_vm, [angle = 0, n = 0]) => {
|
|
63
|
+
const s = Math.sin(rad(angle));
|
|
64
|
+
return s === 0 ? 0 : fromInt(Math.round(toSigned(n) / s));
|
|
65
|
+
},
|
|
66
|
+
CosDiv: (_vm, [angle = 0, n = 0]) => {
|
|
67
|
+
const c = Math.cos(rad(angle));
|
|
68
|
+
return c === 0 ? 0 : fromInt(Math.round(toSigned(n) / c));
|
|
69
|
+
},
|
|
70
|
+
GetTime: (vm, [mode = 0]) => {
|
|
71
|
+
// All modes follow vm.clock so headless runs see time pass consistently.
|
|
72
|
+
const now = new Date(bootDate + vm.clock());
|
|
73
|
+
switch (mode) {
|
|
74
|
+
case 0: return Math.floor((vm.clock() * 60) / 1000) & 0xffff; // ticks (1/60 s)
|
|
75
|
+
case 1: return ((now.getHours() % 12) << 12) | (now.getMinutes() << 6) | now.getSeconds();
|
|
76
|
+
case 2: return (now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1);
|
|
77
|
+
default: return ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate();
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
Platform: (_vm, [op = 0]) => (op === 0 ? 1 /* DOS */ : op === 2 ? 2 /* 256 colours */ : 0),
|
|
81
|
+
};
|
|
82
|
+
const rad = (deg) => (toSigned(deg) * Math.PI) / 180;
|
|
83
|
+
/** SCI angles: 0 = north (up), clockwise, 0..359. */
|
|
84
|
+
function sciAngle(dx, dy) {
|
|
85
|
+
const deg = (Math.atan2(dx, -dy) * 180) / Math.PI;
|
|
86
|
+
return Math.round((deg + 360) % 360);
|
|
87
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ResourceType } from "../../resource/types.ts";
|
|
2
|
+
import type { KernelFn } from "../vm.ts";
|
|
3
|
+
import { NULL, bool, fromInt, toSigned } from "../value.ts";
|
|
4
|
+
|
|
5
|
+
/** Script, object and resource management. */
|
|
6
|
+
export const coreKernels: Record<string, KernelFn> = {
|
|
7
|
+
// Resource loading is eager in this engine, so these only need to answer sensibly.
|
|
8
|
+
Load: (_vm, [, number = 0]) => number,
|
|
9
|
+
Unload: () => 0,
|
|
10
|
+
Lock: () => 0,
|
|
11
|
+
Purge: () => 0,
|
|
12
|
+
ResCheck: (vm, [type = 0, number = 0]) => bool(vm.resources.has({ type: (type & 0x7f) as ResourceType, number })),
|
|
13
|
+
|
|
14
|
+
ScriptID: (vm, [script = 0, index = 0]) => {
|
|
15
|
+
if (!vm.resources.has({ type: ResourceType.Script, number: script })) return NULL;
|
|
16
|
+
return vm.exportAddress(script, index);
|
|
17
|
+
},
|
|
18
|
+
// Unloads the script (see Vm.disposeScript) and returns its optional second argument:
|
|
19
|
+
// scripts use it to hand a value back from code that's about to be unloaded (SRDialog
|
|
20
|
+
// returns the chosen save id this way).
|
|
21
|
+
DisposeScript: (vm, args) => {
|
|
22
|
+
vm.disposeScript(args[0]! & 0xffff);
|
|
23
|
+
return args.length >= 2 ? args[1] : undefined;
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
Clone: (vm, [obj = 0]) => vm.memory.clone(vm.object(obj)).address,
|
|
27
|
+
DisposeClone: (vm, [obj = 0]) => {
|
|
28
|
+
const o = vm.memory.object(obj);
|
|
29
|
+
if (o) vm.memory.disposeClone(o);
|
|
30
|
+
},
|
|
31
|
+
RespondsTo: (vm, [obj = 0, selector = 0]) => {
|
|
32
|
+
const o = vm.memory.object(obj);
|
|
33
|
+
return bool(!!o && vm.respondsTo(o, selector));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let seed = 0x1234;
|
|
38
|
+
const bootDate = Date.now();
|
|
39
|
+
|
|
40
|
+
/** Maths, time and the host platform. */
|
|
41
|
+
export const mathKernels: Record<string, KernelFn> = {
|
|
42
|
+
Random: (_vm, args) => {
|
|
43
|
+
if (args.length === 1) {
|
|
44
|
+
seed = args[0]!; // seed the generator
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
|
48
|
+
if (args.length < 2) return (seed >> 8) & 0xffff;
|
|
49
|
+
const lo = toSigned(args[0]!), hi = toSigned(args[1]!);
|
|
50
|
+
return fromInt(lo + ((seed >> 8) % (hi - lo + 1)));
|
|
51
|
+
},
|
|
52
|
+
Abs: (_vm, [n = 0]) => fromInt(Math.abs(toSigned(n))),
|
|
53
|
+
Sqrt: (_vm, [n = 0]) => Math.floor(Math.sqrt(Math.abs(toSigned(n)))),
|
|
54
|
+
GetAngle: (_vm, [x1 = 0, y1 = 0, x2 = 0, y2 = 0]) => sciAngle(toSigned(x2) - toSigned(x1), toSigned(y2) - toSigned(y1)),
|
|
55
|
+
GetDistance: (_vm, [x1 = 0, y1 = 0, x2 = 0, y2 = 0, perspective = 0]) => {
|
|
56
|
+
const dx = toSigned(x2) - toSigned(x1);
|
|
57
|
+
let dy = toSigned(y2) - toSigned(y1);
|
|
58
|
+
if (perspective) dy = Math.trunc((dy * 3) / 2);
|
|
59
|
+
return Math.round(Math.sqrt(dx * dx + dy * dy)) & 0xffff;
|
|
60
|
+
},
|
|
61
|
+
ATan: (_vm, [x1 = 0, y1 = 0, x2 = 0, y2 = 0]) => sciAngle(toSigned(x2) - toSigned(x1), toSigned(y2) - toSigned(y1)),
|
|
62
|
+
SinMult: (_vm, [angle = 0, n = 0]) => fromInt(Math.round(Math.sin(rad(angle)) * toSigned(n))),
|
|
63
|
+
CosMult: (_vm, [angle = 0, n = 0]) => fromInt(Math.round(Math.cos(rad(angle)) * toSigned(n))),
|
|
64
|
+
SinDiv: (_vm, [angle = 0, n = 0]) => {
|
|
65
|
+
const s = Math.sin(rad(angle));
|
|
66
|
+
return s === 0 ? 0 : fromInt(Math.round(toSigned(n) / s));
|
|
67
|
+
},
|
|
68
|
+
CosDiv: (_vm, [angle = 0, n = 0]) => {
|
|
69
|
+
const c = Math.cos(rad(angle));
|
|
70
|
+
return c === 0 ? 0 : fromInt(Math.round(toSigned(n) / c));
|
|
71
|
+
},
|
|
72
|
+
GetTime: (vm, [mode = 0]) => {
|
|
73
|
+
// All modes follow vm.clock so headless runs see time pass consistently.
|
|
74
|
+
const now = new Date(bootDate + vm.clock());
|
|
75
|
+
switch (mode) {
|
|
76
|
+
case 0: return Math.floor((vm.clock() * 60) / 1000) & 0xffff; // ticks (1/60 s)
|
|
77
|
+
case 1: return ((now.getHours() % 12) << 12) | (now.getMinutes() << 6) | now.getSeconds();
|
|
78
|
+
case 2: return (now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1);
|
|
79
|
+
default: return ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate();
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
Platform: (_vm, [op = 0]) => (op === 0 ? 1 /* DOS */ : op === 2 ? 2 /* 256 colours */ : 0),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const rad = (deg: number) => (toSigned(deg) * Math.PI) / 180;
|
|
86
|
+
|
|
87
|
+
/** SCI angles: 0 = north (up), clockwise, 0..359. */
|
|
88
|
+
function sciAngle(dx: number, dy: number): number {
|
|
89
|
+
const deg = (Math.atan2(dx, -dy) * 180) / Math.PI;
|
|
90
|
+
return Math.round((deg + 360) % 360);
|
|
91
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Cel } from "../../gfx/cel.ts";
|
|
2
|
+
import { type Palette } from "../../gfx/palette.ts";
|
|
3
|
+
import { type Pic } from "../../gfx/pic.ts";
|
|
4
|
+
import { type View } from "../../gfx/view.ts";
|
|
5
|
+
import { type KernelFn, type Vm } from "../vm.ts";
|
|
6
|
+
import { type Value } from "../value.ts";
|
|
7
|
+
export declare const SCREEN_WIDTH = 320;
|
|
8
|
+
export declare const SCREEN_HEIGHT = 200;
|
|
9
|
+
/**
|
|
10
|
+
* SCI32 graphics model: the scripts create Plane objects (layers with a rect, a background
|
|
11
|
+
* pic or colour, and a priority) and register View-like objects as screen items in a plane.
|
|
12
|
+
* Each FrameOut composes planes by priority, then items within a plane by priority.
|
|
13
|
+
* The kernel only remembers *which* objects are registered; their current state (x, y,
|
|
14
|
+
* view, loop, cel...) is read from the objects' properties at compose time.
|
|
15
|
+
*/
|
|
16
|
+
export declare class Graphics {
|
|
17
|
+
private readonly vm;
|
|
18
|
+
readonly planes: Set<number>;
|
|
19
|
+
readonly items: Set<number>;
|
|
20
|
+
frames: number;
|
|
21
|
+
private readonly views;
|
|
22
|
+
private readonly pics;
|
|
23
|
+
readonly basePalette: Palette;
|
|
24
|
+
/** Called on every FrameOut with the composed frame. */
|
|
25
|
+
onFrame?: (frame: Frame) => void;
|
|
26
|
+
/** Drops parsed views and pictures (their resources were replaced). */
|
|
27
|
+
forgetResources(): void;
|
|
28
|
+
constructor(vm: Vm);
|
|
29
|
+
/**
|
|
30
|
+
* View numbers are unsigned (SCI32's system views live at 64xxx), but scripts store them
|
|
31
|
+
* in signed properties; only 0xFFFF (-1) means "no view".
|
|
32
|
+
*/
|
|
33
|
+
view(n: number): View | undefined;
|
|
34
|
+
pic(n: number): Pic | undefined;
|
|
35
|
+
cel(viewNo: number, loopNo: number, celNo: number): {
|
|
36
|
+
cel: Cel;
|
|
37
|
+
mirror: boolean;
|
|
38
|
+
} | undefined;
|
|
39
|
+
/** Reads a numeric property (signed), 0 if absent. */
|
|
40
|
+
prop(obj: Value, name: string): number;
|
|
41
|
+
/** Palette built from every palette submitted so far (base, scripts, pics, views). */
|
|
42
|
+
private source;
|
|
43
|
+
/** Picture each plane last submitted, and view each item last submitted. */
|
|
44
|
+
private readonly planePictures;
|
|
45
|
+
private readonly itemViews;
|
|
46
|
+
/** The palette after PalVary/PalCycle (before fades), as of the last frame. */
|
|
47
|
+
currentPalette: Palette;
|
|
48
|
+
/** Active show-style transitions, by plane. */
|
|
49
|
+
readonly transitions: Map<number, Transition>;
|
|
50
|
+
/** The mouse cursor: a view cel drawn on top at the mouse position (its origin = hotspot). */
|
|
51
|
+
cursor: {
|
|
52
|
+
view: number;
|
|
53
|
+
loop: number;
|
|
54
|
+
cel: number;
|
|
55
|
+
visible: boolean;
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
};
|
|
59
|
+
/** Palette range affected by fade show styles (SetPalStyleRange). */
|
|
60
|
+
styleRange: {
|
|
61
|
+
from: number;
|
|
62
|
+
to: number;
|
|
63
|
+
};
|
|
64
|
+
lastFrame: Frame | undefined;
|
|
65
|
+
get sourcePalette(): Palette | undefined;
|
|
66
|
+
set sourcePalette(pal: Palette | undefined);
|
|
67
|
+
submitPalette(pal: Palette | undefined): void;
|
|
68
|
+
/** Forgets submitted palettes (restart/restore); they're resubmitted as things are drawn. */
|
|
69
|
+
resetPalette(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Where a view item's cel lands, in its plane's coordinates. Scripts scale cels for
|
|
72
|
+
* perspective (the hero shrinks with distance) and to stretch UI backgrounds (dialog boxes)
|
|
73
|
+
* to fit their contents.
|
|
74
|
+
*/
|
|
75
|
+
viewRect(item: Value): {
|
|
76
|
+
cel: Cel;
|
|
77
|
+
mirror: boolean;
|
|
78
|
+
sx: number;
|
|
79
|
+
sy: number;
|
|
80
|
+
x: number;
|
|
81
|
+
y: number;
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
} | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Composes the current frame in three phases, because remap colours (shadows) depend on
|
|
87
|
+
* the final palette: collect what each plane draws (submitting palettes as pictures and
|
|
88
|
+
* views change, like the original), apply palette effects, then blit.
|
|
89
|
+
*/
|
|
90
|
+
compose(): Frame;
|
|
91
|
+
}
|
|
92
|
+
type Clip = {
|
|
93
|
+
left: number;
|
|
94
|
+
top: number;
|
|
95
|
+
right: number;
|
|
96
|
+
bottom: number;
|
|
97
|
+
};
|
|
98
|
+
export interface Frame {
|
|
99
|
+
width: number;
|
|
100
|
+
height: number;
|
|
101
|
+
pixels: Uint8Array;
|
|
102
|
+
palette: Palette;
|
|
103
|
+
/** Set during wipes/dissolves, which mix two differently-coloured images: RGB per pixel. */
|
|
104
|
+
rgb?: Uint8Array;
|
|
105
|
+
}
|
|
106
|
+
/** SCI32 show styles (SetShowStyle): how a plane's new content appears. */
|
|
107
|
+
export declare const ShowStyle: {
|
|
108
|
+
readonly None: 0;
|
|
109
|
+
readonly HShutterOut: 1;
|
|
110
|
+
readonly HShutterIn: 2;
|
|
111
|
+
readonly VShutterOut: 3;
|
|
112
|
+
readonly VShutterIn: 4;
|
|
113
|
+
readonly WipeLeft: 5;
|
|
114
|
+
readonly WipeRight: 6;
|
|
115
|
+
readonly WipeUp: 7;
|
|
116
|
+
readonly WipeDown: 8;
|
|
117
|
+
readonly IrisOut: 9;
|
|
118
|
+
readonly IrisIn: 10;
|
|
119
|
+
readonly DissolveNoMorph: 11;
|
|
120
|
+
readonly Dissolve: 12;
|
|
121
|
+
readonly FadeOut: 13;
|
|
122
|
+
readonly FadeIn: 14;
|
|
123
|
+
};
|
|
124
|
+
interface Transition {
|
|
125
|
+
style: number;
|
|
126
|
+
start: number;
|
|
127
|
+
duration: number;
|
|
128
|
+
rect: Clip;
|
|
129
|
+
/** The screen as it was when the transition started (RGB), for wipes and dissolves. */
|
|
130
|
+
from: Uint8Array | undefined;
|
|
131
|
+
}
|
|
132
|
+
/** Converts an indexed frame to RGB. */
|
|
133
|
+
export declare function frameRgb(f: Frame): Uint8Array;
|
|
134
|
+
/** View cel anchor point, relative to the cel's top-left. */
|
|
135
|
+
export declare function celOrigin(cel: Cel, mirror?: boolean): {
|
|
136
|
+
x: number;
|
|
137
|
+
y: number;
|
|
138
|
+
};
|
|
139
|
+
export declare const graphics: (vm: Vm) => Graphics;
|
|
140
|
+
export declare const graphicsKernels: Record<string, KernelFn>;
|
|
141
|
+
export {};
|