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,89 @@
|
|
|
1
|
+
import { type PaletteEffectsState } from "./kernels/palette.ts";
|
|
2
|
+
import type { Palette } from "../gfx/palette.ts";
|
|
3
|
+
import { type ArrayType } from "./memory.ts";
|
|
4
|
+
import type { Vm } from "./vm.ts";
|
|
5
|
+
import { type Value } from "./value.ts";
|
|
6
|
+
/**
|
|
7
|
+
* A saved game: the VM's memory, not the original interpreter's. Every reference in the game
|
|
8
|
+
* is a (segment, offset) pair, so restoring means recreating each segment at its original id:
|
|
9
|
+
* scripts by number (their code comes from the resources), clones by the script object they
|
|
10
|
+
* were cloned from, and arrays, lists, nodes and bitmaps by value. The call stack isn't
|
|
11
|
+
* saved: like Sierra's interpreter, restoring restarts execution with `theGame replay`.
|
|
12
|
+
*/
|
|
13
|
+
export interface VmSnapshot {
|
|
14
|
+
format: 1;
|
|
15
|
+
segments: (SegmentSnapshot | null)[];
|
|
16
|
+
freeSegments: number[];
|
|
17
|
+
scripts: {
|
|
18
|
+
number: number;
|
|
19
|
+
segment: number;
|
|
20
|
+
localsSegment: number;
|
|
21
|
+
locals: Value[];
|
|
22
|
+
objects: [offset: number, vars: Value[]][];
|
|
23
|
+
}[];
|
|
24
|
+
stack: Value[];
|
|
25
|
+
graphics: {
|
|
26
|
+
planes: Value[];
|
|
27
|
+
items: Value[];
|
|
28
|
+
palette?: Palette;
|
|
29
|
+
effects: PaletteEffectsState;
|
|
30
|
+
};
|
|
31
|
+
audio: {
|
|
32
|
+
masterVolume: number;
|
|
33
|
+
songs: {
|
|
34
|
+
object: Value;
|
|
35
|
+
number: number;
|
|
36
|
+
loop: boolean;
|
|
37
|
+
volume: number;
|
|
38
|
+
position: number;
|
|
39
|
+
}[];
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
type SegmentSnapshot = {
|
|
43
|
+
kind: "script" | "locals" | "stack";
|
|
44
|
+
} | {
|
|
45
|
+
kind: "clone";
|
|
46
|
+
template: Value;
|
|
47
|
+
vars: Value[];
|
|
48
|
+
} | {
|
|
49
|
+
kind: "array";
|
|
50
|
+
type: ArrayType;
|
|
51
|
+
data: Value[];
|
|
52
|
+
} | {
|
|
53
|
+
kind: "list";
|
|
54
|
+
first: number;
|
|
55
|
+
last: number;
|
|
56
|
+
} | {
|
|
57
|
+
kind: "node";
|
|
58
|
+
value: Value;
|
|
59
|
+
key: Value;
|
|
60
|
+
prev: number;
|
|
61
|
+
next: number;
|
|
62
|
+
list: number;
|
|
63
|
+
} | {
|
|
64
|
+
kind: "bitmap";
|
|
65
|
+
width: number;
|
|
66
|
+
height: number;
|
|
67
|
+
pixels: Uint8Array;
|
|
68
|
+
skipColor: number;
|
|
69
|
+
originX: number;
|
|
70
|
+
originY: number;
|
|
71
|
+
};
|
|
72
|
+
export declare function snapshot(vm: Vm): VmSnapshot;
|
|
73
|
+
/** Replaces the VM's state with `snap`. The caller then restarts execution (`replay`). */
|
|
74
|
+
export declare function restore(vm: Vm, snap: VmSnapshot): void;
|
|
75
|
+
/** Sanity check used by tests: every object's -super- points at a live class object. */
|
|
76
|
+
export declare function checkObjects(vm: Vm): string[];
|
|
77
|
+
/**
|
|
78
|
+
* Saved games as JSON text, for moving them between the browser (IndexedDB keeps them as
|
|
79
|
+
* structured clones) and the Node tools. Typed arrays become `{ $typed, data }`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function savesToJson(list: {
|
|
82
|
+
info: unknown;
|
|
83
|
+
snapshot: VmSnapshot;
|
|
84
|
+
}[]): string;
|
|
85
|
+
export declare function savesFromJson<Info>(text: string): {
|
|
86
|
+
info: Info;
|
|
87
|
+
snapshot: VmSnapshot;
|
|
88
|
+
}[];
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { ObjectSlot } from "../script/script.js";
|
|
2
|
+
import { audio } from "./kernels/audio.js";
|
|
3
|
+
import { graphics } from "./kernels/graphics.js";
|
|
4
|
+
import { paletteEffects } from "./kernels/palette.js";
|
|
5
|
+
import { cloneShape } from "./memory.js";
|
|
6
|
+
import { makeRef, segmentOf } from "./value.js";
|
|
7
|
+
const seg = (v) => (v ? segmentOf(v.address) : -1);
|
|
8
|
+
export function snapshot(vm) {
|
|
9
|
+
const m = vm.memory;
|
|
10
|
+
const segments = m.segments.map((s) => {
|
|
11
|
+
switch (s.kind) {
|
|
12
|
+
case "script":
|
|
13
|
+
case "locals":
|
|
14
|
+
case "stack": return { kind: s.kind };
|
|
15
|
+
case "clone": return { kind: "clone", template: s.object.template.address, vars: s.object.vars.slice() };
|
|
16
|
+
case "array": return { kind: "array", type: s.array.type, data: s.array.data.slice() };
|
|
17
|
+
case "list": return { kind: "list", first: seg(s.list.first), last: seg(s.list.last) };
|
|
18
|
+
case "node": return { kind: "node", value: s.node.value, key: s.node.key, prev: seg(s.node.prev), next: seg(s.node.next), list: seg(s.node.list) };
|
|
19
|
+
case "bitmap": return { kind: "bitmap", ...s.bitmap, pixels: s.bitmap.pixels.slice() };
|
|
20
|
+
default: return null;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const g = graphics(vm);
|
|
24
|
+
const a = audio(vm);
|
|
25
|
+
const now = a.ticks();
|
|
26
|
+
return {
|
|
27
|
+
format: 1,
|
|
28
|
+
segments,
|
|
29
|
+
freeSegments: [...m.free_],
|
|
30
|
+
scripts: vm.loadedScripts.map((s) => ({
|
|
31
|
+
number: s.number,
|
|
32
|
+
segment: s.segment,
|
|
33
|
+
localsSegment: s.localsSegment,
|
|
34
|
+
locals: s.locals.slice(),
|
|
35
|
+
objects: [...s.objects].map(([offset, obj]) => [offset, obj.vars.slice()]),
|
|
36
|
+
})),
|
|
37
|
+
stack: m.stack.slice(),
|
|
38
|
+
graphics: {
|
|
39
|
+
planes: [...g.planes],
|
|
40
|
+
items: [...g.items],
|
|
41
|
+
palette: g.sourcePalette && { rgb: g.sourcePalette.rgb.slice(), used: g.sourcePalette.used.slice() },
|
|
42
|
+
effects: paletteEffects(vm).save(),
|
|
43
|
+
},
|
|
44
|
+
audio: {
|
|
45
|
+
masterVolume: a.masterVolume,
|
|
46
|
+
songs: a.playingSongs().map((p) => ({ object: p.object, number: p.number, loop: p.player.loop, volume: p.player.volume, position: p.player.position(now) })),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Replaces the VM's state with `snap`. The caller then restarts execution (`replay`). */
|
|
51
|
+
export function restore(vm, snap) {
|
|
52
|
+
if (snap.format !== 1)
|
|
53
|
+
throw new Error(`Unsupported save format ${snap.format}`);
|
|
54
|
+
audio(vm).stopEverything();
|
|
55
|
+
vm.resetMemory();
|
|
56
|
+
const m = vm.memory;
|
|
57
|
+
if (m.stackSegment !== snap.segments.findIndex((s) => s?.kind === "stack"))
|
|
58
|
+
throw new Error("Stack segment moved");
|
|
59
|
+
// Reserve the whole table so nothing we create lands on a saved id by accident.
|
|
60
|
+
m.allocateAt(snap.segments.length - 1, { kind: "free" });
|
|
61
|
+
m.setFreeSegments([]);
|
|
62
|
+
for (const s of vm.restoreScripts(snap.scripts)) {
|
|
63
|
+
const saved = snap.scripts.find((x) => x.number === s.number);
|
|
64
|
+
s.locals.splice(0, s.locals.length, ...saved.locals);
|
|
65
|
+
for (const [offset, vars] of saved.objects) {
|
|
66
|
+
const obj = s.objects.get(offset);
|
|
67
|
+
if (obj)
|
|
68
|
+
obj.vars = vars.slice();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const nodes = new Map();
|
|
72
|
+
const lists = new Map();
|
|
73
|
+
snap.segments.forEach((s, id) => {
|
|
74
|
+
if (!s)
|
|
75
|
+
return;
|
|
76
|
+
const address = makeRef(id, 0);
|
|
77
|
+
switch (s.kind) {
|
|
78
|
+
case "clone": {
|
|
79
|
+
const template = m.object(s.template);
|
|
80
|
+
if (!template)
|
|
81
|
+
throw new Error(`Clone template ${s.template.toString(16)} missing`);
|
|
82
|
+
const obj = cloneShape(template.template ?? template, s.vars.slice());
|
|
83
|
+
obj.address = address;
|
|
84
|
+
m.allocateAt(id, { kind: "clone", object: obj });
|
|
85
|
+
m.objects.set(address, obj);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "array":
|
|
89
|
+
m.allocateAt(id, { kind: "array", array: { type: s.type, data: s.data.slice() } });
|
|
90
|
+
break;
|
|
91
|
+
case "bitmap":
|
|
92
|
+
m.allocateAt(id, { kind: "bitmap", bitmap: { width: s.width, height: s.height, pixels: s.pixels.slice(), skipColor: s.skipColor, originX: s.originX, originY: s.originY } });
|
|
93
|
+
break;
|
|
94
|
+
case "list": {
|
|
95
|
+
const list = { address, first: undefined, last: undefined };
|
|
96
|
+
lists.set(id, list);
|
|
97
|
+
m.allocateAt(id, { kind: "list", list });
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case "node": {
|
|
101
|
+
const node = { address, value: s.value, key: s.key, prev: undefined, next: undefined, list: undefined };
|
|
102
|
+
nodes.set(id, node);
|
|
103
|
+
m.allocateAt(id, { kind: "node", node });
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
// Link lists and nodes now that they all exist.
|
|
109
|
+
snap.segments.forEach((s, id) => {
|
|
110
|
+
if (s?.kind === "list")
|
|
111
|
+
Object.assign(lists.get(id), { first: nodes.get(s.first), last: nodes.get(s.last) });
|
|
112
|
+
if (s?.kind === "node")
|
|
113
|
+
Object.assign(nodes.get(id), { prev: nodes.get(s.prev), next: nodes.get(s.next), list: lists.get(s.list) });
|
|
114
|
+
});
|
|
115
|
+
m.setFreeSegments(snap.freeSegments.slice());
|
|
116
|
+
// Derived identity slots (see Vm.fixObjectIdentity): recompute rather than trust old saves.
|
|
117
|
+
for (const obj of m.objects.values())
|
|
118
|
+
vm.fixObjectIdentity(obj);
|
|
119
|
+
m.stack.splice(0, m.stack.length, ...snap.stack);
|
|
120
|
+
const g = graphics(vm);
|
|
121
|
+
g.planes.clear();
|
|
122
|
+
g.items.clear();
|
|
123
|
+
for (const p of snap.graphics.planes)
|
|
124
|
+
g.planes.add(p);
|
|
125
|
+
for (const i of snap.graphics.items)
|
|
126
|
+
g.items.add(i);
|
|
127
|
+
g.resetPalette();
|
|
128
|
+
g.sourcePalette = snap.graphics.palette && { rgb: snap.graphics.palette.rgb.slice(), used: snap.graphics.palette.used.slice() };
|
|
129
|
+
paletteEffects(vm).load(snap.graphics.effects, Math.floor((vm.clock() * 60) / 1000));
|
|
130
|
+
const a = audio(vm);
|
|
131
|
+
a.masterVolume = snap.audio.masterVolume;
|
|
132
|
+
a.midi?.setMasterVolume?.(a.masterVolume);
|
|
133
|
+
for (const song of snap.audio.songs)
|
|
134
|
+
a.startSong(song.object, song.number, song.loop, song.volume, song.position);
|
|
135
|
+
}
|
|
136
|
+
/** Sanity check used by tests: every object's -super- points at a live class object. */
|
|
137
|
+
export function checkObjects(vm) {
|
|
138
|
+
const problems = [];
|
|
139
|
+
for (const obj of vm.memory.objects.values()) {
|
|
140
|
+
const sup = obj.vars[ObjectSlot.Super];
|
|
141
|
+
if (sup && !vm.memory.object(sup))
|
|
142
|
+
problems.push(`${obj.name}: -super- ${sup.toString(16)} is not an object`);
|
|
143
|
+
}
|
|
144
|
+
return problems;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Saved games as JSON text, for moving them between the browser (IndexedDB keeps them as
|
|
148
|
+
* structured clones) and the Node tools. Typed arrays become `{ $typed, data }`.
|
|
149
|
+
*/
|
|
150
|
+
export function savesToJson(list) {
|
|
151
|
+
return JSON.stringify(list, (_key, value) => ArrayBuffer.isView(value) && !(value instanceof DataView)
|
|
152
|
+
? { $typed: value.constructor.name, data: Array.from(value) }
|
|
153
|
+
: value);
|
|
154
|
+
}
|
|
155
|
+
const TYPED = { Uint8Array, Uint16Array, Int16Array, Uint32Array, Int32Array, Float32Array, Float64Array };
|
|
156
|
+
export function savesFromJson(text) {
|
|
157
|
+
return JSON.parse(text, (_key, value) => {
|
|
158
|
+
const t = value;
|
|
159
|
+
return t && typeof t === "object" && t.$typed && TYPED[t.$typed] ? new TYPED[t.$typed](t.data) : value;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { ObjectSlot } from "../script/script.ts";
|
|
2
|
+
import { audio } from "./kernels/audio.ts";
|
|
3
|
+
import { graphics } from "./kernels/graphics.ts";
|
|
4
|
+
import { paletteEffects, type PaletteEffectsState } from "./kernels/palette.ts";
|
|
5
|
+
import type { Palette } from "../gfx/palette.ts";
|
|
6
|
+
import { cloneShape, type ArrayType, type ListNode, type SciList, type SciObject } from "./memory.ts";
|
|
7
|
+
import type { Vm } from "./vm.ts";
|
|
8
|
+
import { makeRef, segmentOf, type Value } from "./value.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A saved game: the VM's memory, not the original interpreter's. Every reference in the game
|
|
12
|
+
* is a (segment, offset) pair, so restoring means recreating each segment at its original id:
|
|
13
|
+
* scripts by number (their code comes from the resources), clones by the script object they
|
|
14
|
+
* were cloned from, and arrays, lists, nodes and bitmaps by value. The call stack isn't
|
|
15
|
+
* saved: like Sierra's interpreter, restoring restarts execution with `theGame replay`.
|
|
16
|
+
*/
|
|
17
|
+
export interface VmSnapshot {
|
|
18
|
+
format: 1;
|
|
19
|
+
segments: (SegmentSnapshot | null)[];
|
|
20
|
+
freeSegments: number[];
|
|
21
|
+
scripts: { number: number; segment: number; localsSegment: number; locals: Value[]; objects: [offset: number, vars: Value[]][] }[];
|
|
22
|
+
stack: Value[];
|
|
23
|
+
graphics: { planes: Value[]; items: Value[]; palette?: Palette; effects: PaletteEffectsState };
|
|
24
|
+
audio: { masterVolume: number; songs: { object: Value; number: number; loop: boolean; volume: number; position: number }[] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type SegmentSnapshot =
|
|
28
|
+
| { kind: "script" | "locals" | "stack" }
|
|
29
|
+
| { kind: "clone"; template: Value; vars: Value[] }
|
|
30
|
+
| { kind: "array"; type: ArrayType; data: Value[] }
|
|
31
|
+
| { kind: "list"; first: number; last: number }
|
|
32
|
+
| { kind: "node"; value: Value; key: Value; prev: number; next: number; list: number }
|
|
33
|
+
| { kind: "bitmap"; width: number; height: number; pixels: Uint8Array; skipColor: number; originX: number; originY: number };
|
|
34
|
+
|
|
35
|
+
const seg = (v: { address: Value } | undefined) => (v ? segmentOf(v.address) : -1);
|
|
36
|
+
|
|
37
|
+
export function snapshot(vm: Vm): VmSnapshot {
|
|
38
|
+
const m = vm.memory;
|
|
39
|
+
const segments = m.segments.map((s): SegmentSnapshot | null => {
|
|
40
|
+
switch (s.kind) {
|
|
41
|
+
case "script": case "locals": case "stack": return { kind: s.kind };
|
|
42
|
+
case "clone": return { kind: "clone", template: s.object.template!.address, vars: s.object.vars.slice() };
|
|
43
|
+
case "array": return { kind: "array", type: s.array.type, data: s.array.data.slice() };
|
|
44
|
+
case "list": return { kind: "list", first: seg(s.list.first), last: seg(s.list.last) };
|
|
45
|
+
case "node": return { kind: "node", value: s.node.value, key: s.node.key, prev: seg(s.node.prev), next: seg(s.node.next), list: seg(s.node.list) };
|
|
46
|
+
case "bitmap": return { kind: "bitmap", ...s.bitmap, pixels: s.bitmap.pixels.slice() };
|
|
47
|
+
default: return null;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const g = graphics(vm);
|
|
51
|
+
const a = audio(vm);
|
|
52
|
+
const now = a.ticks();
|
|
53
|
+
return {
|
|
54
|
+
format: 1,
|
|
55
|
+
segments,
|
|
56
|
+
freeSegments: [...m.free_],
|
|
57
|
+
scripts: vm.loadedScripts.map((s) => ({
|
|
58
|
+
number: s.number,
|
|
59
|
+
segment: s.segment,
|
|
60
|
+
localsSegment: s.localsSegment,
|
|
61
|
+
locals: s.locals.slice(),
|
|
62
|
+
objects: [...s.objects].map(([offset, obj]) => [offset, obj.vars.slice()]),
|
|
63
|
+
})),
|
|
64
|
+
stack: m.stack.slice(),
|
|
65
|
+
graphics: {
|
|
66
|
+
planes: [...g.planes],
|
|
67
|
+
items: [...g.items],
|
|
68
|
+
palette: g.sourcePalette && { rgb: g.sourcePalette.rgb.slice(), used: g.sourcePalette.used.slice() },
|
|
69
|
+
effects: paletteEffects(vm).save(),
|
|
70
|
+
},
|
|
71
|
+
audio: {
|
|
72
|
+
masterVolume: a.masterVolume,
|
|
73
|
+
songs: a.playingSongs().map((p) => ({ object: p.object, number: p.number, loop: p.player.loop, volume: p.player.volume, position: p.player.position(now) })),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Replaces the VM's state with `snap`. The caller then restarts execution (`replay`). */
|
|
79
|
+
export function restore(vm: Vm, snap: VmSnapshot): void {
|
|
80
|
+
if (snap.format !== 1) throw new Error(`Unsupported save format ${snap.format}`);
|
|
81
|
+
audio(vm).stopEverything();
|
|
82
|
+
vm.resetMemory();
|
|
83
|
+
const m = vm.memory;
|
|
84
|
+
if (m.stackSegment !== snap.segments.findIndex((s) => s?.kind === "stack")) throw new Error("Stack segment moved");
|
|
85
|
+
|
|
86
|
+
// Reserve the whole table so nothing we create lands on a saved id by accident.
|
|
87
|
+
m.allocateAt(snap.segments.length - 1, { kind: "free" });
|
|
88
|
+
m.setFreeSegments([]);
|
|
89
|
+
|
|
90
|
+
for (const s of vm.restoreScripts(snap.scripts)) {
|
|
91
|
+
const saved = snap.scripts.find((x) => x.number === s.number)!;
|
|
92
|
+
s.locals.splice(0, s.locals.length, ...saved.locals);
|
|
93
|
+
for (const [offset, vars] of saved.objects) {
|
|
94
|
+
const obj = s.objects.get(offset);
|
|
95
|
+
if (obj) obj.vars = vars.slice();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const nodes = new Map<number, ListNode>();
|
|
100
|
+
const lists = new Map<number, SciList>();
|
|
101
|
+
snap.segments.forEach((s, id) => {
|
|
102
|
+
if (!s) return;
|
|
103
|
+
const address = makeRef(id, 0);
|
|
104
|
+
switch (s.kind) {
|
|
105
|
+
case "clone": {
|
|
106
|
+
const template = m.object(s.template);
|
|
107
|
+
if (!template) throw new Error(`Clone template ${s.template.toString(16)} missing`);
|
|
108
|
+
const obj: SciObject = cloneShape(template.template ?? template, s.vars.slice());
|
|
109
|
+
obj.address = address;
|
|
110
|
+
m.allocateAt(id, { kind: "clone", object: obj });
|
|
111
|
+
m.objects.set(address, obj);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "array":
|
|
115
|
+
m.allocateAt(id, { kind: "array", array: { type: s.type, data: s.data.slice() } });
|
|
116
|
+
break;
|
|
117
|
+
case "bitmap":
|
|
118
|
+
m.allocateAt(id, { kind: "bitmap", bitmap: { width: s.width, height: s.height, pixels: s.pixels.slice(), skipColor: s.skipColor, originX: s.originX, originY: s.originY } });
|
|
119
|
+
break;
|
|
120
|
+
case "list": {
|
|
121
|
+
const list: SciList = { address, first: undefined, last: undefined };
|
|
122
|
+
lists.set(id, list);
|
|
123
|
+
m.allocateAt(id, { kind: "list", list });
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "node": {
|
|
127
|
+
const node: ListNode = { address, value: s.value, key: s.key, prev: undefined, next: undefined, list: undefined };
|
|
128
|
+
nodes.set(id, node);
|
|
129
|
+
m.allocateAt(id, { kind: "node", node });
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// Link lists and nodes now that they all exist.
|
|
135
|
+
snap.segments.forEach((s, id) => {
|
|
136
|
+
if (s?.kind === "list") Object.assign(lists.get(id)!, { first: nodes.get(s.first), last: nodes.get(s.last) });
|
|
137
|
+
if (s?.kind === "node") Object.assign(nodes.get(id)!, { prev: nodes.get(s.prev), next: nodes.get(s.next), list: lists.get(s.list) });
|
|
138
|
+
});
|
|
139
|
+
m.setFreeSegments(snap.freeSegments.slice());
|
|
140
|
+
// Derived identity slots (see Vm.fixObjectIdentity): recompute rather than trust old saves.
|
|
141
|
+
for (const obj of m.objects.values()) vm.fixObjectIdentity(obj);
|
|
142
|
+
m.stack.splice(0, m.stack.length, ...snap.stack);
|
|
143
|
+
|
|
144
|
+
const g = graphics(vm);
|
|
145
|
+
g.planes.clear();
|
|
146
|
+
g.items.clear();
|
|
147
|
+
for (const p of snap.graphics.planes) g.planes.add(p);
|
|
148
|
+
for (const i of snap.graphics.items) g.items.add(i);
|
|
149
|
+
g.resetPalette();
|
|
150
|
+
g.sourcePalette = snap.graphics.palette && { rgb: snap.graphics.palette.rgb.slice(), used: snap.graphics.palette.used.slice() };
|
|
151
|
+
paletteEffects(vm).load(snap.graphics.effects, Math.floor((vm.clock() * 60) / 1000));
|
|
152
|
+
|
|
153
|
+
const a = audio(vm);
|
|
154
|
+
a.masterVolume = snap.audio.masterVolume;
|
|
155
|
+
a.midi?.setMasterVolume?.(a.masterVolume);
|
|
156
|
+
for (const song of snap.audio.songs) a.startSong(song.object, song.number, song.loop, song.volume, song.position);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Sanity check used by tests: every object's -super- points at a live class object. */
|
|
160
|
+
export function checkObjects(vm: Vm): string[] {
|
|
161
|
+
const problems: string[] = [];
|
|
162
|
+
for (const obj of vm.memory.objects.values()) {
|
|
163
|
+
const sup = obj.vars[ObjectSlot.Super]!;
|
|
164
|
+
if (sup && !vm.memory.object(sup)) problems.push(`${obj.name}: -super- ${sup.toString(16)} is not an object`);
|
|
165
|
+
}
|
|
166
|
+
return problems;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Saved games as JSON text, for moving them between the browser (IndexedDB keeps them as
|
|
171
|
+
* structured clones) and the Node tools. Typed arrays become `{ $typed, data }`.
|
|
172
|
+
*/
|
|
173
|
+
export function savesToJson(list: { info: unknown; snapshot: VmSnapshot }[]): string {
|
|
174
|
+
return JSON.stringify(list, (_key, value: unknown) =>
|
|
175
|
+
ArrayBuffer.isView(value) && !(value instanceof DataView)
|
|
176
|
+
? { $typed: value.constructor.name, data: Array.from(value as unknown as ArrayLike<number>) }
|
|
177
|
+
: value,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const TYPED: Record<string, new (data: number[]) => ArrayBufferView> = { Uint8Array, Uint16Array, Int16Array, Uint32Array, Int32Array, Float32Array, Float64Array };
|
|
182
|
+
|
|
183
|
+
export function savesFromJson<Info>(text: string): { info: Info; snapshot: VmSnapshot }[] {
|
|
184
|
+
return JSON.parse(text, (_key, value: unknown) => {
|
|
185
|
+
const t = value as { $typed?: string; data?: number[] } | null;
|
|
186
|
+
return t && typeof t === "object" && t.$typed && TYPED[t.$typed] ? new TYPED[t.$typed]!(t.data!) : value;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An SCI value ("reg_t"): a (segment, offset) pair packed into one JS number as
|
|
3
|
+
* `segment * 0x10000 + offset`. Segment 0 holds plain 16-bit integers; every other
|
|
4
|
+
* segment is something addressable: a script's heap, a clone, an array, a list...
|
|
5
|
+
*
|
|
6
|
+
* Numbers are stored unsigned (0..0xFFFF) and read signed where the bytecode wants it.
|
|
7
|
+
*/
|
|
8
|
+
export type Value = number;
|
|
9
|
+
export declare const NULL: Value;
|
|
10
|
+
export declare const makeRef: (segment: number, offset: number) => Value;
|
|
11
|
+
export declare const segmentOf: (v: Value) => number;
|
|
12
|
+
export declare const offsetOf: (v: Value) => number;
|
|
13
|
+
export declare const isNumber: (v: Value) => boolean;
|
|
14
|
+
export declare const toSigned: (v: Value) => number;
|
|
15
|
+
export declare const fromInt: (n: number) => Value;
|
|
16
|
+
export declare const bool: (b: boolean) => Value;
|
|
17
|
+
export declare const formatValue: (v: Value) => string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const NULL = 0;
|
|
2
|
+
export const makeRef = (segment, offset) => segment * 0x10000 + (offset & 0xffff);
|
|
3
|
+
export const segmentOf = (v) => Math.floor(v / 0x10000);
|
|
4
|
+
export const offsetOf = (v) => v & 0xffff;
|
|
5
|
+
export const isNumber = (v) => v < 0x10000;
|
|
6
|
+
export const toSigned = (v) => {
|
|
7
|
+
const n = v & 0xffff;
|
|
8
|
+
return n >= 0x8000 ? n - 0x10000 : n;
|
|
9
|
+
};
|
|
10
|
+
export const fromInt = (n) => n & 0xffff;
|
|
11
|
+
export const bool = (b) => (b ? 1 : 0);
|
|
12
|
+
export const formatValue = (v) => isNumber(v) ? String(toSigned(v)) : `${segmentOf(v).toString(16)}:${offsetOf(v).toString(16).padStart(4, "0")}`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An SCI value ("reg_t"): a (segment, offset) pair packed into one JS number as
|
|
3
|
+
* `segment * 0x10000 + offset`. Segment 0 holds plain 16-bit integers; every other
|
|
4
|
+
* segment is something addressable: a script's heap, a clone, an array, a list...
|
|
5
|
+
*
|
|
6
|
+
* Numbers are stored unsigned (0..0xFFFF) and read signed where the bytecode wants it.
|
|
7
|
+
*/
|
|
8
|
+
export type Value = number;
|
|
9
|
+
|
|
10
|
+
export const NULL: Value = 0;
|
|
11
|
+
|
|
12
|
+
export const makeRef = (segment: number, offset: number): Value => segment * 0x10000 + (offset & 0xffff);
|
|
13
|
+
export const segmentOf = (v: Value): number => Math.floor(v / 0x10000);
|
|
14
|
+
export const offsetOf = (v: Value): number => v & 0xffff;
|
|
15
|
+
export const isNumber = (v: Value): boolean => v < 0x10000;
|
|
16
|
+
|
|
17
|
+
export const toSigned = (v: Value): number => {
|
|
18
|
+
const n = v & 0xffff;
|
|
19
|
+
return n >= 0x8000 ? n - 0x10000 : n;
|
|
20
|
+
};
|
|
21
|
+
export const fromInt = (n: number): Value => n & 0xffff;
|
|
22
|
+
export const bool = (b: boolean): Value => (b ? 1 : 0);
|
|
23
|
+
|
|
24
|
+
export const formatValue = (v: Value): string =>
|
|
25
|
+
isNumber(v) ? String(toSigned(v)) : `${segmentOf(v).toString(16)}:${offsetOf(v).toString(16).padStart(4, "0")}`;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { ResourceManager } from "../resource/manager.ts";
|
|
2
|
+
import { Memory, type LoadedScript, type SciObject } from "./memory.ts";
|
|
3
|
+
import { type Value } from "./value.ts";
|
|
4
|
+
/** A message a native (generator) kernel wants sent; the VM resumes it with the result. */
|
|
5
|
+
export interface SendRequest {
|
|
6
|
+
object: Value;
|
|
7
|
+
selector: number;
|
|
8
|
+
args: Value[];
|
|
9
|
+
}
|
|
10
|
+
/** A native kernel's request to end the current frame (it has drawn one) and resume next tick. */
|
|
11
|
+
export interface FrameRequest {
|
|
12
|
+
frame: true;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A kernel function receives its arguments (argc = args.length) and returns acc.
|
|
16
|
+
* Kernels that call back into scripts are generators: they yield SendRequests and run as a
|
|
17
|
+
* "native frame" on the VM's frame stack, so a FrameOut inside the callback can still pause
|
|
18
|
+
* the whole VM for the browser.
|
|
19
|
+
*/
|
|
20
|
+
export type KernelFn = (vm: Vm, args: Value[]) => Value | void | Generator<SendRequest | FrameRequest, Value | void, Value>;
|
|
21
|
+
/** SCI2 -info- flag: a drawing property changed since the screen item was last updated. */
|
|
22
|
+
export declare const INFO_VIEW_CHANGED = 8;
|
|
23
|
+
export declare class VmError extends Error {
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The SCI virtual machine: a stack machine with an accumulator, executing bytecode methods
|
|
27
|
+
* of objects that talk to each other through `send`.
|
|
28
|
+
*
|
|
29
|
+
* Execution uses an explicit frame stack rather than JS recursion, so `run()` can stop at
|
|
30
|
+
* any instruction boundary (e.g. when FrameOut wants the browser to draw) and pick up later.
|
|
31
|
+
*/
|
|
32
|
+
export declare class Vm {
|
|
33
|
+
readonly resources: ResourceManager;
|
|
34
|
+
memory: Memory;
|
|
35
|
+
readonly selectors: string[];
|
|
36
|
+
readonly selectorIds: Map<string, number>;
|
|
37
|
+
private readonly classScripts;
|
|
38
|
+
private readonly classes;
|
|
39
|
+
private readonly scripts;
|
|
40
|
+
private readonly kernels;
|
|
41
|
+
readonly missingKernels: Map<string, number>;
|
|
42
|
+
acc: Value;
|
|
43
|
+
prev: Value;
|
|
44
|
+
private sp;
|
|
45
|
+
private rest;
|
|
46
|
+
private frames;
|
|
47
|
+
/** Set by kernels (FrameOut) to end the current `run()` slice. */
|
|
48
|
+
yieldRequested: boolean;
|
|
49
|
+
instructions: number;
|
|
50
|
+
/** Milliseconds since boot. Browser: real time. Headless runs can drive it per frame. */
|
|
51
|
+
clock: () => number;
|
|
52
|
+
/** Host hook: move the mouse (SetCursor warp), e.g. update the input position. */
|
|
53
|
+
moveMouse?: (x: number, y: number) => void;
|
|
54
|
+
/** Called once per FrameOut (music sequencing, etc.). */
|
|
55
|
+
readonly frameHooks: (() => void)[];
|
|
56
|
+
/** Optional hook for tracing method calls (object, selector name, argument count). */
|
|
57
|
+
onSend?: (object: SciObject, selector: string, argc: number) => void;
|
|
58
|
+
/** Optional hook for tracing kernel calls. */
|
|
59
|
+
onKernelCall?: (name: string, args: Value[], result: Value) => void;
|
|
60
|
+
constructor(resources: ResourceManager);
|
|
61
|
+
registerKernels(table: Record<string, KernelFn>): void;
|
|
62
|
+
loadScript(number: number): LoadedScript;
|
|
63
|
+
/**
|
|
64
|
+
* Creates a script's segments, locals and objects from its resources, without linking
|
|
65
|
+
* instances to their classes (which may load other scripts). Restoring a save instantiates
|
|
66
|
+
* every script at its saved segment ids first, then resolves them all.
|
|
67
|
+
*/
|
|
68
|
+
private instantiateScript;
|
|
69
|
+
private resolveScript;
|
|
70
|
+
/**
|
|
71
|
+
* `Obj::isKindOf` compares -propDict- and -classScript- to decide whether two objects share
|
|
72
|
+
* a class, so (as in the original interpreter) every object's -classScript- is its script
|
|
73
|
+
* number and an instance's -propDict- is its class's. Left as raw heap values, unrelated
|
|
74
|
+
* objects can match (e.g. a spell icon claiming to be an inventory item).
|
|
75
|
+
*/
|
|
76
|
+
fixObjectIdentity(obj: SciObject): void;
|
|
77
|
+
/**
|
|
78
|
+
* DisposeScript: as in SSCI the script is unloaded, so its objects start fresh from the
|
|
79
|
+
* resource the next time it's used. Dialogs rely on that: SRDialog's init offsets its
|
|
80
|
+
* buttons from where they are, which only works on fresh objects. A script still running
|
|
81
|
+
* (the dialog disposing its own script) goes once its last frame returns.
|
|
82
|
+
*/
|
|
83
|
+
disposeScript(number: number): void;
|
|
84
|
+
private readonly disposed;
|
|
85
|
+
private unloadDisposed;
|
|
86
|
+
/** Objects the host keeps outside VM memory (planes, screen items, playing sounds). */
|
|
87
|
+
readonly hostRoots: (() => Iterable<Value>)[];
|
|
88
|
+
/**
|
|
89
|
+
* Whether anything live still needs the script: a reference into it, or a clone or
|
|
90
|
+
* subclass of one of its classes, reachable from the roots (other scripts' locals and
|
|
91
|
+
* objects, the stack, acc, running frames, the host). As in ScummVM's GC, only reachable
|
|
92
|
+
* data counts, so garbage (a list nobody points to any more) doesn't pin it; a disposed
|
|
93
|
+
* script that's still needed stays loaded (a pending Cue to a room's script, say).
|
|
94
|
+
*/
|
|
95
|
+
private scriptInUse;
|
|
96
|
+
private unloadScript;
|
|
97
|
+
get loadedScripts(): LoadedScript[];
|
|
98
|
+
/** Throws away all scripts, objects and execution state (restart / before a restore). */
|
|
99
|
+
resetMemory(): void;
|
|
100
|
+
/** Drops the call stack: nothing that was running will resume. */
|
|
101
|
+
resetExecution(): void;
|
|
102
|
+
/** Recreates scripts at their saved segment ids, then links them (see instantiateScript). */
|
|
103
|
+
restoreScripts(entries: {
|
|
104
|
+
number: number;
|
|
105
|
+
segment: number;
|
|
106
|
+
localsSegment: number;
|
|
107
|
+
}[]): LoadedScript[];
|
|
108
|
+
localsSegment(s: LoadedScript): number;
|
|
109
|
+
classObject(species: number): SciObject;
|
|
110
|
+
/** Address of export `index` of `script` (objects: the object; procedures: a code ref). */
|
|
111
|
+
exportAddress(scriptNumber: number, index: number): Value;
|
|
112
|
+
object(v: Value): SciObject;
|
|
113
|
+
selector(name: string): number;
|
|
114
|
+
/** Reads a property by selector name; undefined if the object doesn't have it. */
|
|
115
|
+
getProp(obj: Value | SciObject, name: string): Value | undefined;
|
|
116
|
+
setProp(obj: Value | SciObject, name: string, value: Value): void;
|
|
117
|
+
/**
|
|
118
|
+
* Writes a property. SCI2's interpreter also marks the object "changed since drawn"
|
|
119
|
+
* (bit 0x0008 of -info-) when the property is one that affects drawing (variable slots
|
|
120
|
+
* 26-44: plane, x, y, z, scale..., priority, inset, view, loop, cel, bitmap), whoever
|
|
121
|
+
* writes it: script, send or kernel. Updating the screen item clears it. Scripts rely on
|
|
122
|
+
* it: Actor::doit only runs its scaler and UpdateScreenItem when the bit is set.
|
|
123
|
+
* (ScummVM: Object::mustSetViewVisible, updateInfoFlagViewVisible.)
|
|
124
|
+
*/
|
|
125
|
+
writeVar(obj: SciObject, slot: number, value: Value): void;
|
|
126
|
+
private findMethod;
|
|
127
|
+
respondsTo(obj: SciObject, selector: number): boolean;
|
|
128
|
+
private push;
|
|
129
|
+
/**
|
|
130
|
+
* Starts a send of `selector` with `args` to `object` as a new top-level frame.
|
|
131
|
+
* Used to boot the game (`theGame play`); `run()` then drives it.
|
|
132
|
+
*/
|
|
133
|
+
start(object: Value, selectorName: string, args?: Value[]): void;
|
|
134
|
+
/**
|
|
135
|
+
* Synchronously sends a message and runs until it returns: for kernels that call back
|
|
136
|
+
* into scripts (e.g. ListEachElementDo). Returns the resulting acc.
|
|
137
|
+
*/
|
|
138
|
+
invoke(object: Value, selector: number, args?: Value[]): Value;
|
|
139
|
+
get running(): boolean;
|
|
140
|
+
/** Runs until the game yields (FrameOut), finishes, or `budget` instructions pass. */
|
|
141
|
+
run(budget?: number): void;
|
|
142
|
+
/** Dispatches remaining selectors of `caller.send`; returns when a method frame is pushed or the send ends. */
|
|
143
|
+
private continueSend;
|
|
144
|
+
private beginSend;
|
|
145
|
+
private callKernel;
|
|
146
|
+
private callLocal;
|
|
147
|
+
/** Resumes a native frame: performs its next send request, or finishes it. */
|
|
148
|
+
private resumeNative;
|
|
149
|
+
private varArray;
|
|
150
|
+
/** Executes until frames drop to `stopDepth`, a yield (top level only), or the budget runs out. */
|
|
151
|
+
private execute;
|
|
152
|
+
/** Human-readable call stack, for errors and debugging. */
|
|
153
|
+
backtrace(): string[];
|
|
154
|
+
isScriptLoaded(n: number): boolean;
|
|
155
|
+
}
|