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,279 @@
|
|
|
1
|
+
import { disassemble, formatDisassembly, type ClassInfo, type ObjectInfo, type PropValue } from "../../../../packages/sci/src/index.ts";
|
|
2
|
+
import { editorLink, el, showValue, type Explorer } from "./context.ts";
|
|
3
|
+
|
|
4
|
+
const link = (ex: Explorer, route: string, text: string) => el("a", { href: ex.href(route) }, text);
|
|
5
|
+
const roomLink = (ex: Explorer, n: number) => {
|
|
6
|
+
const room = ex.index.rooms.find((r) => r.number === n);
|
|
7
|
+
return link(ex, `room/${n}`, room ? `${n} ${room.name}` : `${n}`);
|
|
8
|
+
};
|
|
9
|
+
const scriptLink = (ex: Explorer, n: number) =>
|
|
10
|
+
ex.index.rooms.some((r) => r.number === n) ? roomLink(ex, n) : link(ex, `script/${n}`, `script ${n}`);
|
|
11
|
+
const classBySpecies = (ex: Explorer, species: number) => ex.index.classes.find((c) => c.species === species);
|
|
12
|
+
const classLink = (ex: Explorer, species: number) => {
|
|
13
|
+
const c = classBySpecies(ex, species);
|
|
14
|
+
return c ? link(ex, `class/${c.species}`, c.name) : el("span", {}, `class ${species}`);
|
|
15
|
+
};
|
|
16
|
+
const objectLink = (ex: Explorer, o: ObjectInfo) => link(ex, `object/${o.script}/${o.offset}`, o.name);
|
|
17
|
+
const prop = (o: { props: PropValue[] }, name: string) => o.props.find((p) => p.name === name)?.value;
|
|
18
|
+
|
|
19
|
+
/** Root first. */
|
|
20
|
+
function ancestry(ex: Explorer, species: number): ClassInfo[] {
|
|
21
|
+
const chain: ClassInfo[] = [];
|
|
22
|
+
for (let c = classBySpecies(ex, species); c && chain.length < 64; c = c.superclass < 0 ? undefined : classBySpecies(ex, c.superclass)) chain.unshift(c);
|
|
23
|
+
return chain;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const breadcrumb = (ex: Explorer, species: number) => {
|
|
27
|
+
const chain = ancestry(ex, species);
|
|
28
|
+
const out = el("p", { className: "meta" });
|
|
29
|
+
chain.forEach((c, i) => out.append(...(i ? [" → "] : []), link(ex, `class/${c.species}`, c.name)));
|
|
30
|
+
return out;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Lazily disassembled listing of a script, optionally only one object's methods. */
|
|
34
|
+
function disassembly(ex: Explorer, script: number, title: string, owner?: number): HTMLElement {
|
|
35
|
+
const pre = el("pre", { className: "asm" }, "…");
|
|
36
|
+
const details = el("details", {}, el("summary", {}, title), pre);
|
|
37
|
+
details.addEventListener("toggle", async () => {
|
|
38
|
+
if (!details.open || pre.dataset.done) return;
|
|
39
|
+
pre.dataset.done = "1";
|
|
40
|
+
const s = await ex.world.script(script);
|
|
41
|
+
let fns = disassemble(s, ex.world.context());
|
|
42
|
+
if (owner !== undefined) fns = fns.filter((f) => f.owner?.offset === owner);
|
|
43
|
+
pre.textContent = formatDisassembly(s, fns);
|
|
44
|
+
}, { once: false });
|
|
45
|
+
return details;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Messages for one module, optionally only for some nouns; grouped by noun. */
|
|
49
|
+
async function messageTable(ex: Explorer, module: number, nouns?: Set<number>, objects: ObjectInfo[] = []): Promise<HTMLElement | undefined> {
|
|
50
|
+
const file = await ex.messages(module);
|
|
51
|
+
if (!file) return undefined;
|
|
52
|
+
const records = file.records.filter((r) => !nouns || nouns.has(r.noun));
|
|
53
|
+
if (!records.length) return undefined;
|
|
54
|
+
const namesByNoun = new Map<number, string[]>();
|
|
55
|
+
for (const o of objects) {
|
|
56
|
+
const noun = prop(o, "noun");
|
|
57
|
+
if (noun) namesByNoun.set(noun, [...(namesByNoun.get(noun) ?? []), o.name]);
|
|
58
|
+
}
|
|
59
|
+
const verb = (v: number) => (v ? ex.index.verbs[v] ?? `verb ${v}` : "—");
|
|
60
|
+
const table = el("table", {}, el("tr", {}, el("th", {}, "noun"), el("th", {}, "verb"), el("th", {}, "cond"), el("th", {}, "seq"), el("th", {}, "text")));
|
|
61
|
+
for (const r of [...records].sort((a, b) => a.noun - b.noun || a.verb - b.verb || a.cond - b.cond || a.seq - b.seq)) {
|
|
62
|
+
const who = namesByNoun.get(r.noun)?.join(", ");
|
|
63
|
+
table.append(el("tr", {},
|
|
64
|
+
el("td", { className: "num", title: who ?? "" }, `${r.noun}${who ? ` ${who}` : ""}`),
|
|
65
|
+
el("td", { className: "num" }, verb(r.verb)),
|
|
66
|
+
el("td", { className: "num" }, `${r.cond}`),
|
|
67
|
+
el("td", { className: "num" }, `${r.seq}`),
|
|
68
|
+
el("td", {}, r.text || el("span", { className: "meta" }, "(empty)")),
|
|
69
|
+
));
|
|
70
|
+
}
|
|
71
|
+
return el("details", { open: records.length <= 40 }, el("summary", {}, `Messages (${records.length})`), table);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The room picture with static object positions drawn on top. */
|
|
75
|
+
async function roomPicture(ex: Explorer, picture: number, objects: ObjectInfo[]): Promise<{ element: HTMLElement; highlight(o?: ObjectInfo): void } | undefined> {
|
|
76
|
+
const canvas = await ex.pic(picture);
|
|
77
|
+
if (!canvas) return undefined;
|
|
78
|
+
const copy = el("canvas", { width: canvas.width, height: canvas.height });
|
|
79
|
+
copy.getContext("2d")!.drawImage(canvas, 0, 0);
|
|
80
|
+
const box = el("div", { className: "pic" }, copy);
|
|
81
|
+
box.style.aspectRatio = `${canvas.width} / ${canvas.height * 1.2}`;
|
|
82
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
83
|
+
const svg = document.createElementNS(NS, "svg");
|
|
84
|
+
svg.setAttribute("viewBox", `0 0 ${canvas.width} ${canvas.height}`);
|
|
85
|
+
svg.setAttribute("preserveAspectRatio", "none");
|
|
86
|
+
const marks = new Map<ObjectInfo, SVGGElement>();
|
|
87
|
+
for (const o of objects) {
|
|
88
|
+
const [l = 0, t = 0, r = 0, b = 0] = ["nsLeft", "nsTop", "nsRight", "nsBottom"].map((n) => prop(o, n) ?? 0);
|
|
89
|
+
const x = prop(o, "x") ?? 0, y = prop(o, "y") ?? 0;
|
|
90
|
+
const g = document.createElementNS(NS, "g");
|
|
91
|
+
const title = document.createElementNS(NS, "title");
|
|
92
|
+
title.textContent = `${o.name} (${o.className})`;
|
|
93
|
+
g.append(title);
|
|
94
|
+
if (r > l && b > t && r < 0x8000) {
|
|
95
|
+
const rect = document.createElementNS(NS, "rect");
|
|
96
|
+
Object.entries({ x: l, y: t, width: r - l, height: b - t }).forEach(([k, v]) => rect.setAttribute(k, String(v)));
|
|
97
|
+
g.append(rect);
|
|
98
|
+
} else if ((x || y) && x < 0x8000 && y < 0x8000) {
|
|
99
|
+
const dot = document.createElementNS(NS, "circle");
|
|
100
|
+
Object.entries({ cx: x, cy: y, r: 2.5 }).forEach(([k, v]) => dot.setAttribute(k, String(v)));
|
|
101
|
+
g.append(dot);
|
|
102
|
+
} else continue;
|
|
103
|
+
const label = document.createElementNS(NS, "text");
|
|
104
|
+
label.setAttribute("x", String(r > l ? l + 2 : x + 4));
|
|
105
|
+
label.setAttribute("y", String(r > l ? t + 7 : y + 2));
|
|
106
|
+
label.textContent = o.name;
|
|
107
|
+
g.append(label);
|
|
108
|
+
svg.append(g);
|
|
109
|
+
marks.set(o, g);
|
|
110
|
+
}
|
|
111
|
+
box.append(svg);
|
|
112
|
+
return {
|
|
113
|
+
element: box,
|
|
114
|
+
highlight(o) {
|
|
115
|
+
for (const [obj, g] of marks) g.classList.toggle("hl", obj === o);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function exitList(ex: Explorer, room: number): HTMLElement {
|
|
121
|
+
const out = ex.index.exits.filter((e) => e.from === room);
|
|
122
|
+
const into = ex.index.exits.filter((e) => e.to === room);
|
|
123
|
+
const rows = (list: typeof out, other: (e: (typeof out)[number]) => number) => {
|
|
124
|
+
const table = el("table");
|
|
125
|
+
for (const e of [...list].sort((a, b) => other(a) - other(b))) {
|
|
126
|
+
table.append(el("tr", {},
|
|
127
|
+
el("td", {}, roomLink(ex, other(e))),
|
|
128
|
+
el("td", {}, el("span", { className: `kind ${e.kind}` }, e.kind)),
|
|
129
|
+
el("td", { className: "num" }, e.script === room ? e.where : el("span", {}, scriptLink(ex, e.script), ` ${e.where}`)),
|
|
130
|
+
));
|
|
131
|
+
}
|
|
132
|
+
return list.length ? table : el("p", { className: "meta" }, "none found");
|
|
133
|
+
};
|
|
134
|
+
const unresolved = ex.index.unresolved.filter((e) => e.from === room);
|
|
135
|
+
return el("div", {},
|
|
136
|
+
el("h3", {}, `Leads to (${new Set(out.map((e) => e.to)).size})`), rows(out, (e) => e.to),
|
|
137
|
+
el("h3", {}, `Entered from (${new Set(into.map((e) => e.from)).size})`), rows(into, (e) => e.from),
|
|
138
|
+
unresolved.length ? el("p", { className: "meta" }, `Also ${unresolved.map((e) => `${e.where} = ${e.to}`).join(", ")}: not room numbers (maze codes or cut rooms).`) : null,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function objectTable(ex: Explorer, objects: ObjectInfo[], onHover?: (o?: ObjectInfo) => void): HTMLElement {
|
|
143
|
+
const table = el("table", {}, el("tr", {}, el("th", {}, "object"), el("th", {}, "class"), el("th", {}, "noun"), el("th", {}, "methods")));
|
|
144
|
+
for (const o of objects) {
|
|
145
|
+
const row = el("tr", {},
|
|
146
|
+
el("td", {}, objectLink(ex, o)),
|
|
147
|
+
el("td", {}, classLink(ex, o.superclass)),
|
|
148
|
+
el("td", { className: "num" }, `${prop(o, "noun") || ""}`),
|
|
149
|
+
el("td", { className: "num" }, o.methods.join(" ")),
|
|
150
|
+
);
|
|
151
|
+
if (onHover) {
|
|
152
|
+
row.addEventListener("mouseenter", () => { row.classList.add("hl"); onHover(o); });
|
|
153
|
+
row.addEventListener("mouseleave", () => { row.classList.remove("hl"); onHover(undefined); });
|
|
154
|
+
}
|
|
155
|
+
table.append(row);
|
|
156
|
+
}
|
|
157
|
+
return table;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function renderRoom(ex: Explorer, n: number): Promise<HTMLElement> {
|
|
161
|
+
const room = ex.index.rooms.find((r) => r.number === n);
|
|
162
|
+
const info = ex.index.scripts.find((s) => s.number === n);
|
|
163
|
+
if (!room || !info) return el("p", { className: "error" }, `No room ${n}`);
|
|
164
|
+
const helpers = room.uses.filter((u) => !ex.index.rooms.some((r) => r.number === u));
|
|
165
|
+
const helperObjects = ex.index.scripts.filter((s) => helpers.includes(s.number) && s.number >= 100).flatMap((s) => s.objects);
|
|
166
|
+
const pic = room.picture >= 0 ? await roomPicture(ex, room.picture, info.objects) : undefined;
|
|
167
|
+
|
|
168
|
+
const roomObject = info.objects.find((o) => o.name === room.name)!;
|
|
169
|
+
const messages = await messageTable(ex, n, undefined, [...info.objects, ...helperObjects]);
|
|
170
|
+
return el("div", {},
|
|
171
|
+
el("h2", {}, `Room ${n} · ${room.name} `, editorLink(n, ex.mods)),
|
|
172
|
+
breadcrumb(ex, roomObject.superclass),
|
|
173
|
+
el("p", { className: "meta" }, `picture ${room.picture} · noun ${room.noun} · ${info.objects.length} objects`),
|
|
174
|
+
room.uses.length ? el("div", { className: "chips" }, el("span", { className: "meta" }, "uses"), ...room.uses.map((u) => scriptLink(ex, u))) : null,
|
|
175
|
+
pic?.element,
|
|
176
|
+
exitList(ex, n),
|
|
177
|
+
el("h3", {}, `Objects (${info.objects.length})`),
|
|
178
|
+
el("p", { className: "meta" }, "Boxes and dots are positions set in the object's declaration; objects placed by code at run time don't show."),
|
|
179
|
+
objectTable(ex, info.objects, pic?.highlight),
|
|
180
|
+
messages ?? el("p", { className: "meta" }, "No messages."),
|
|
181
|
+
disassembly(ex, n, "Disassembly"),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function renderScript(ex: Explorer, n: number): Promise<HTMLElement> {
|
|
186
|
+
const info = ex.index.scripts.find((s) => s.number === n);
|
|
187
|
+
if (!info) return el("p", { className: "error" }, `No script ${n}`);
|
|
188
|
+
const users = ex.index.scripts.filter((s) => s.references.includes(n)).map((s) => s.number);
|
|
189
|
+
const classes = ex.index.classes.filter((c) => c.script === n);
|
|
190
|
+
const messages = await messageTable(ex, n, undefined, info.objects);
|
|
191
|
+
return el("div", {},
|
|
192
|
+
el("h2", {}, `Script ${n}`),
|
|
193
|
+
users.length ? el("div", { className: "chips" }, el("span", { className: "meta" }, "used by"), ...users.map((u) => scriptLink(ex, u))) : null,
|
|
194
|
+
info.references.length ? el("div", { className: "chips" }, el("span", { className: "meta" }, "uses"), ...info.references.map((u) => scriptLink(ex, u))) : null,
|
|
195
|
+
classes.length ? el("div", {}, el("h3", {}, `Classes (${classes.length})`), el("div", { className: "chips" }, ...classes.map((c) => classLink(ex, c.species)))) : null,
|
|
196
|
+
el("h3", {}, `Objects (${info.objects.length})`),
|
|
197
|
+
objectTable(ex, info.objects),
|
|
198
|
+
messages,
|
|
199
|
+
disassembly(ex, n, "Disassembly"),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Props with where each value comes from: the class's default or this object's own. */
|
|
204
|
+
function propTable(props: PropValue[], defaults: PropValue[] | undefined): HTMLElement {
|
|
205
|
+
const table = el("table", {}, el("tr", {}, el("th", {}, "property"), el("th", {}, "value"), el("th", {}, defaults ? "class default" : "")));
|
|
206
|
+
for (const p of props) {
|
|
207
|
+
const d = defaults?.find((x) => x.name === p.name);
|
|
208
|
+
const changed = !!d && d.value !== p.value;
|
|
209
|
+
const isNew = defaults && !d;
|
|
210
|
+
table.append(el("tr", {},
|
|
211
|
+
el("td", { className: changed || isNew ? "prop-changed" : "prop-inherited" }, p.name),
|
|
212
|
+
el("td", { className: changed || isNew ? "prop-changed" : "" }, showValue(p.value)),
|
|
213
|
+
el("td", { className: "num" }, d ? showValue(d.value) : isNew ? "new" : ""),
|
|
214
|
+
));
|
|
215
|
+
}
|
|
216
|
+
return table;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Each method name with the nearest ancestor that also defines it (so this overrides it). */
|
|
220
|
+
function methodList(ex: Explorer, methods: string[], superclass: number): HTMLElement {
|
|
221
|
+
const chain = superclass < 0 ? [] : ancestry(ex, superclass).reverse();
|
|
222
|
+
const list = el("div", { className: "chips" });
|
|
223
|
+
for (const m of methods) {
|
|
224
|
+
const from = chain.find((c) => c.methods.includes(m));
|
|
225
|
+
list.append(el("span", { title: from ? `overrides ${from.name}::${m}` : "new" }, from ? `${m}` : el("strong", {}, m)));
|
|
226
|
+
}
|
|
227
|
+
return methods.length ? list : el("p", { className: "meta" }, "none");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function renderObject(ex: Explorer, script: number, offset: number): Promise<HTMLElement> {
|
|
231
|
+
const o = ex.object(script, offset);
|
|
232
|
+
if (!o) return el("p", { className: "error" }, "No such object");
|
|
233
|
+
const cls = classBySpecies(ex, o.superclass);
|
|
234
|
+
const noun = prop(o, "noun");
|
|
235
|
+
const siblings = ex.index.scripts.find((s) => s.number === script)?.objects ?? [];
|
|
236
|
+
const messages = noun ? await messageTable(ex, script, new Set([noun]), siblings) : undefined;
|
|
237
|
+
return el("div", {},
|
|
238
|
+
el("h2", {}, o.name),
|
|
239
|
+
el("p", { className: "meta" }, "instance in ", scriptLink(ex, script)),
|
|
240
|
+
breadcrumb(ex, o.superclass),
|
|
241
|
+
el("h3", {}, "Methods (bold = new, others override the class)"),
|
|
242
|
+
methodList(ex, o.methods, o.superclass),
|
|
243
|
+
messages ?? null,
|
|
244
|
+
el("h3", {}, "Properties"),
|
|
245
|
+
propTable(o.props, cls?.props),
|
|
246
|
+
disassembly(ex, script, "Disassembly of its methods", offset),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function renderClass(ex: Explorer, species: number): HTMLElement {
|
|
251
|
+
const c = classBySpecies(ex, species);
|
|
252
|
+
if (!c) return el("p", { className: "error" }, `No class ${species}`);
|
|
253
|
+
const sup = c.superclass < 0 ? undefined : classBySpecies(ex, c.superclass);
|
|
254
|
+
const subclasses = ex.index.classes.filter((x) => x.superclass === species);
|
|
255
|
+
const instances = ex.index.scripts.flatMap((s) => s.objects.filter((o) => o.superclass === species));
|
|
256
|
+
const inherited = ancestry(ex, species).slice(0, -1).reverse();
|
|
257
|
+
const shown = instances.slice(0, 300);
|
|
258
|
+
return el("div", {},
|
|
259
|
+
el("h2", {}, c.name),
|
|
260
|
+
el("p", { className: "meta" }, `class #${c.species} in `, scriptLink(ex, c.script)),
|
|
261
|
+
breadcrumb(ex, species),
|
|
262
|
+
subclasses.length ? el("div", {}, el("h3", {}, `Subclasses (${subclasses.length})`), el("div", { className: "chips" }, ...subclasses.map((s) => classLink(ex, s.species)))) : null,
|
|
263
|
+
el("h3", {}, "Methods (bold = new, others override an ancestor)"),
|
|
264
|
+
methodList(ex, c.methods, c.superclass),
|
|
265
|
+
inherited.length ? el("details", {}, el("summary", { className: "meta" }, "Inherited methods"),
|
|
266
|
+
...inherited.map((a) => el("p", {}, link(ex, `class/${a.species}`, a.name), el("span", { className: "meta" }, `: ${a.methods.filter((m) => !c.methods.includes(m)).join(" ")}`)))) : null,
|
|
267
|
+
el("h3", {}, "Properties"),
|
|
268
|
+
propTable(c.props, sup?.props),
|
|
269
|
+
el("h3", {}, `Direct instances (${instances.length})`),
|
|
270
|
+
instances.length ? el("div", { className: "chips" }, ...shown.map((o) => el("a", { href: ex.href(`object/${o.script}/${o.offset}`), title: `script ${o.script}` }, `${o.name}`))) : el("p", { className: "meta" }, "none"),
|
|
271
|
+
instances.length > shown.length ? el("p", { className: "meta" }, `…and ${instances.length - shown.length} more`) : null,
|
|
272
|
+
disassembly(ex, c.script, "Disassembly of its methods", ex.index.scripts.length ? classOffset(ex, c) : undefined),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Classes aren't in `scripts[].objects` (instances only): find the heap offset from the world. */
|
|
277
|
+
function classOffset(ex: Explorer, c: ClassInfo): number | undefined {
|
|
278
|
+
return ex.world.classObject(c.species)?.offset;
|
|
279
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Explorer } from "./context.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The live view: the game running in the explorer, with overlays of what the interpreter
|
|
4
|
+
* knows (sprites, clickable features, walk polygons, paths) and inspectors that follow
|
|
5
|
+
* objects' properties as they change.
|
|
6
|
+
*/
|
|
7
|
+
export interface LiveView {
|
|
8
|
+
element: HTMLElement;
|
|
9
|
+
/** Starts the game on first use; resumes if the view paused it on leaving. */
|
|
10
|
+
activate(): Promise<void>;
|
|
11
|
+
/** Pauses the game (and its audio) while another tab is shown. */
|
|
12
|
+
deactivate(): void;
|
|
13
|
+
/** Inspector panel for the running room, refreshed while shown. */
|
|
14
|
+
roomPanel(): LivePanel;
|
|
15
|
+
/** Inspector panel for one live object by address. */
|
|
16
|
+
objectPanel(address: number): LivePanel;
|
|
17
|
+
}
|
|
18
|
+
export interface LivePanel {
|
|
19
|
+
element: HTMLElement;
|
|
20
|
+
refresh(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function createLiveView(ex: Explorer, inspect: (address: number) => void): LiveView;
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { collectionElements, drawnItems, findMessage, gameGlobal, isKindOf, namedObject, objectAt, ResourceType, heroInPlay, polygonOf, requestRoom, roomPlane, signedProp, } from "../../../../packages/sci/src/index.js";
|
|
2
|
+
import { createOverlay, drawRoomOverlay } from "../overlay.js";
|
|
3
|
+
import { GameSession } from "../session.js";
|
|
4
|
+
import { editorUrl, el, showValue } from "./context.js";
|
|
5
|
+
// --- The view -------------------------------------------------------------------------
|
|
6
|
+
export function createLiveView(ex, inspect) {
|
|
7
|
+
const canvas = el("canvas", { width: 320, height: 200 });
|
|
8
|
+
const overlay = createOverlay();
|
|
9
|
+
const screen = el("div", { className: "live-screen" }, canvas);
|
|
10
|
+
screen.append(overlay);
|
|
11
|
+
const toggles = {
|
|
12
|
+
sprites: toggle("Sprites", true),
|
|
13
|
+
features: toggle("Features", false),
|
|
14
|
+
polygons: toggle("Polygons", true, "Walk polygons: green total access, yellow near point, red barred, blue contained"),
|
|
15
|
+
paths: toggle("Paths", true),
|
|
16
|
+
};
|
|
17
|
+
const inspectMode = toggle("Inspect clicks", false);
|
|
18
|
+
const pauseButton = el("button", { type: "button" }, "Pause");
|
|
19
|
+
const stepButton = el("button", { type: "button", disabled: true }, "Step");
|
|
20
|
+
const soundButton = el("button", { type: "button" }, "Sound: off");
|
|
21
|
+
const snapButton = el("button", { type: "button" }, "Snapshot");
|
|
22
|
+
const restoreButton = el("button", { type: "button", disabled: true }, "Restore");
|
|
23
|
+
const info = el("span", { className: "meta" }, "Starting…");
|
|
24
|
+
// Jump to any room once a hero is in play (the game changes rooms itself: global 13).
|
|
25
|
+
const roomInput = el("input", { type: "number", placeholder: "room", min: "1", max: "999" });
|
|
26
|
+
roomInput.style.width = "5.5em";
|
|
27
|
+
const goButton = el("button", { type: "button", title: "Rooms may expect story state (flags, items); ?debug adds Sierra's debug keys" }, "Teleport");
|
|
28
|
+
goButton.addEventListener("click", () => {
|
|
29
|
+
const room = Number(roomInput.value);
|
|
30
|
+
if (!session || !room)
|
|
31
|
+
return;
|
|
32
|
+
if (!heroInPlay(session.vm))
|
|
33
|
+
info.textContent = "Create a hero (or restore a save) first";
|
|
34
|
+
else if (!ex.rm.has({ type: ResourceType.Script, number: room }))
|
|
35
|
+
info.textContent = `No room ${room}`;
|
|
36
|
+
else
|
|
37
|
+
requestRoom(session.vm, room);
|
|
38
|
+
});
|
|
39
|
+
// The room the game is in, in the room editor (when a mod describes it in YAML).
|
|
40
|
+
const editLink = el("a", { className: "edit-link", target: "_blank", hidden: true, title: "Open this room in the room editor" });
|
|
41
|
+
let editRoom;
|
|
42
|
+
const errorBox = el("pre", { className: "error", hidden: true });
|
|
43
|
+
const element = el("div", { className: "live" }, el("div", { className: "live-bar" }, pauseButton, stepButton, soundButton, snapButton, restoreButton, roomInput, goButton, info, editLink), el("div", { className: "live-bar" }, ...Object.values(toggles).map((t) => t.label), inspectMode.label, el("span", { className: "meta" }, "Alt+click also inspects")), screen, errorBox);
|
|
44
|
+
let session;
|
|
45
|
+
let starting;
|
|
46
|
+
let pausedByTab = false;
|
|
47
|
+
/** The object shown in the inspector, outlined on the overlay. */
|
|
48
|
+
let selected;
|
|
49
|
+
let quickSave;
|
|
50
|
+
const log = [];
|
|
51
|
+
const rooms = [];
|
|
52
|
+
function toggle(name, on, title = "") {
|
|
53
|
+
const input = el("input", { type: "checkbox", checked: on });
|
|
54
|
+
return { input, label: el("label", { title }, input, ` ${name}`) };
|
|
55
|
+
}
|
|
56
|
+
const setPaused = (paused) => {
|
|
57
|
+
if (!session)
|
|
58
|
+
return;
|
|
59
|
+
session.paused = paused;
|
|
60
|
+
pauseButton.textContent = paused ? "Resume" : "Pause";
|
|
61
|
+
stepButton.disabled = !paused;
|
|
62
|
+
};
|
|
63
|
+
pauseButton.addEventListener("click", () => setPaused(!session?.paused));
|
|
64
|
+
stepButton.addEventListener("click", () => session?.step(1));
|
|
65
|
+
soundButton.addEventListener("click", () => {
|
|
66
|
+
if (!session)
|
|
67
|
+
return;
|
|
68
|
+
session.setMuted(!session.muted);
|
|
69
|
+
soundButton.textContent = session.muted ? "Sound: off" : "Sound: on";
|
|
70
|
+
});
|
|
71
|
+
snapButton.addEventListener("click", () => {
|
|
72
|
+
if (!session)
|
|
73
|
+
return;
|
|
74
|
+
quickSave = session.snapshot();
|
|
75
|
+
restoreButton.disabled = false;
|
|
76
|
+
restoreButton.title = `Snapshot from room ${gameGlobal(session.vm, "curRoomNum")}, frame ${session.frames}`;
|
|
77
|
+
});
|
|
78
|
+
restoreButton.addEventListener("click", () => quickSave && session?.restore(quickSave));
|
|
79
|
+
/** What's under a screen point: the frontmost sprite, else a feature of the room. */
|
|
80
|
+
function hitTest(x, y) {
|
|
81
|
+
const vm = session.vm;
|
|
82
|
+
const hit = drawnItems(vm).find((d) => x >= d.x && x < d.x + d.w && y >= d.y && y < d.y + d.h);
|
|
83
|
+
if (hit)
|
|
84
|
+
return hit.item;
|
|
85
|
+
const plane = roomPlane(vm);
|
|
86
|
+
if (!plane)
|
|
87
|
+
return undefined;
|
|
88
|
+
const px = x - plane.left, py = y - plane.top;
|
|
89
|
+
return collectionElements(vm, namedObject(vm, "features")).find((f) => {
|
|
90
|
+
const check = objectAt(vm, vm.getProp(f, "onMeCheck"));
|
|
91
|
+
if (check && isKindOf(vm, check, "Polygon"))
|
|
92
|
+
return pointInPolygon(px, py, polygonOf(vm, check).points);
|
|
93
|
+
const [l, t, r, b] = ["nsLeft", "nsTop", "nsRight", "nsBottom"].map((n) => signedProp(vm, f, n));
|
|
94
|
+
return r > l && px >= l && px <= r && py >= t && py <= b;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function drawOverlay() {
|
|
98
|
+
if (!session)
|
|
99
|
+
return;
|
|
100
|
+
drawRoomOverlay(overlay, session.vm, {
|
|
101
|
+
sprites: toggles.sprites.input.checked,
|
|
102
|
+
features: toggles.features.input.checked,
|
|
103
|
+
polygons: toggles.polygons.input.checked,
|
|
104
|
+
paths: toggles.paths.input.checked,
|
|
105
|
+
selected,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
let lastInfo = 0;
|
|
109
|
+
async function start() {
|
|
110
|
+
session = await GameSession.create({
|
|
111
|
+
canvas,
|
|
112
|
+
rm: ex.rm,
|
|
113
|
+
mods: ex.mods,
|
|
114
|
+
muted: true,
|
|
115
|
+
runHidden: new URLSearchParams(location.search).has("background"),
|
|
116
|
+
debug: new URLSearchParams(location.search).has("debug"),
|
|
117
|
+
interceptClick: (x, y, e) => {
|
|
118
|
+
if (!inspectMode.input.checked && !e.altKey)
|
|
119
|
+
return false;
|
|
120
|
+
const hit = hitTest(x, y);
|
|
121
|
+
if (hit)
|
|
122
|
+
inspect(hit.address);
|
|
123
|
+
return true;
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const vm = session.vm;
|
|
127
|
+
// Log what the game says: messages fetched by a Messager (`nextMsg`), not every UI label.
|
|
128
|
+
let saying = false;
|
|
129
|
+
vm.onSend = (_object, selector) => {
|
|
130
|
+
if (selector === "nextMsg")
|
|
131
|
+
saying = true;
|
|
132
|
+
};
|
|
133
|
+
vm.onKernelCall = (name, args) => {
|
|
134
|
+
if (name === "Message" && args[0] === 0 && args.length === 7 && saying) {
|
|
135
|
+
saying = false;
|
|
136
|
+
const [, module = 0, noun = 0, verb = 0, cond = 0, seq = 0] = args;
|
|
137
|
+
const frame = session.frames;
|
|
138
|
+
void ex.messages(module).then((file) => {
|
|
139
|
+
const text = (file && findMessage(file, { noun, verb, cond, seq })?.text) || `${module}/${noun}/${verb}/${cond}/${seq}`;
|
|
140
|
+
log.push({ frame, text });
|
|
141
|
+
if (log.length > 30)
|
|
142
|
+
log.shift();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
session.onDraw.push(() => {
|
|
147
|
+
if (session.error) {
|
|
148
|
+
errorBox.hidden = false;
|
|
149
|
+
errorBox.textContent = `${session.error.message}\n${session.error.backtrace.map((l) => ` at ${l}`).join("\n")}`;
|
|
150
|
+
}
|
|
151
|
+
const room = gameGlobal(vm, "curRoomNum") ?? 0;
|
|
152
|
+
if (room && rooms.at(-1)?.room !== room)
|
|
153
|
+
rooms.push({ frame: session.frames, room });
|
|
154
|
+
drawOverlay();
|
|
155
|
+
const now = performance.now();
|
|
156
|
+
if (now - lastInfo > 500) {
|
|
157
|
+
lastInfo = now;
|
|
158
|
+
info.textContent = `room ${room} · frame ${session.frames} · ${session.paused ? "paused" : `${session.fps} fps`}`;
|
|
159
|
+
if (room !== editRoom) {
|
|
160
|
+
editRoom = room;
|
|
161
|
+
editLink.hidden = true;
|
|
162
|
+
if (room) {
|
|
163
|
+
void editorUrl(room, ex.mods).then((url) => {
|
|
164
|
+
if (!url || editRoom !== room)
|
|
165
|
+
return;
|
|
166
|
+
editLink.href = url;
|
|
167
|
+
editLink.textContent = `Edit room ${room}`;
|
|
168
|
+
editLink.hidden = false;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (activePanel?.element.isConnected)
|
|
173
|
+
activePanel.refresh();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
// Automated testing (?background) can reach the session from the console.
|
|
177
|
+
if (new URLSearchParams(location.search).has("background"))
|
|
178
|
+
globalThis.liveSession = session;
|
|
179
|
+
session.start();
|
|
180
|
+
}
|
|
181
|
+
let activePanel;
|
|
182
|
+
const track = (panel) => {
|
|
183
|
+
activePanel = panel;
|
|
184
|
+
panel.refresh();
|
|
185
|
+
return panel;
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
element,
|
|
189
|
+
async activate() {
|
|
190
|
+
await (starting ??= start());
|
|
191
|
+
if (pausedByTab)
|
|
192
|
+
setPaused(false);
|
|
193
|
+
pausedByTab = false;
|
|
194
|
+
},
|
|
195
|
+
deactivate() {
|
|
196
|
+
if (session && !session.paused) {
|
|
197
|
+
setPaused(true);
|
|
198
|
+
pausedByTab = true;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
roomPanel: () => {
|
|
202
|
+
selected = undefined;
|
|
203
|
+
return track(roomPanel(ex, () => session, log, rooms));
|
|
204
|
+
},
|
|
205
|
+
objectPanel: (address) => {
|
|
206
|
+
selected = address;
|
|
207
|
+
return track(objectPanel(ex, () => session, address));
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function pointInPolygon(x, y, points) {
|
|
212
|
+
let inside = false;
|
|
213
|
+
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
214
|
+
const [xi, yi] = points[i], [xj, yj] = points[j];
|
|
215
|
+
if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi)
|
|
216
|
+
inside = !inside;
|
|
217
|
+
}
|
|
218
|
+
return inside;
|
|
219
|
+
}
|
|
220
|
+
// --- Inspector panels ---------------------------------------------------------------
|
|
221
|
+
const liveHref = (ex, o) => ex.href(`live/${o.address.toString(16)}`);
|
|
222
|
+
const liveLink = (ex, o) => (o ? el("a", { href: liveHref(ex, o) }, o.name) : el("span", { className: "meta" }, "—"));
|
|
223
|
+
/** Where an object is defined, for a link to the static inspector. */
|
|
224
|
+
function staticRoute(o) {
|
|
225
|
+
if (o.isClass)
|
|
226
|
+
return `class/${o.species}`;
|
|
227
|
+
const def = o.isClone && o.template ? o.template : o;
|
|
228
|
+
return def.isClass ? `class/${def.species}` : `object/${def.script.number}/${def.address & 0xffff}`;
|
|
229
|
+
}
|
|
230
|
+
function roomPanel(ex, getSession, log, rooms) {
|
|
231
|
+
const element = el("div", {}, el("p", { className: "meta" }, "Starting the game…"));
|
|
232
|
+
return {
|
|
233
|
+
element,
|
|
234
|
+
refresh() {
|
|
235
|
+
const session = getSession();
|
|
236
|
+
if (!session)
|
|
237
|
+
return;
|
|
238
|
+
const vm = session.vm;
|
|
239
|
+
const room = objectAt(vm, gameGlobal(vm, "curRoom"));
|
|
240
|
+
const roomNum = gameGlobal(vm, "curRoomNum") ?? 0;
|
|
241
|
+
const ego = objectAt(vm, gameGlobal(vm, "ego"));
|
|
242
|
+
const scriptState = (o) => {
|
|
243
|
+
const s = objectAt(vm, vm.getProp(o, "script"));
|
|
244
|
+
return s ? el("span", {}, liveLink(ex, s), el("span", { className: "meta" }, ` state ${signedProp(vm, s, "state")}`)) : "";
|
|
245
|
+
};
|
|
246
|
+
const table = (list, extra) => {
|
|
247
|
+
const t = el("table");
|
|
248
|
+
for (const o of list)
|
|
249
|
+
t.append(el("tr", {}, el("td", {}, liveLink(ex, o)), ...extra(o).map((c) => el("td", { className: "num" }, c))));
|
|
250
|
+
return list.length ? t : el("p", { className: "meta" }, "none");
|
|
251
|
+
};
|
|
252
|
+
const cast = collectionElements(vm, namedObject(vm, "cast"));
|
|
253
|
+
const features = collectionElements(vm, namedObject(vm, "features"));
|
|
254
|
+
const regions = collectionElements(vm, namedObject(vm, "regions"));
|
|
255
|
+
element.replaceChildren(el("h2", {}, `Live: room ${roomNum}`), el("p", { className: "meta" }, room ? liveLink(ex, room) : "", " · ", el("a", { href: ex.href(`room/${roomNum}`) }, "static view"), room ? el("span", {}, " · script ", scriptState(room)) : ""), ego ? el("p", {}, "ego ", liveLink(ex, ego), el("span", { className: "meta" }, ` at ${signedProp(vm, ego, "x")},${signedProp(vm, ego, "y")} · view ${signedProp(vm, ego, "view")} loop ${signedProp(vm, ego, "loop")} cel ${signedProp(vm, ego, "cel")} `), scriptState(ego)) : "", el("h3", {}, `Cast (${cast.length})`), table(cast, (o) => [`${signedProp(vm, o, "x")},${signedProp(vm, o, "y")}`, `view ${signedProp(vm, o, "view")}`, scriptState(o)]), el("h3", {}, `Features (${features.length})`), table(features, (o) => [`noun ${signedProp(vm, o, "noun")}`]), el("h3", {}, `Regions (${regions.length})`), table(regions, (o) => [scriptState(o)]), el("h3", {}, "Messages"), log.length ? el("div", {}, ...[...log].reverse().map((m) => el("p", {}, el("span", { className: "meta" }, `${m.frame} `), m.text))) : el("p", { className: "meta" }, "none yet"), el("h3", {}, "Rooms visited"), el("div", { className: "chips" }, ...rooms.map((r) => el("a", { href: ex.href(`room/${r.room}`), title: `frame ${r.frame}` }, `${r.room}`))));
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/** Objects' header slots come first; the name pointer is the last of them. */
|
|
260
|
+
const NAME_SLOT = 8;
|
|
261
|
+
function objectPanel(ex, getSession, address) {
|
|
262
|
+
const element = el("div", {}, el("p", { className: "meta" }, "Starting the game…"));
|
|
263
|
+
const cells = new Map();
|
|
264
|
+
const values = new Map();
|
|
265
|
+
let built;
|
|
266
|
+
const render = (vm, v) => {
|
|
267
|
+
const o = v && v < 0x10000 ? undefined : objectAt(vm, v);
|
|
268
|
+
if (o)
|
|
269
|
+
return liveLink(ex, o);
|
|
270
|
+
return showValue(v & 0xffff);
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
element,
|
|
274
|
+
refresh() {
|
|
275
|
+
const session = getSession();
|
|
276
|
+
if (!session)
|
|
277
|
+
return;
|
|
278
|
+
const vm = session.vm;
|
|
279
|
+
const o = objectAt(vm, address);
|
|
280
|
+
if (!o) {
|
|
281
|
+
element.replaceChildren(el("h2", {}, "Gone"), el("p", { className: "meta" }, `Object ${address.toString(16)} no longer exists (disposed, or the room changed).`));
|
|
282
|
+
built = undefined;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (built !== o) {
|
|
286
|
+
built = o;
|
|
287
|
+
cells.clear();
|
|
288
|
+
values.clear();
|
|
289
|
+
// By slot: some properties have no selector name in the vocabulary.
|
|
290
|
+
const props = [...o.propIndex].sort((a, b) => a[1] - b[1]).map(([sel, slot]) => ({ name: vm.selectors[sel] ?? `sel_${sel}`, slot }));
|
|
291
|
+
const table = el("table");
|
|
292
|
+
for (const { name, slot } of props) {
|
|
293
|
+
// Header slots (-objID-, -size-, dictionaries...) aren't interesting; `name` is shown as text.
|
|
294
|
+
if (slot < NAME_SLOT || name.startsWith("-"))
|
|
295
|
+
continue;
|
|
296
|
+
const cell = el("td");
|
|
297
|
+
cells.set(slot, cell);
|
|
298
|
+
table.append(el("tr", {}, el("td", { className: "num" }, name), cell));
|
|
299
|
+
}
|
|
300
|
+
const cls = vm.classObject(o.isClass ? o.species : o.superclass);
|
|
301
|
+
element.replaceChildren(el("h2", {}, o.name), el("p", { className: "meta" }, o.isClone ? "clone" : o.isClass ? "class" : "instance", " of ", cls ? el("a", { href: ex.href(`class/${cls.species}`) }, cls.name) : "?", " · ", el("a", { href: ex.href(staticRoute(o)) }, "definition"), ` · address ${o.address.toString(16)}`), el("p", { className: "meta" }, "Values update live; changes flash."), table);
|
|
302
|
+
}
|
|
303
|
+
for (const [slot, cell] of cells) {
|
|
304
|
+
const v = o.vars[slot] ?? 0;
|
|
305
|
+
if (values.get(slot) === v)
|
|
306
|
+
continue;
|
|
307
|
+
const first = !values.has(slot);
|
|
308
|
+
values.set(slot, v);
|
|
309
|
+
cell.replaceChildren(slot === NAME_SLOT ? o.name : render(vm, v));
|
|
310
|
+
if (!first) {
|
|
311
|
+
cell.classList.remove("flash");
|
|
312
|
+
void cell.offsetWidth; // restart the animation
|
|
313
|
+
cell.classList.add("flash");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|