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,914 @@
|
|
|
1
|
+
import type { ResourceManager } from "../resource/manager.ts";
|
|
2
|
+
import { ResourceType } from "../resource/types.ts";
|
|
3
|
+
import { kernelNames } from "../script/kernel-names.ts";
|
|
4
|
+
import { ObjectSlot, parseScript } from "../script/script.ts";
|
|
5
|
+
import { parseClassTable, parseSelectorNames } from "../script/vocab.ts";
|
|
6
|
+
import { Memory, type LoadedScript, type SciObject } from "./memory.ts";
|
|
7
|
+
import { NULL, bool, formatValue, isNumber, makeRef, offsetOf, segmentOf, toSigned, type Value } from "./value.ts";
|
|
8
|
+
|
|
9
|
+
/** A message a native (generator) kernel wants sent; the VM resumes it with the result. */
|
|
10
|
+
export interface SendRequest {
|
|
11
|
+
object: Value;
|
|
12
|
+
selector: number;
|
|
13
|
+
args: Value[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A native kernel's request to end the current frame (it has drawn one) and resume next tick. */
|
|
17
|
+
export interface FrameRequest {
|
|
18
|
+
frame: true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A kernel function receives its arguments (argc = args.length) and returns acc.
|
|
23
|
+
* Kernels that call back into scripts are generators: they yield SendRequests and run as a
|
|
24
|
+
* "native frame" on the VM's frame stack, so a FrameOut inside the callback can still pause
|
|
25
|
+
* the whole VM for the browser.
|
|
26
|
+
*/
|
|
27
|
+
export type KernelFn = (vm: Vm, args: Value[]) => Value | void | Generator<SendRequest | FrameRequest, Value | void, Value>;
|
|
28
|
+
|
|
29
|
+
interface SendItem {
|
|
30
|
+
selector: number;
|
|
31
|
+
argc: number;
|
|
32
|
+
/** Stack index of the selector word; argc is at base+1, args follow. */
|
|
33
|
+
base: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A multi-selector send in progress, resumed each time a method it invoked returns. */
|
|
37
|
+
interface SendJob {
|
|
38
|
+
object: SciObject;
|
|
39
|
+
/** For `super`: class where method lookup starts. */
|
|
40
|
+
startClass: number | undefined;
|
|
41
|
+
items: SendItem[];
|
|
42
|
+
index: number;
|
|
43
|
+
spAfter: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Frame {
|
|
47
|
+
script: LoadedScript;
|
|
48
|
+
pc: number;
|
|
49
|
+
self: SciObject | undefined;
|
|
50
|
+
/** Stack index of this call's argc (param 0). */
|
|
51
|
+
paramBase: number;
|
|
52
|
+
/** Stack index of temp 0. */
|
|
53
|
+
tempBase: number;
|
|
54
|
+
/** Stack pointer to restore when this frame returns (call/callk-style frames). */
|
|
55
|
+
restoreSp: number;
|
|
56
|
+
/** Set on the *caller* while a send it started is still dispatching selectors. */
|
|
57
|
+
send?: SendJob;
|
|
58
|
+
/** Frames started by `invoke` stop the nested run loop when they return. */
|
|
59
|
+
stopOnReturn?: boolean;
|
|
60
|
+
/** Native frame: a generator kernel waiting on the sends it requested. */
|
|
61
|
+
native?: Generator<SendRequest | FrameRequest, Value | void, Value>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** SCI2 -info- flag: a drawing property changed since the screen item was last updated. */
|
|
65
|
+
export const INFO_VIEW_CHANGED = 0x0008;
|
|
66
|
+
/** SCI2 variable slots whose writes set it (games with nsLeft-style selectors). */
|
|
67
|
+
const VIEW_VAR_FIRST = 26, VIEW_VAR_LAST = 44;
|
|
68
|
+
|
|
69
|
+
export class VmError extends Error {}
|
|
70
|
+
|
|
71
|
+
/** Opcodes, for readability in the dispatch loop (instruction byte = opcode << 1 | small). */
|
|
72
|
+
enum Op {
|
|
73
|
+
bnot, add, sub, mul, div, mod, shr, shl, xor, and, or, neg, not,
|
|
74
|
+
eq, ne, gt, ge, lt, le, ugt, uge, ult, ule,
|
|
75
|
+
bt, bnt, jmp, ldi, push, pushi, toss, dup, link,
|
|
76
|
+
call, callk, callb, calle, ret, send,
|
|
77
|
+
class_ = 0x28, self_ = 0x2a, super_, rest, lea, selfID,
|
|
78
|
+
pprev = 0x30, pToa, aTop, pTos, sTop, ipToa, dpToa, ipTos, dpTos,
|
|
79
|
+
lofsa, lofss, push0, push1, push2, pushSelf,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The SCI virtual machine: a stack machine with an accumulator, executing bytecode methods
|
|
84
|
+
* of objects that talk to each other through `send`.
|
|
85
|
+
*
|
|
86
|
+
* Execution uses an explicit frame stack rather than JS recursion, so `run()` can stop at
|
|
87
|
+
* any instruction boundary (e.g. when FrameOut wants the browser to draw) and pick up later.
|
|
88
|
+
*/
|
|
89
|
+
export class Vm {
|
|
90
|
+
memory = new Memory();
|
|
91
|
+
readonly selectors: string[];
|
|
92
|
+
readonly selectorIds = new Map<string, number>();
|
|
93
|
+
private readonly classScripts: number[];
|
|
94
|
+
private readonly classes = new Map<number, SciObject>();
|
|
95
|
+
private readonly scripts = new Map<number, LoadedScript>();
|
|
96
|
+
private readonly kernels: (KernelFn | undefined)[] = [];
|
|
97
|
+
readonly missingKernels = new Map<string, number>();
|
|
98
|
+
|
|
99
|
+
acc: Value = NULL;
|
|
100
|
+
prev: Value = NULL;
|
|
101
|
+
private sp = 0;
|
|
102
|
+
private rest = 0;
|
|
103
|
+
private frames: Frame[] = [];
|
|
104
|
+
/** Set by kernels (FrameOut) to end the current `run()` slice. */
|
|
105
|
+
yieldRequested = false;
|
|
106
|
+
instructions = 0;
|
|
107
|
+
/** Milliseconds since boot. Browser: real time. Headless runs can drive it per frame. */
|
|
108
|
+
clock: () => number = (() => {
|
|
109
|
+
const t0 = Date.now();
|
|
110
|
+
return () => Date.now() - t0;
|
|
111
|
+
})();
|
|
112
|
+
/** Host hook: move the mouse (SetCursor warp), e.g. update the input position. */
|
|
113
|
+
moveMouse?: (x: number, y: number) => void;
|
|
114
|
+
/** Called once per FrameOut (music sequencing, etc.). */
|
|
115
|
+
readonly frameHooks: (() => void)[] = [];
|
|
116
|
+
/** Optional hook for tracing method calls (object, selector name, argument count). */
|
|
117
|
+
onSend?: (object: SciObject, selector: string, argc: number) => void;
|
|
118
|
+
/** Optional hook for tracing kernel calls. */
|
|
119
|
+
onKernelCall?: (name: string, args: Value[], result: Value) => void;
|
|
120
|
+
|
|
121
|
+
constructor(readonly resources: ResourceManager) {
|
|
122
|
+
this.selectors = parseSelectorNames(resources.loadSync({ type: ResourceType.Vocab, number: 997 }).data);
|
|
123
|
+
this.selectors.forEach((name, id) => this.selectorIds.set(name, id));
|
|
124
|
+
this.classScripts = parseClassTable(resources.loadSync({ type: ResourceType.Vocab, number: 996 }).data);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
registerKernels(table: Record<string, KernelFn>): void {
|
|
128
|
+
kernelNames.forEach((name, i) => {
|
|
129
|
+
if (table[name]) this.kernels[i] = table[name];
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Scripts, classes, objects ---------------------------------------------
|
|
134
|
+
|
|
135
|
+
loadScript(number: number): LoadedScript {
|
|
136
|
+
const existing = this.scripts.get(number);
|
|
137
|
+
if (existing) return existing;
|
|
138
|
+
const loaded = this.instantiateScript(number);
|
|
139
|
+
this.resolveScript(loaded);
|
|
140
|
+
return loaded;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Creates a script's segments, locals and objects from its resources, without linking
|
|
145
|
+
* instances to their classes (which may load other scripts). Restoring a save instantiates
|
|
146
|
+
* every script at its saved segment ids first, then resolves them all.
|
|
147
|
+
*/
|
|
148
|
+
private instantiateScript(number: number, at?: { segment: number; localsSegment: number }): LoadedScript {
|
|
149
|
+
const code = this.resources.loadSync({ type: ResourceType.Script, number }).data;
|
|
150
|
+
const heap = this.resources.loadSync({ type: ResourceType.Heap, number }).data;
|
|
151
|
+
const script = parseScript(number, code, heap);
|
|
152
|
+
const hv = new DataView(heap.buffer, heap.byteOffset, heap.byteLength);
|
|
153
|
+
|
|
154
|
+
// Heap words listed in the relocation table are pointers into this script's heap.
|
|
155
|
+
const relocTable = hv.getUint16(0, true);
|
|
156
|
+
const relocated = new Set<number>();
|
|
157
|
+
for (let i = 0; i < hv.getUint16(relocTable, true); i++) relocated.add(hv.getUint16(relocTable + 2 + i * 2, true));
|
|
158
|
+
|
|
159
|
+
const loaded: LoadedScript = {
|
|
160
|
+
number,
|
|
161
|
+
script,
|
|
162
|
+
segment: 0,
|
|
163
|
+
localsSegment: 0,
|
|
164
|
+
locals: [],
|
|
165
|
+
objects: new Map(),
|
|
166
|
+
code,
|
|
167
|
+
codeView: new DataView(code.buffer, code.byteOffset, code.byteLength),
|
|
168
|
+
};
|
|
169
|
+
if (at) {
|
|
170
|
+
loaded.segment = at.segment;
|
|
171
|
+
loaded.localsSegment = at.localsSegment;
|
|
172
|
+
this.memory.allocateAt(at.segment, { kind: "script", script: loaded });
|
|
173
|
+
this.memory.allocateAt(at.localsSegment, { kind: "locals", script: loaded });
|
|
174
|
+
} else {
|
|
175
|
+
loaded.segment = this.memory.allocate({ kind: "script", script: loaded });
|
|
176
|
+
loaded.localsSegment = this.memory.allocate({ kind: "locals", script: loaded });
|
|
177
|
+
}
|
|
178
|
+
const word = (heapOffset: number) => {
|
|
179
|
+
const w = hv.getUint16(heapOffset, true);
|
|
180
|
+
return relocated.has(heapOffset) ? makeRef(loaded.segment, w) : w;
|
|
181
|
+
};
|
|
182
|
+
loaded.locals = script.locals.map((_, i) => word(4 + i * 2));
|
|
183
|
+
this.scripts.set(number, loaded);
|
|
184
|
+
|
|
185
|
+
for (const def of script.objects) {
|
|
186
|
+
const obj: SciObject = {
|
|
187
|
+
address: makeRef(loaded.segment, def.offset),
|
|
188
|
+
name: def.name,
|
|
189
|
+
script: loaded,
|
|
190
|
+
vars: def.properties.map((_, i) => word(def.offset + i * 2)),
|
|
191
|
+
isClass: def.isClass,
|
|
192
|
+
methods: new Map(def.methods.map((m) => [m.selector, m.offset])),
|
|
193
|
+
propIndex: new Map(),
|
|
194
|
+
superclass: def.superclass,
|
|
195
|
+
species: def.isClass ? def.species : def.superclass,
|
|
196
|
+
isClone: false,
|
|
197
|
+
template: undefined,
|
|
198
|
+
};
|
|
199
|
+
if (def.isClass) {
|
|
200
|
+
def.propSelectors!.forEach((sel, slot) => obj.propIndex.set(sel, slot));
|
|
201
|
+
this.classes.set(def.species, obj);
|
|
202
|
+
}
|
|
203
|
+
loaded.objects.set(def.offset, obj);
|
|
204
|
+
this.memory.objects.set(obj.address, obj);
|
|
205
|
+
}
|
|
206
|
+
return loaded;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private resolveScript(loaded: LoadedScript): void {
|
|
210
|
+
// Instances share their class's property layout; the class may live in another script.
|
|
211
|
+
// At runtime -super- holds the superclass *object* and a class's -species- is itself
|
|
212
|
+
// (the heap stores class numbers), which is what Obj::isKindOf walks.
|
|
213
|
+
for (const obj of loaded.objects.values()) {
|
|
214
|
+
if (!obj.isClass) obj.propIndex = this.classObject(obj.superclass).propIndex;
|
|
215
|
+
if (obj.isClass) obj.vars[ObjectSlot.Species] = obj.address;
|
|
216
|
+
obj.vars[ObjectSlot.Super] = obj.superclass === 0xffff ? NULL : this.classObject(obj.superclass).address;
|
|
217
|
+
this.fixObjectIdentity(obj);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* `Obj::isKindOf` compares -propDict- and -classScript- to decide whether two objects share
|
|
223
|
+
* a class, so (as in the original interpreter) every object's -classScript- is its script
|
|
224
|
+
* number and an instance's -propDict- is its class's. Left as raw heap values, unrelated
|
|
225
|
+
* objects can match (e.g. a spell icon claiming to be an inventory item).
|
|
226
|
+
*/
|
|
227
|
+
fixObjectIdentity(obj: SciObject): void {
|
|
228
|
+
const source = obj.isClone ? (obj.template ?? obj) : obj;
|
|
229
|
+
obj.vars[ObjectSlot.ClassScript] = source.script.number;
|
|
230
|
+
const cls = source.isClass ? source : this.classes.get(source.superclass);
|
|
231
|
+
if (cls && !obj.isClass) obj.vars[ObjectSlot.PropDict] = cls.vars[ObjectSlot.PropDict]!;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* DisposeScript: as in SSCI the script is unloaded, so its objects start fresh from the
|
|
236
|
+
* resource the next time it's used. Dialogs rely on that: SRDialog's init offsets its
|
|
237
|
+
* buttons from where they are, which only works on fresh objects. A script still running
|
|
238
|
+
* (the dialog disposing its own script) goes once its last frame returns.
|
|
239
|
+
*/
|
|
240
|
+
disposeScript(number: number): void {
|
|
241
|
+
if (number === 0 || !this.scripts.has(number)) return;
|
|
242
|
+
this.disposed.add(number);
|
|
243
|
+
this.unloadDisposed();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private readonly disposed = new Set<number>();
|
|
247
|
+
|
|
248
|
+
private unloadDisposed(): void {
|
|
249
|
+
for (const n of this.disposed) {
|
|
250
|
+
const s = this.scripts.get(n);
|
|
251
|
+
if (s && this.frames.some((f) => f.script === s)) continue;
|
|
252
|
+
this.disposed.delete(n);
|
|
253
|
+
if (s && !this.scriptInUse(s)) this.unloadScript(s);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Objects the host keeps outside VM memory (planes, screen items, playing sounds). */
|
|
258
|
+
readonly hostRoots: (() => Iterable<Value>)[] = [];
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Whether anything live still needs the script: a reference into it, or a clone or
|
|
262
|
+
* subclass of one of its classes, reachable from the roots (other scripts' locals and
|
|
263
|
+
* objects, the stack, acc, running frames, the host). As in ScummVM's GC, only reachable
|
|
264
|
+
* data counts, so garbage (a list nobody points to any more) doesn't pin it; a disposed
|
|
265
|
+
* script that's still needed stays loaded (a pending Cue to a room's script, say).
|
|
266
|
+
*/
|
|
267
|
+
private scriptInUse(s: LoadedScript): boolean {
|
|
268
|
+
const m = this.memory;
|
|
269
|
+
const species = new Set<number>();
|
|
270
|
+
for (const o of s.objects.values()) if (o.isClass) species.add(o.species);
|
|
271
|
+
const seen = new Set<number>();
|
|
272
|
+
const pending: Value[] = [this.acc];
|
|
273
|
+
for (const other of this.scripts.values()) {
|
|
274
|
+
if (other === s) continue;
|
|
275
|
+
pending.push(...other.locals);
|
|
276
|
+
for (const o of other.objects.values()) pending.push(o.address);
|
|
277
|
+
}
|
|
278
|
+
for (let i = 0; i < this.sp; i++) pending.push(m.stack[i]!);
|
|
279
|
+
for (const f of this.frames) if (f.self) pending.push(f.self.address);
|
|
280
|
+
for (const roots of this.hostRoots) pending.push(...roots());
|
|
281
|
+
while (pending.length) {
|
|
282
|
+
const v = pending.pop()!;
|
|
283
|
+
const id = segmentOf(v);
|
|
284
|
+
if (id === 0) continue;
|
|
285
|
+
// Only references to its objects count: stale words that point into its code, strings
|
|
286
|
+
// or locals (a leftover temp, a by-reference argument kept in another script's locals)
|
|
287
|
+
// don't keep SSCI from unloading it either.
|
|
288
|
+
if (id === s.segment && m.objects.has(v)) return true;
|
|
289
|
+
if (m.segments[id]?.kind === "script") {
|
|
290
|
+
// Objects in other scripts are distinct per offset; follow each once.
|
|
291
|
+
if (seen.has(v)) continue;
|
|
292
|
+
seen.add(v);
|
|
293
|
+
} else {
|
|
294
|
+
if (seen.has(id * 0x10000)) continue;
|
|
295
|
+
seen.add(id * 0x10000);
|
|
296
|
+
}
|
|
297
|
+
const seg = m.segments[id];
|
|
298
|
+
const o = m.objects.get(v);
|
|
299
|
+
if (o) {
|
|
300
|
+
if (o.isClone ? o.template?.script === s : species.has(o.superclass)) return true;
|
|
301
|
+
pending.push(...o.vars);
|
|
302
|
+
} else if (seg?.kind === "array") pending.push(...seg.array.data);
|
|
303
|
+
else if (seg?.kind === "list") for (let n = seg.list.first; n; n = n.next) pending.push(n.value, n.key);
|
|
304
|
+
else if (seg?.kind === "node") pending.push(seg.node.value, seg.node.key);
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
private unloadScript(s: LoadedScript): void {
|
|
311
|
+
for (const o of s.objects.values()) {
|
|
312
|
+
this.memory.objects.delete(o.address);
|
|
313
|
+
if (o.isClass && this.classes.get(o.species) === o) this.classes.delete(o.species);
|
|
314
|
+
}
|
|
315
|
+
this.memory.free(s.segment);
|
|
316
|
+
this.memory.free(s.localsSegment);
|
|
317
|
+
this.scripts.delete(s.number);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// --- Save/restore support ----------------------------------------------------
|
|
321
|
+
|
|
322
|
+
get loadedScripts(): LoadedScript[] {
|
|
323
|
+
return [...this.scripts.values()];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Throws away all scripts, objects and execution state (restart / before a restore). */
|
|
327
|
+
resetMemory(): void {
|
|
328
|
+
// Keep the stack array: a restore/restart kernel runs inside execute(), which holds it.
|
|
329
|
+
const stack = this.memory.stack;
|
|
330
|
+
stack.fill(0);
|
|
331
|
+
this.memory = new Memory(stack);
|
|
332
|
+
this.scripts.clear();
|
|
333
|
+
this.classes.clear();
|
|
334
|
+
this.disposed.clear();
|
|
335
|
+
this.resetExecution();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Drops the call stack: nothing that was running will resume. */
|
|
339
|
+
resetExecution(): void {
|
|
340
|
+
this.frames = [];
|
|
341
|
+
this.sp = 0;
|
|
342
|
+
this.rest = 0;
|
|
343
|
+
this.acc = NULL;
|
|
344
|
+
this.prev = NULL;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Recreates scripts at their saved segment ids, then links them (see instantiateScript). */
|
|
348
|
+
restoreScripts(entries: { number: number; segment: number; localsSegment: number }[]): LoadedScript[] {
|
|
349
|
+
const loaded = entries.map((e) => {
|
|
350
|
+
const s = this.instantiateScript(e.number, e);
|
|
351
|
+
this.scripts.set(e.number, s);
|
|
352
|
+
return s;
|
|
353
|
+
});
|
|
354
|
+
for (const s of loaded) this.resolveScript(s);
|
|
355
|
+
return loaded;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
localsSegment(s: LoadedScript): number {
|
|
359
|
+
return s.localsSegment;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
classObject(species: number): SciObject {
|
|
363
|
+
let cls = this.classes.get(species);
|
|
364
|
+
if (!cls) {
|
|
365
|
+
const scriptNumber = this.classScripts[species];
|
|
366
|
+
if (scriptNumber === undefined) throw new VmError(`Unknown class ${species}`);
|
|
367
|
+
this.loadScript(scriptNumber);
|
|
368
|
+
cls = this.classes.get(species);
|
|
369
|
+
if (!cls) throw new VmError(`Class ${species} not found in script ${scriptNumber}`);
|
|
370
|
+
}
|
|
371
|
+
return cls;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Address of export `index` of `script` (objects: the object; procedures: a code ref). */
|
|
375
|
+
exportAddress(scriptNumber: number, index: number): Value {
|
|
376
|
+
const s = this.loadScript(scriptNumber);
|
|
377
|
+
const e = s.script.exports[index];
|
|
378
|
+
if (!e || e.kind === "empty") return NULL;
|
|
379
|
+
return makeRef(s.segment, e.offset);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
object(v: Value): SciObject {
|
|
383
|
+
const obj = this.memory.object(v);
|
|
384
|
+
if (!obj) throw new VmError(`Not an object: ${formatValue(v)}`);
|
|
385
|
+
return obj;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
selector(name: string): number {
|
|
389
|
+
const id = this.selectorIds.get(name);
|
|
390
|
+
if (id === undefined) throw new VmError(`Unknown selector ${name}`);
|
|
391
|
+
return id;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Reads a property by selector name; undefined if the object doesn't have it. */
|
|
395
|
+
getProp(obj: Value | SciObject, name: string): Value | undefined {
|
|
396
|
+
const o = typeof obj === "number" ? this.object(obj) : obj;
|
|
397
|
+
const slot = o.propIndex.get(this.selector(name));
|
|
398
|
+
return slot === undefined ? undefined : o.vars[slot];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
setProp(obj: Value | SciObject, name: string, value: Value): void {
|
|
402
|
+
const o = typeof obj === "number" ? this.object(obj) : obj;
|
|
403
|
+
const slot = o.propIndex.get(this.selector(name));
|
|
404
|
+
if (slot !== undefined) this.writeVar(o, slot, value);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Writes a property. SCI2's interpreter also marks the object "changed since drawn"
|
|
409
|
+
* (bit 0x0008 of -info-) when the property is one that affects drawing (variable slots
|
|
410
|
+
* 26-44: plane, x, y, z, scale..., priority, inset, view, loop, cel, bitmap), whoever
|
|
411
|
+
* writes it: script, send or kernel. Updating the screen item clears it. Scripts rely on
|
|
412
|
+
* it: Actor::doit only runs its scaler and UpdateScreenItem when the bit is set.
|
|
413
|
+
* (ScummVM: Object::mustSetViewVisible, updateInfoFlagViewVisible.)
|
|
414
|
+
*/
|
|
415
|
+
writeVar(obj: SciObject, slot: number, value: Value): void {
|
|
416
|
+
obj.vars[slot] = value;
|
|
417
|
+
if (slot >= VIEW_VAR_FIRST && slot <= VIEW_VAR_LAST) obj.vars[ObjectSlot.Info] = (obj.vars[ObjectSlot.Info]! | INFO_VIEW_CHANGED) & 0xffff;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private findMethod(obj: SciObject, selector: number, startClass?: number): { script: LoadedScript; offset: number } | undefined {
|
|
421
|
+
if (startClass === undefined) {
|
|
422
|
+
const own = obj.methods.get(selector);
|
|
423
|
+
if (own !== undefined) return { script: obj.script, offset: own };
|
|
424
|
+
}
|
|
425
|
+
let species = startClass ?? obj.superclass;
|
|
426
|
+
for (let guard = 0; guard < 64 && species !== 0xffff; guard++) {
|
|
427
|
+
const cls = this.classObject(species);
|
|
428
|
+
const m = cls.methods.get(selector);
|
|
429
|
+
if (m !== undefined) return { script: cls.script, offset: m };
|
|
430
|
+
if (cls.superclass === species) break;
|
|
431
|
+
species = cls.superclass;
|
|
432
|
+
}
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
respondsTo(obj: SciObject, selector: number): boolean {
|
|
437
|
+
return obj.propIndex.has(selector) || this.findMethod(obj, selector) !== undefined;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// --- Invocation ------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
private push(v: Value) {
|
|
443
|
+
this.memory.stack[this.sp++] = v;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Starts a send of `selector` with `args` to `object` as a new top-level frame.
|
|
448
|
+
* Used to boot the game (`theGame play`); `run()` then drives it.
|
|
449
|
+
*/
|
|
450
|
+
start(object: Value, selectorName: string, args: Value[] = []): void {
|
|
451
|
+
const base = this.sp;
|
|
452
|
+
this.push(this.selector(selectorName));
|
|
453
|
+
this.push(args.length);
|
|
454
|
+
for (const a of args) this.push(a);
|
|
455
|
+
const job: SendJob = {
|
|
456
|
+
object: this.object(object),
|
|
457
|
+
startClass: undefined,
|
|
458
|
+
items: [{ selector: this.selector(selectorName), argc: args.length, base }],
|
|
459
|
+
index: 0,
|
|
460
|
+
spAfter: base,
|
|
461
|
+
};
|
|
462
|
+
// A dummy root frame owns the send so that returning from it ends execution.
|
|
463
|
+
const root: Frame = { script: this.loadScript(0), pc: -1, self: undefined, paramBase: base, tempBase: base, restoreSp: base, send: job };
|
|
464
|
+
this.frames.push(root);
|
|
465
|
+
this.continueSend(root);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Synchronously sends a message and runs until it returns: for kernels that call back
|
|
470
|
+
* into scripts (e.g. ListEachElementDo). Returns the resulting acc.
|
|
471
|
+
*/
|
|
472
|
+
invoke(object: Value, selector: number, args: Value[] = []): Value {
|
|
473
|
+
const obj = this.object(object);
|
|
474
|
+
const slot = obj.propIndex.get(selector);
|
|
475
|
+
if (slot !== undefined) {
|
|
476
|
+
if (args.length) this.writeVar(obj, slot, args[0]!);
|
|
477
|
+
return obj.vars[slot]!;
|
|
478
|
+
}
|
|
479
|
+
const savedSp = this.sp;
|
|
480
|
+
const savedAcc = this.acc;
|
|
481
|
+
const depth = this.frames.length;
|
|
482
|
+
const base = this.sp;
|
|
483
|
+
this.push(selector);
|
|
484
|
+
this.push(args.length);
|
|
485
|
+
for (const a of args) this.push(a);
|
|
486
|
+
const method = this.findMethod(obj, selector);
|
|
487
|
+
if (!method) throw new VmError(`${obj.name} does not understand ${this.selectors[selector]}`);
|
|
488
|
+
this.frames.push({ script: method.script, pc: method.offset, self: obj, paramBase: base + 1, tempBase: this.sp, restoreSp: base, stopOnReturn: true });
|
|
489
|
+
this.execute(Infinity, depth);
|
|
490
|
+
const result = this.acc;
|
|
491
|
+
this.sp = savedSp;
|
|
492
|
+
this.acc = savedAcc;
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
get running(): boolean {
|
|
497
|
+
return this.frames.length > 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Runs until the game yields (FrameOut), finishes, or `budget` instructions pass. */
|
|
501
|
+
run(budget = 5_000_000): void {
|
|
502
|
+
this.yieldRequested = false;
|
|
503
|
+
this.execute(budget, 0);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Dispatches remaining selectors of `caller.send`; returns when a method frame is pushed or the send ends. */
|
|
507
|
+
private continueSend(caller: Frame): void {
|
|
508
|
+
const job = caller.send!;
|
|
509
|
+
while (job.index < job.items.length) {
|
|
510
|
+
const item = job.items[job.index++]!;
|
|
511
|
+
const obj = job.object;
|
|
512
|
+
const stack = this.memory.stack;
|
|
513
|
+
const slot = job.startClass === undefined ? obj.propIndex.get(item.selector) : undefined;
|
|
514
|
+
if (slot !== undefined) {
|
|
515
|
+
if (item.argc === 0) this.acc = obj.vars[slot]!;
|
|
516
|
+
else this.writeVar(obj, slot, stack[item.base + 2]!);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const method = this.findMethod(obj, item.selector, job.startClass);
|
|
520
|
+
if (!method) {
|
|
521
|
+
throw new VmError(`${obj.name} does not understand ${this.selectors[item.selector] ?? item.selector}`);
|
|
522
|
+
}
|
|
523
|
+
this.onSend?.(obj, this.selectors[item.selector] ?? String(item.selector), item.argc);
|
|
524
|
+
this.frames.push({
|
|
525
|
+
script: method.script,
|
|
526
|
+
pc: method.offset,
|
|
527
|
+
self: obj,
|
|
528
|
+
paramBase: item.base + 1,
|
|
529
|
+
tempBase: this.sp,
|
|
530
|
+
restoreSp: this.sp,
|
|
531
|
+
});
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
this.sp = job.spAfter;
|
|
535
|
+
caller.send = undefined;
|
|
536
|
+
if (caller.pc === -1) this.frames.pop(); // root frame from start()
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private beginSend(caller: Frame, object: SciObject, frameBytes: number, startClass?: number) {
|
|
540
|
+
const stack = this.memory.stack;
|
|
541
|
+
const words = frameBytes / 2 + this.rest;
|
|
542
|
+
const base = this.sp - words;
|
|
543
|
+
if (this.rest) stack[base + 1] = (stack[base + 1]! + this.rest) & 0xffff;
|
|
544
|
+
this.rest = 0;
|
|
545
|
+
const items: SendItem[] = [];
|
|
546
|
+
for (let i = base; i < base + words; ) {
|
|
547
|
+
const argc = stack[i + 1]!;
|
|
548
|
+
items.push({ selector: stack[i]!, argc, base: i });
|
|
549
|
+
i += 2 + argc;
|
|
550
|
+
}
|
|
551
|
+
caller.send = { object, startClass, items, index: 0, spAfter: base };
|
|
552
|
+
this.continueSend(caller);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private callKernel(index: number, frameBytes: number) {
|
|
556
|
+
const stack = this.memory.stack;
|
|
557
|
+
const slot = this.sp - frameBytes / 2 - 1 - this.rest;
|
|
558
|
+
const argc = (stack[slot]! + this.rest) & 0xffff;
|
|
559
|
+
this.rest = 0;
|
|
560
|
+
const args = stack.slice(slot + 1, slot + 1 + argc);
|
|
561
|
+
this.sp = slot;
|
|
562
|
+
const name = kernelNames[index] ?? `kernel${index}`;
|
|
563
|
+
const fn = this.kernels[index];
|
|
564
|
+
let result: Value = 0;
|
|
565
|
+
if (fn) {
|
|
566
|
+
const r = fn(this, args);
|
|
567
|
+
if (typeof r === "object") {
|
|
568
|
+
const top = this.frames[this.frames.length - 1]!;
|
|
569
|
+
this.frames.push({ script: top.script, pc: -2, self: undefined, paramBase: slot, tempBase: this.sp, restoreSp: slot, native: r });
|
|
570
|
+
this.onKernelCall?.(name, args, 0);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
result = r ?? this.acc;
|
|
574
|
+
} else {
|
|
575
|
+
this.missingKernels.set(name, (this.missingKernels.get(name) ?? 0) + 1);
|
|
576
|
+
}
|
|
577
|
+
this.acc = result;
|
|
578
|
+
this.onKernelCall?.(name, args, result);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private callLocal(script: LoadedScript, offset: number, frameBytes: number, self: SciObject | undefined) {
|
|
582
|
+
const stack = this.memory.stack;
|
|
583
|
+
const slot = this.sp - frameBytes / 2 - 1 - this.rest;
|
|
584
|
+
if (this.rest) stack[slot] = (stack[slot]! + this.rest) & 0xffff;
|
|
585
|
+
this.rest = 0;
|
|
586
|
+
this.frames.push({ script, pc: offset, self, paramBase: slot, tempBase: this.sp, restoreSp: slot });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Resumes a native frame: performs its next send request, or finishes it. */
|
|
590
|
+
private resumeNative(frame: Frame): void {
|
|
591
|
+
const gen = frame.native!;
|
|
592
|
+
let r = gen.next(this.acc);
|
|
593
|
+
while (!r.done) {
|
|
594
|
+
if ("frame" in r.value) {
|
|
595
|
+
// Modal kernels (EditText) wait for input across frames: stop this run slice here
|
|
596
|
+
// and continue the generator on the next one.
|
|
597
|
+
this.yieldRequested = true;
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const { object, selector, args } = r.value;
|
|
601
|
+
const obj = this.memory.object(object);
|
|
602
|
+
if (!obj) {
|
|
603
|
+
r = gen.next(NULL);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
const slot = obj.propIndex.get(selector);
|
|
607
|
+
if (slot !== undefined) {
|
|
608
|
+
if (args.length) this.writeVar(obj, slot, args[0]!);
|
|
609
|
+
r = gen.next(obj.vars[slot]!);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const method = this.findMethod(obj, selector);
|
|
613
|
+
if (!method) throw new VmError(`${obj.name} does not understand ${this.selectors[selector] ?? selector}`);
|
|
614
|
+
const base = this.sp;
|
|
615
|
+
this.push(selector);
|
|
616
|
+
this.push(args.length);
|
|
617
|
+
for (const a of args) this.push(a);
|
|
618
|
+
this.frames.push({ script: method.script, pc: method.offset, self: obj, paramBase: base + 1, tempBase: this.sp, restoreSp: base });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
this.frames.pop();
|
|
622
|
+
this.sp = frame.restoreSp;
|
|
623
|
+
this.acc = r.value ?? NULL;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// --- Variables ---------------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
private varArray(kind: number, f: Frame): { array: Value[]; base: number } {
|
|
629
|
+
switch (kind) {
|
|
630
|
+
case 0: return { array: this.scripts.get(0)!.locals, base: 0 };
|
|
631
|
+
case 1: return { array: f.script.locals, base: 0 };
|
|
632
|
+
case 2: return { array: this.memory.stack, base: f.tempBase };
|
|
633
|
+
default: return { array: this.memory.stack, base: f.paramBase };
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// --- Interpreter loop --------------------------------------------------------
|
|
638
|
+
|
|
639
|
+
/** Executes until frames drop to `stopDepth`, a yield (top level only), or the budget runs out. */
|
|
640
|
+
private execute(budget: number, stopDepth: number): void {
|
|
641
|
+
const stack = this.memory.stack;
|
|
642
|
+
let f = this.frames[this.frames.length - 1]!;
|
|
643
|
+
let code = f.script.code;
|
|
644
|
+
let view = f.script.codeView;
|
|
645
|
+
let pc = f.pc;
|
|
646
|
+
let n = 0;
|
|
647
|
+
|
|
648
|
+
const reload = () => {
|
|
649
|
+
f = this.frames[this.frames.length - 1]!;
|
|
650
|
+
code = f.script.code;
|
|
651
|
+
view = f.script.codeView;
|
|
652
|
+
pc = f.pc;
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
while (this.frames.length > stopDepth) {
|
|
657
|
+
if (n++ >= budget || (stopDepth === 0 && this.yieldRequested)) break;
|
|
658
|
+
if (f.native) {
|
|
659
|
+
this.resumeNative(f);
|
|
660
|
+
if (this.frames.length <= stopDepth) return;
|
|
661
|
+
reload();
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const insAt = pc;
|
|
665
|
+
const raw = code[pc++]!;
|
|
666
|
+
const op = raw >> 1;
|
|
667
|
+
const small = raw & 1;
|
|
668
|
+
const word = () => {
|
|
669
|
+
const v = small ? code[pc]! : view.getUint16(pc, true);
|
|
670
|
+
pc += small ? 1 : 2;
|
|
671
|
+
return v;
|
|
672
|
+
};
|
|
673
|
+
const sword = () => {
|
|
674
|
+
const v = small ? view.getInt8(pc) : view.getInt16(pc, true);
|
|
675
|
+
pc += small ? 1 : 2;
|
|
676
|
+
return v;
|
|
677
|
+
};
|
|
678
|
+
const u16 = () => {
|
|
679
|
+
const v = view.getUint16(pc, true);
|
|
680
|
+
pc += 2;
|
|
681
|
+
return v;
|
|
682
|
+
};
|
|
683
|
+
const pop = () => stack[--this.sp]!;
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
if (op >= 0x40) {
|
|
687
|
+
// Variable ops: [load|store|inc|dec] [acc|stack] [global|local|temp|param] [indexed]
|
|
688
|
+
const kind = op & 3;
|
|
689
|
+
const toStack = (op & 4) !== 0;
|
|
690
|
+
const indexed = (op & 8) !== 0;
|
|
691
|
+
const action = op >> 4; // 4 load, 5 store, 6 inc, 7 dec
|
|
692
|
+
let index = word();
|
|
693
|
+
if (indexed) index += toSigned(this.acc);
|
|
694
|
+
const { array, base } = this.varArray(kind, f);
|
|
695
|
+
const i = base + index;
|
|
696
|
+
if (kind === 3 && index > stack[f.paramBase]! && action === 4) {
|
|
697
|
+
// Reading a parameter the caller didn't pass: SCI gives 0.
|
|
698
|
+
if (toStack) this.push(0);
|
|
699
|
+
else this.acc = 0;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
switch (action) {
|
|
703
|
+
case 4:
|
|
704
|
+
if (toStack) this.push(array[i] ?? 0);
|
|
705
|
+
else this.acc = array[i] ?? 0;
|
|
706
|
+
break;
|
|
707
|
+
case 5:
|
|
708
|
+
if (indexed && !toStack) {
|
|
709
|
+
this.acc = pop();
|
|
710
|
+
array[i] = this.acc;
|
|
711
|
+
} else array[i] = toStack ? pop() : this.acc;
|
|
712
|
+
break;
|
|
713
|
+
case 6:
|
|
714
|
+
case 7: {
|
|
715
|
+
const v = ((array[i] ?? 0) + (action === 6 ? 1 : -1)) & 0xffff;
|
|
716
|
+
array[i] = v;
|
|
717
|
+
if (toStack) this.push(v);
|
|
718
|
+
else this.acc = v;
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
switch (op) {
|
|
726
|
+
case Op.bnot: this.acc ^= 0xffff; break;
|
|
727
|
+
case Op.add: this.acc = arith(pop(), this.acc, (a, b) => a + b); break;
|
|
728
|
+
case Op.sub: this.acc = arith(pop(), this.acc, (a, b) => a - b); break;
|
|
729
|
+
case Op.mul: this.acc = (toSigned(pop()) * toSigned(this.acc)) & 0xffff; break;
|
|
730
|
+
case Op.div: {
|
|
731
|
+
const a = toSigned(pop()), b = toSigned(this.acc);
|
|
732
|
+
this.acc = b === 0 ? 0 : Math.trunc(a / b) & 0xffff;
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
case Op.mod: {
|
|
736
|
+
const a = toSigned(pop()), b = Math.abs(toSigned(this.acc));
|
|
737
|
+
let r = b === 0 ? 0 : a % b;
|
|
738
|
+
if (r < 0) r += b;
|
|
739
|
+
this.acc = r & 0xffff;
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
case Op.shr: this.acc = (pop() & 0xffff) >>> (this.acc & 0xf); break;
|
|
743
|
+
case Op.shl: this.acc = ((pop() & 0xffff) << (this.acc & 0xf)) & 0xffff; break;
|
|
744
|
+
case Op.xor: this.acc = (pop() ^ this.acc) & 0xffff; break;
|
|
745
|
+
case Op.and: this.acc = pop() & this.acc & 0xffff; break;
|
|
746
|
+
case Op.or: this.acc = (pop() | this.acc) & 0xffff; break;
|
|
747
|
+
case Op.neg: this.acc = -toSigned(this.acc) & 0xffff; break;
|
|
748
|
+
case Op.not: this.acc = bool(this.acc === 0); break;
|
|
749
|
+
case Op.eq: this.prev = this.acc; this.acc = bool(pop() === this.prev); break;
|
|
750
|
+
case Op.ne: this.prev = this.acc; this.acc = bool(pop() !== this.prev); break;
|
|
751
|
+
case Op.gt: this.prev = this.acc; this.acc = bool(compare(pop(), this.prev, true) > 0); break;
|
|
752
|
+
case Op.ge: this.prev = this.acc; this.acc = bool(compare(pop(), this.prev, true) >= 0); break;
|
|
753
|
+
case Op.lt: this.prev = this.acc; this.acc = bool(compare(pop(), this.prev, true) < 0); break;
|
|
754
|
+
case Op.le: this.prev = this.acc; this.acc = bool(compare(pop(), this.prev, true) <= 0); break;
|
|
755
|
+
case Op.ugt: this.prev = this.acc; this.acc = bool(compare(pop(), this.prev, false) > 0); break;
|
|
756
|
+
case Op.uge: this.prev = this.acc; this.acc = bool(compare(pop(), this.prev, false) >= 0); break;
|
|
757
|
+
case Op.ult: this.prev = this.acc; this.acc = bool(compare(pop(), this.prev, false) < 0); break;
|
|
758
|
+
case Op.ule: this.prev = this.acc; this.acc = bool(compare(pop(), this.prev, false) <= 0); break;
|
|
759
|
+
case Op.bt: { const d = sword(); if (this.acc !== 0) pc += d; break; }
|
|
760
|
+
case Op.bnt: { const d = sword(); if (this.acc === 0) pc += d; break; }
|
|
761
|
+
case Op.jmp: { const d = sword(); pc += d; break; }
|
|
762
|
+
case Op.ldi: this.acc = sword() & 0xffff; break;
|
|
763
|
+
case Op.push: this.push(this.acc); break;
|
|
764
|
+
case Op.pushi: this.push(sword() & 0xffff); break;
|
|
765
|
+
case Op.toss: this.sp--; break;
|
|
766
|
+
case Op.dup: this.push(stack[this.sp - 1]!); break;
|
|
767
|
+
case Op.link: { const count = word(); for (let i = 0; i < count; i++) this.push(0); break; }
|
|
768
|
+
|
|
769
|
+
case Op.call: {
|
|
770
|
+
const d = sword();
|
|
771
|
+
const frame = u16();
|
|
772
|
+
f.pc = pc;
|
|
773
|
+
this.callLocal(f.script, pc + d, frame, f.self);
|
|
774
|
+
reload();
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
case Op.callk: {
|
|
778
|
+
const k = word();
|
|
779
|
+
const frame = u16();
|
|
780
|
+
f.pc = pc;
|
|
781
|
+
this.callKernel(k, frame);
|
|
782
|
+
if (this.frames[this.frames.length - 1] !== f) reload(); // kernel could have changed frames
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
case Op.callb:
|
|
786
|
+
case Op.calle: {
|
|
787
|
+
const scriptNumber = op === Op.callb ? 0 : word();
|
|
788
|
+
const index = word();
|
|
789
|
+
const frame = u16();
|
|
790
|
+
f.pc = pc;
|
|
791
|
+
const target = this.loadScript(scriptNumber);
|
|
792
|
+
const e = target.script.exports[index];
|
|
793
|
+
if (!e || e.kind !== "code") throw new VmError(`script ${scriptNumber} export ${index} is not code`);
|
|
794
|
+
this.callLocal(target, e.offset, frame, f.self);
|
|
795
|
+
reload();
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
case Op.ret: {
|
|
799
|
+
const done = this.frames.pop()!;
|
|
800
|
+
if (this.disposed.size) this.unloadDisposed();
|
|
801
|
+
if (done.stopOnReturn) {
|
|
802
|
+
this.sp = done.restoreSp;
|
|
803
|
+
if (this.frames.length <= stopDepth) return;
|
|
804
|
+
reload();
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
const caller = this.frames[this.frames.length - 1];
|
|
808
|
+
if (!caller) return;
|
|
809
|
+
if (caller.send) this.continueSend(caller);
|
|
810
|
+
else this.sp = done.restoreSp;
|
|
811
|
+
if (this.frames.length <= stopDepth) return;
|
|
812
|
+
reload();
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
case Op.send: {
|
|
816
|
+
const frame = word();
|
|
817
|
+
f.pc = pc;
|
|
818
|
+
this.beginSend(f, this.object(this.acc), frame);
|
|
819
|
+
reload();
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
case Op.class_: this.acc = this.classObject(word()).address; break;
|
|
823
|
+
case Op.self_: {
|
|
824
|
+
const frame = word();
|
|
825
|
+
f.pc = pc;
|
|
826
|
+
this.beginSend(f, f.self!, frame);
|
|
827
|
+
reload();
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
case Op.super_: {
|
|
831
|
+
const cls = word();
|
|
832
|
+
const frame = u16();
|
|
833
|
+
f.pc = pc;
|
|
834
|
+
this.beginSend(f, f.self!, frame, cls);
|
|
835
|
+
reload();
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
case Op.rest: {
|
|
839
|
+
const from = word();
|
|
840
|
+
const argc = stack[f.paramBase]!;
|
|
841
|
+
this.rest = Math.max(argc - from + 1, 0);
|
|
842
|
+
for (let i = from; i <= argc; i++) this.push(stack[f.paramBase + i]!);
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
case Op.lea: {
|
|
846
|
+
const type = word() >> 1;
|
|
847
|
+
let index = word();
|
|
848
|
+
if (type & 0x08) index += toSigned(this.acc);
|
|
849
|
+
const kind = type & 3;
|
|
850
|
+
if (kind === 0) this.acc = makeRef(this.localsSegment(this.scripts.get(0)!), index * 2);
|
|
851
|
+
else if (kind === 1) this.acc = makeRef(this.localsSegment(f.script), index * 2);
|
|
852
|
+
else this.acc = makeRef(this.memory.stackSegment, ((kind === 2 ? f.tempBase : f.paramBase) + index) * 2);
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
case Op.selfID: this.acc = f.self?.address ?? NULL; break;
|
|
856
|
+
case Op.pprev: this.push(this.prev); break;
|
|
857
|
+
case Op.pToa: this.acc = f.self!.vars[word() >> 1]!; break;
|
|
858
|
+
case Op.aTop: this.writeVar(f.self!, word() >> 1, this.acc); break;
|
|
859
|
+
case Op.pTos: this.push(f.self!.vars[word() >> 1]!); break;
|
|
860
|
+
case Op.sTop: this.writeVar(f.self!, word() >> 1, pop()); break;
|
|
861
|
+
case Op.ipToa: case Op.dpToa: case Op.ipTos: case Op.dpTos: {
|
|
862
|
+
const i = word() >> 1;
|
|
863
|
+
const v = (f.self!.vars[i]! + (op === Op.ipToa || op === Op.ipTos ? 1 : -1)) & 0xffff;
|
|
864
|
+
this.writeVar(f.self!, i, v);
|
|
865
|
+
if (op === Op.ipTos || op === Op.dpTos) this.push(v);
|
|
866
|
+
else this.acc = v;
|
|
867
|
+
break;
|
|
868
|
+
}
|
|
869
|
+
case Op.lofsa: this.acc = makeRef(f.script.segment, word()); break;
|
|
870
|
+
case Op.lofss: this.push(makeRef(f.script.segment, word())); break;
|
|
871
|
+
case Op.push0: this.push(0); break;
|
|
872
|
+
case Op.push1: this.push(1); break;
|
|
873
|
+
case Op.push2: this.push(2); break;
|
|
874
|
+
case Op.pushSelf: this.push(f.self?.address ?? NULL); break;
|
|
875
|
+
default:
|
|
876
|
+
throw new VmError(`Invalid opcode 0x${raw.toString(16)}`);
|
|
877
|
+
}
|
|
878
|
+
} catch (e) {
|
|
879
|
+
if (e instanceof VmError && !e.message.includes(" @ ")) {
|
|
880
|
+
e.message += ` @ script ${f.script.number} pc ${insAt.toString(16)} in ${f.self?.name ?? "procedure"}`;
|
|
881
|
+
}
|
|
882
|
+
throw e;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (this.frames.length) this.frames[this.frames.length - 1]!.pc = pc;
|
|
886
|
+
} finally {
|
|
887
|
+
this.instructions += n;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/** Human-readable call stack, for errors and debugging. */
|
|
892
|
+
backtrace(): string[] {
|
|
893
|
+
return this.frames.filter((f) => f.pc >= 0).map((f) => `script ${f.script.number} pc ${f.pc.toString(16)} self ${f.self?.name ?? "-"}`).reverse();
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
isScriptLoaded(n: number): boolean {
|
|
897
|
+
return this.scripts.has(n);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/** Arithmetic on numbers, or pointer ± number / pointer − pointer within one segment. */
|
|
902
|
+
function arith(a: Value, b: Value, fn: (x: number, y: number) => number): Value {
|
|
903
|
+
if (isNumber(a) && isNumber(b)) return fn(a, b) & 0xffff;
|
|
904
|
+
if (!isNumber(a) && isNumber(b)) return makeRef(segmentOf(a), fn(offsetOf(a), toSigned(b)));
|
|
905
|
+
if (isNumber(a) && !isNumber(b)) return makeRef(segmentOf(b), fn(toSigned(a), offsetOf(b)));
|
|
906
|
+
if (segmentOf(a) === segmentOf(b)) return fn(offsetOf(a), offsetOf(b)) & 0xffff;
|
|
907
|
+
return fn(a, b) & 0xffff;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function compare(a: Value, b: Value, signed: boolean): number {
|
|
911
|
+
if (isNumber(a) && isNumber(b)) return signed ? toSigned(a) - toSigned(b) : a - b;
|
|
912
|
+
if (segmentOf(a) === segmentOf(b)) return offsetOf(a) - offsetOf(b);
|
|
913
|
+
return a - b;
|
|
914
|
+
}
|