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.
Files changed (357) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -2
  4. package/apps/viewer/editor.html +43 -0
  5. package/apps/viewer/explore.html +27 -0
  6. package/apps/viewer/index.html +26 -0
  7. package/apps/viewer/package.json +27 -0
  8. package/apps/viewer/play.html +26 -0
  9. package/apps/viewer/plugin.d.ts +18 -0
  10. package/apps/viewer/plugin.js +75 -0
  11. package/apps/viewer/plugin.ts +84 -0
  12. package/apps/viewer/src/adlib.d.ts +20 -0
  13. package/apps/viewer/src/adlib.js +55 -0
  14. package/apps/viewer/src/adlib.ts +62 -0
  15. package/apps/viewer/src/app.d.ts +5 -0
  16. package/apps/viewer/src/app.js +5 -0
  17. package/apps/viewer/src/app.ts +5 -0
  18. package/apps/viewer/src/asset-index.d.ts +22 -0
  19. package/apps/viewer/src/asset-index.js +74 -0
  20. package/apps/viewer/src/asset-index.ts +90 -0
  21. package/apps/viewer/src/audio.d.ts +28 -0
  22. package/apps/viewer/src/audio.js +102 -0
  23. package/apps/viewer/src/audio.ts +110 -0
  24. package/apps/viewer/src/editor-handles.d.ts +63 -0
  25. package/apps/viewer/src/editor-handles.js +332 -0
  26. package/apps/viewer/src/editor-handles.ts +368 -0
  27. package/apps/viewer/src/editor-languages.d.ts +23 -0
  28. package/apps/viewer/src/editor-languages.js +157 -0
  29. package/apps/viewer/src/editor-languages.ts +135 -0
  30. package/apps/viewer/src/editor.css +213 -0
  31. package/apps/viewer/src/editor.d.ts +1 -0
  32. package/apps/viewer/src/editor.js +393 -0
  33. package/apps/viewer/src/editor.ts +412 -0
  34. package/apps/viewer/src/explore/classes.d.ts +7 -0
  35. package/apps/viewer/src/explore/classes.js +63 -0
  36. package/apps/viewer/src/explore/classes.ts +61 -0
  37. package/apps/viewer/src/explore/context.d.ts +33 -0
  38. package/apps/viewer/src/explore/context.js +96 -0
  39. package/apps/viewer/src/explore/context.ts +124 -0
  40. package/apps/viewer/src/explore/explore.css +93 -0
  41. package/apps/viewer/src/explore/explore.d.ts +1 -0
  42. package/apps/viewer/src/explore/explore.js +102 -0
  43. package/apps/viewer/src/explore/explore.ts +104 -0
  44. package/apps/viewer/src/explore/inspect.d.ts +5 -0
  45. package/apps/viewer/src/explore/inspect.js +208 -0
  46. package/apps/viewer/src/explore/inspect.ts +279 -0
  47. package/apps/viewer/src/explore/live.d.ts +22 -0
  48. package/apps/viewer/src/explore/live.js +318 -0
  49. package/apps/viewer/src/explore/live.ts +365 -0
  50. package/apps/viewer/src/explore/map.d.ts +11 -0
  51. package/apps/viewer/src/explore/map.js +182 -0
  52. package/apps/viewer/src/explore/map.ts +193 -0
  53. package/apps/viewer/src/files.d.ts +21 -0
  54. package/apps/viewer/src/files.js +69 -0
  55. package/apps/viewer/src/files.ts +72 -0
  56. package/apps/viewer/src/game-profile.d.ts +19 -0
  57. package/apps/viewer/src/game-profile.js +4 -0
  58. package/apps/viewer/src/game-profile.ts +26 -0
  59. package/apps/viewer/src/main.d.ts +1 -0
  60. package/apps/viewer/src/main.js +473 -0
  61. package/apps/viewer/src/main.ts +527 -0
  62. package/apps/viewer/src/music.d.ts +14 -0
  63. package/apps/viewer/src/music.js +32 -0
  64. package/apps/viewer/src/music.ts +34 -0
  65. package/apps/viewer/src/opl-worklet.d.ts +1 -0
  66. package/apps/viewer/src/opl-worklet.js +47 -0
  67. package/apps/viewer/src/opl-worklet.ts +55 -0
  68. package/apps/viewer/src/overlay.d.ts +31 -0
  69. package/apps/viewer/src/overlay.js +103 -0
  70. package/apps/viewer/src/overlay.ts +123 -0
  71. package/apps/viewer/src/play.css +14 -0
  72. package/apps/viewer/src/play.d.ts +1 -0
  73. package/apps/viewer/src/play.js +179 -0
  74. package/apps/viewer/src/play.ts +177 -0
  75. package/apps/viewer/src/saves.d.ts +10 -0
  76. package/apps/viewer/src/saves.js +50 -0
  77. package/apps/viewer/src/saves.ts +51 -0
  78. package/apps/viewer/src/session.d.ts +91 -0
  79. package/apps/viewer/src/session.js +301 -0
  80. package/apps/viewer/src/session.ts +321 -0
  81. package/apps/viewer/src/sound-preview.d.ts +7 -0
  82. package/apps/viewer/src/sound-preview.js +214 -0
  83. package/apps/viewer/src/sound-preview.ts +228 -0
  84. package/apps/viewer/src/style.css +64 -0
  85. package/apps/viewer/vite.config.ts +25 -0
  86. package/bin/sci-ts.mjs +9 -0
  87. package/lib/994.sc +49 -0
  88. package/lib/995.sc +53 -0
  89. package/lib/996.sc +299 -0
  90. package/lib/997.sc +198 -0
  91. package/lib/998.sc +338 -0
  92. package/lib/999.sc +189 -0
  93. package/lib/resources.d.ts +2 -0
  94. package/lib/resources.js +84 -0
  95. package/lib/resources.ts +88 -0
  96. package/lib/system.sh +62 -0
  97. package/package.json +75 -4
  98. package/packages/content/room.schema.json +267 -0
  99. package/packages/content/src/index.d.ts +3 -0
  100. package/packages/content/src/index.js +3 -0
  101. package/packages/content/src/index.ts +3 -0
  102. package/packages/content/src/room.d.ts +111 -0
  103. package/packages/content/src/room.js +886 -0
  104. package/packages/content/src/room.ts +1011 -0
  105. package/packages/content/src/target.d.ts +80 -0
  106. package/packages/content/src/target.js +1 -0
  107. package/packages/content/src/target.ts +60 -0
  108. package/packages/content/src/yarn.d.ts +90 -0
  109. package/packages/content/src/yarn.js +214 -0
  110. package/packages/content/src/yarn.ts +261 -0
  111. package/packages/sci/src/analysis/game.d.ts +90 -0
  112. package/packages/sci/src/analysis/game.js +221 -0
  113. package/packages/sci/src/analysis/game.ts +305 -0
  114. package/packages/sci/src/analysis/runtime.d.ts +58 -0
  115. package/packages/sci/src/analysis/runtime.js +133 -0
  116. package/packages/sci/src/analysis/runtime.ts +163 -0
  117. package/packages/sci/src/audio/adlib.d.ts +73 -0
  118. package/packages/sci/src/audio/adlib.js +280 -0
  119. package/packages/sci/src/audio/adlib.ts +329 -0
  120. package/packages/sci/src/audio/index.d.ts +43 -0
  121. package/packages/sci/src/audio/index.js +99 -0
  122. package/packages/sci/src/audio/index.ts +122 -0
  123. package/packages/sci/src/audio/midi.d.ts +92 -0
  124. package/packages/sci/src/audio/midi.js +242 -0
  125. package/packages/sci/src/audio/midi.ts +262 -0
  126. package/packages/sci/src/audio/opl2.d.ts +74 -0
  127. package/packages/sci/src/audio/opl2.js +283 -0
  128. package/packages/sci/src/audio/opl2.ts +283 -0
  129. package/packages/sci/src/audio/sol.d.ts +32 -0
  130. package/packages/sci/src/audio/sol.js +81 -0
  131. package/packages/sci/src/audio/sol.ts +100 -0
  132. package/packages/sci/src/audio/write.d.ts +35 -0
  133. package/packages/sci/src/audio/write.js +91 -0
  134. package/packages/sci/src/audio/write.ts +108 -0
  135. package/packages/sci/src/gfx/cel.d.ts +36 -0
  136. package/packages/sci/src/gfx/cel.js +82 -0
  137. package/packages/sci/src/gfx/cel.ts +110 -0
  138. package/packages/sci/src/gfx/palette.d.ts +24 -0
  139. package/packages/sci/src/gfx/palette.js +49 -0
  140. package/packages/sci/src/gfx/palette.ts +56 -0
  141. package/packages/sci/src/gfx/pic-file.d.ts +46 -0
  142. package/packages/sci/src/gfx/pic-file.js +114 -0
  143. package/packages/sci/src/gfx/pic-file.ts +155 -0
  144. package/packages/sci/src/gfx/pic.d.ts +27 -0
  145. package/packages/sci/src/gfx/pic.js +34 -0
  146. package/packages/sci/src/gfx/pic.ts +53 -0
  147. package/packages/sci/src/gfx/quantize.d.ts +42 -0
  148. package/packages/sci/src/gfx/quantize.js +199 -0
  149. package/packages/sci/src/gfx/quantize.ts +218 -0
  150. package/packages/sci/src/gfx/view-file.d.ts +62 -0
  151. package/packages/sci/src/gfx/view-file.js +182 -0
  152. package/packages/sci/src/gfx/view-file.ts +233 -0
  153. package/packages/sci/src/gfx/view.d.ts +28 -0
  154. package/packages/sci/src/gfx/view.js +45 -0
  155. package/packages/sci/src/gfx/view.ts +62 -0
  156. package/packages/sci/src/index.d.ts +38 -0
  157. package/packages/sci/src/index.js +38 -0
  158. package/packages/sci/src/index.ts +38 -0
  159. package/packages/sci/src/motion/pathfinding.d.ts +40 -0
  160. package/packages/sci/src/motion/pathfinding.js +140 -0
  161. package/packages/sci/src/motion/pathfinding.ts +147 -0
  162. package/packages/sci/src/resource/archive.d.ts +16 -0
  163. package/packages/sci/src/resource/archive.js +51 -0
  164. package/packages/sci/src/resource/archive.ts +56 -0
  165. package/packages/sci/src/resource/lzs.d.ts +12 -0
  166. package/packages/sci/src/resource/lzs.js +64 -0
  167. package/packages/sci/src/resource/lzs.ts +66 -0
  168. package/packages/sci/src/resource/manager.d.ts +45 -0
  169. package/packages/sci/src/resource/manager.js +158 -0
  170. package/packages/sci/src/resource/manager.ts +184 -0
  171. package/packages/sci/src/resource/map.d.ts +18 -0
  172. package/packages/sci/src/resource/map.js +34 -0
  173. package/packages/sci/src/resource/map.ts +44 -0
  174. package/packages/sci/src/resource/types.d.ts +37 -0
  175. package/packages/sci/src/resource/types.js +45 -0
  176. package/packages/sci/src/resource/types.ts +57 -0
  177. package/packages/sci/src/script/assembly.d.ts +67 -0
  178. package/packages/sci/src/script/assembly.js +748 -0
  179. package/packages/sci/src/script/assembly.ts +714 -0
  180. package/packages/sci/src/script/compiler.d.ts +75 -0
  181. package/packages/sci/src/script/compiler.js +993 -0
  182. package/packages/sci/src/script/compiler.ts +1040 -0
  183. package/packages/sci/src/script/disasm.d.ts +81 -0
  184. package/packages/sci/src/script/disasm.js +318 -0
  185. package/packages/sci/src/script/disasm.ts +323 -0
  186. package/packages/sci/src/script/kernel-names.d.ts +6 -0
  187. package/packages/sci/src/script/kernel-names.js +165 -0
  188. package/packages/sci/src/script/kernel-names.ts +165 -0
  189. package/packages/sci/src/script/opcodes.d.ts +22 -0
  190. package/packages/sci/src/script/opcodes.js +45 -0
  191. package/packages/sci/src/script/opcodes.ts +72 -0
  192. package/packages/sci/src/script/script.d.ts +56 -0
  193. package/packages/sci/src/script/script.js +63 -0
  194. package/packages/sci/src/script/script.ts +111 -0
  195. package/packages/sci/src/script/sexpr.d.ts +45 -0
  196. package/packages/sci/src/script/sexpr.js +151 -0
  197. package/packages/sci/src/script/sexpr.ts +139 -0
  198. package/packages/sci/src/script/vocab.d.ts +15 -0
  199. package/packages/sci/src/script/vocab.js +44 -0
  200. package/packages/sci/src/script/vocab.ts +48 -0
  201. package/packages/sci/src/script/world.d.ts +25 -0
  202. package/packages/sci/src/script/world.js +65 -0
  203. package/packages/sci/src/script/world.ts +70 -0
  204. package/packages/sci/src/text/font.d.ts +37 -0
  205. package/packages/sci/src/text/font.js +103 -0
  206. package/packages/sci/src/text/font.ts +125 -0
  207. package/packages/sci/src/text/message.d.ts +69 -0
  208. package/packages/sci/src/text/message.js +171 -0
  209. package/packages/sci/src/text/message.ts +205 -0
  210. package/packages/sci/src/vm/kernels/arrays.d.ts +19 -0
  211. package/packages/sci/src/vm/kernels/arrays.js +182 -0
  212. package/packages/sci/src/vm/kernels/arrays.ts +164 -0
  213. package/packages/sci/src/vm/kernels/audio.d.ts +70 -0
  214. package/packages/sci/src/vm/kernels/audio.js +347 -0
  215. package/packages/sci/src/vm/kernels/audio.ts +349 -0
  216. package/packages/sci/src/vm/kernels/core.d.ts +5 -0
  217. package/packages/sci/src/vm/kernels/core.js +87 -0
  218. package/packages/sci/src/vm/kernels/core.ts +91 -0
  219. package/packages/sci/src/vm/kernels/graphics.d.ts +141 -0
  220. package/packages/sci/src/vm/kernels/graphics.js +467 -0
  221. package/packages/sci/src/vm/kernels/graphics.ts +502 -0
  222. package/packages/sci/src/vm/kernels/index.d.ts +8 -0
  223. package/packages/sci/src/vm/kernels/index.js +29 -0
  224. package/packages/sci/src/vm/kernels/index.ts +32 -0
  225. package/packages/sci/src/vm/kernels/lists.d.ts +6 -0
  226. package/packages/sci/src/vm/kernels/lists.js +127 -0
  227. package/packages/sci/src/vm/kernels/lists.ts +111 -0
  228. package/packages/sci/src/vm/kernels/motion.d.ts +7 -0
  229. package/packages/sci/src/vm/kernels/motion.js +204 -0
  230. package/packages/sci/src/vm/kernels/motion.ts +194 -0
  231. package/packages/sci/src/vm/kernels/palette.d.ts +82 -0
  232. package/packages/sci/src/vm/kernels/palette.js +292 -0
  233. package/packages/sci/src/vm/kernels/palette.ts +303 -0
  234. package/packages/sci/src/vm/kernels/saves.d.ts +45 -0
  235. package/packages/sci/src/vm/kernels/saves.js +123 -0
  236. package/packages/sci/src/vm/kernels/saves.ts +149 -0
  237. package/packages/sci/src/vm/kernels/system.d.ts +62 -0
  238. package/packages/sci/src/vm/kernels/system.js +110 -0
  239. package/packages/sci/src/vm/kernels/system.ts +128 -0
  240. package/packages/sci/src/vm/kernels/text.d.ts +13 -0
  241. package/packages/sci/src/vm/kernels/text.js +338 -0
  242. package/packages/sci/src/vm/kernels/text.ts +328 -0
  243. package/packages/sci/src/vm/memory.d.ts +138 -0
  244. package/packages/sci/src/vm/memory.js +168 -0
  245. package/packages/sci/src/vm/memory.ts +266 -0
  246. package/packages/sci/src/vm/replace.d.ts +11 -0
  247. package/packages/sci/src/vm/replace.js +14 -0
  248. package/packages/sci/src/vm/replace.ts +16 -0
  249. package/packages/sci/src/vm/savegame.d.ts +89 -0
  250. package/packages/sci/src/vm/savegame.js +161 -0
  251. package/packages/sci/src/vm/savegame.ts +188 -0
  252. package/packages/sci/src/vm/value.d.ts +17 -0
  253. package/packages/sci/src/vm/value.js +12 -0
  254. package/packages/sci/src/vm/value.ts +25 -0
  255. package/packages/sci/src/vm/vm.d.ts +155 -0
  256. package/packages/sci/src/vm/vm.js +1053 -0
  257. package/packages/sci/src/vm/vm.ts +914 -0
  258. package/tools/adlib-render.d.ts +1 -0
  259. package/tools/adlib-render.js +49 -0
  260. package/tools/adlib-render.ts +42 -0
  261. package/tools/analyze.d.ts +1 -0
  262. package/tools/analyze.js +25 -0
  263. package/tools/analyze.ts +25 -0
  264. package/tools/art/build.d.ts +34 -0
  265. package/tools/art/build.js +175 -0
  266. package/tools/art/build.ts +153 -0
  267. package/tools/art.d.ts +1 -0
  268. package/tools/art.js +31 -0
  269. package/tools/art.ts +29 -0
  270. package/tools/asm.d.ts +1 -0
  271. package/tools/asm.js +61 -0
  272. package/tools/asm.ts +55 -0
  273. package/tools/cli.d.ts +1 -0
  274. package/tools/cli.js +99 -0
  275. package/tools/cli.ts +100 -0
  276. package/tools/editor-server.d.ts +29 -0
  277. package/tools/editor-server.js +162 -0
  278. package/tools/editor-server.ts +176 -0
  279. package/tools/explore.d.ts +1 -0
  280. package/tools/explore.js +305 -0
  281. package/tools/explore.ts +250 -0
  282. package/tools/game/audio.d.ts +19 -0
  283. package/tools/game/audio.js +143 -0
  284. package/tools/game/audio.ts +130 -0
  285. package/tools/game/build.d.ts +46 -0
  286. package/tools/game/build.js +259 -0
  287. package/tools/game/build.ts +283 -0
  288. package/tools/game/defaults.d.ts +26 -0
  289. package/tools/game/defaults.js +82 -0
  290. package/tools/game/defaults.ts +83 -0
  291. package/tools/game/font.d.ts +3 -0
  292. package/tools/game/font.js +124 -0
  293. package/tools/game/font.ts +126 -0
  294. package/tools/game/kit.d.ts +7 -0
  295. package/tools/game/kit.js +7 -0
  296. package/tools/game/kit.ts +10 -0
  297. package/tools/game/new.d.ts +8 -0
  298. package/tools/game/new.js +39 -0
  299. package/tools/game/new.ts +38 -0
  300. package/tools/game/target.d.ts +7 -0
  301. package/tools/game/target.js +41 -0
  302. package/tools/game/target.ts +42 -0
  303. package/tools/game/template/resources.ts.tmpl +43 -0
  304. package/tools/game/template/rooms/1.room.yaml +9 -0
  305. package/tools/game/template/rooms/1.yarn +17 -0
  306. package/tools/game/template/scripts/0.sc +24 -0
  307. package/tools/game.d.ts +1 -0
  308. package/tools/game.js +77 -0
  309. package/tools/game.ts +70 -0
  310. package/tools/mod-build.d.ts +28 -0
  311. package/tools/mod-build.js +173 -0
  312. package/tools/mod-build.ts +179 -0
  313. package/tools/mod-target.d.ts +16 -0
  314. package/tools/mod-target.js +26 -0
  315. package/tools/mod-target.ts +32 -0
  316. package/tools/mod.d.ts +1 -0
  317. package/tools/mod.js +47 -0
  318. package/tools/mod.ts +45 -0
  319. package/tools/msg.d.ts +1 -0
  320. package/tools/msg.js +40 -0
  321. package/tools/msg.ts +36 -0
  322. package/tools/node-files.d.ts +3 -0
  323. package/tools/node-files.js +34 -0
  324. package/tools/node-files.ts +33 -0
  325. package/tools/patches.d.ts +3 -0
  326. package/tools/patches.js +13 -0
  327. package/tools/patches.ts +13 -0
  328. package/tools/pic-build.d.ts +9 -0
  329. package/tools/pic-build.js +26 -0
  330. package/tools/pic-build.ts +27 -0
  331. package/tools/pic.d.ts +1 -0
  332. package/tools/pic.js +64 -0
  333. package/tools/pic.ts +61 -0
  334. package/tools/play.d.ts +1 -0
  335. package/tools/play.js +54 -0
  336. package/tools/play.ts +54 -0
  337. package/tools/png.d.ts +16 -0
  338. package/tools/png.js +124 -0
  339. package/tools/png.ts +123 -0
  340. package/tools/res.d.ts +1 -0
  341. package/tools/res.js +48 -0
  342. package/tools/res.ts +50 -0
  343. package/tools/scenario.d.ts +1 -0
  344. package/tools/scenario.js +74 -0
  345. package/tools/scenario.ts +80 -0
  346. package/tools/site/build.d.ts +8 -0
  347. package/tools/site/build.js +110 -0
  348. package/tools/site/build.ts +116 -0
  349. package/tools/site/markdown.d.ts +11 -0
  350. package/tools/site/markdown.js +88 -0
  351. package/tools/site/markdown.ts +84 -0
  352. package/tools/view-build.d.ts +34 -0
  353. package/tools/view-build.js +47 -0
  354. package/tools/view-build.ts +71 -0
  355. package/tools/view.d.ts +1 -0
  356. package/tools/view.js +54 -0
  357. package/tools/view.ts +52 -0
@@ -0,0 +1,305 @@
1
+ import type { ResourceManager } from "../resource/manager.ts";
2
+ import { ResourceType } from "../resource/types.ts";
3
+ import { disassemble, type DisasmFunction, type ValueFact } from "../script/disasm.ts";
4
+ import { kernelNames } from "../script/kernel-names.ts";
5
+ import { ObjectSlot, type Script, type ScriptObject } from "../script/script.ts";
6
+ import { ScriptWorld } from "../script/world.ts";
7
+
8
+ /**
9
+ * A static map of a game, built from its scripts alone: which scripts are rooms, how rooms
10
+ * connect, the class hierarchy and every object. Nothing runs; facts come from the
11
+ * disassembler's stack simulation, so anything computed at run time is missed (and marked
12
+ * as such where we can tell).
13
+ */
14
+ export interface GameIndex {
15
+ rooms: RoomInfo[];
16
+ exits: Exit[];
17
+ /** Exits whose target isn't a room script (maze codes, cut rooms). */
18
+ unresolved: Exit[];
19
+ classes: ClassInfo[];
20
+ scripts: ScriptInfo[];
21
+ /**
22
+ * Verb names, guessed from the objects that send them: icons, inventory items and spells
23
+ * keep their verb in `message` (iconLook → 1, theCloth → 54).
24
+ */
25
+ verbs: Record<number, string>;
26
+ }
27
+
28
+ export interface RoomInfo {
29
+ number: number;
30
+ /** The room object's name, e.g. "rm710". */
31
+ name: string;
32
+ className: string;
33
+ picture: number;
34
+ noun: number;
35
+ /** Scripts this room loads (ScriptID and export calls), which may hold more of its logic. */
36
+ uses: number[];
37
+ }
38
+
39
+ /**
40
+ * How we know two rooms connect:
41
+ * newRoom `curRoom newRoom: N` with a constant
42
+ * script a script object calls `newRoom: register`, started with `setScript: s 0 N`
43
+ * property `newRoom:` with one of the object's own properties (exit features)
44
+ * direction the room's north/south/east/west property
45
+ * entry the target room checks `prevRoomNum == N`: it can be entered from N
46
+ */
47
+ export type ExitKind = "newRoom" | "script" | "property" | "direction" | "entry";
48
+
49
+ export interface Exit {
50
+ from: number;
51
+ to: number;
52
+ kind: ExitKind;
53
+ /** The script whose code holds the evidence (may be a helper, not `from` itself). */
54
+ script: number;
55
+ /** Method or procedure label, e.g. "sExit::changeState". */
56
+ where: string;
57
+ }
58
+
59
+ export interface PropValue {
60
+ name: string;
61
+ value: number;
62
+ }
63
+
64
+ export interface ClassInfo {
65
+ species: number;
66
+ name: string;
67
+ script: number;
68
+ /** Superclass species, or -1 for a root class. */
69
+ superclass: number;
70
+ props: PropValue[];
71
+ methods: string[];
72
+ }
73
+
74
+ export interface ObjectInfo {
75
+ name: string;
76
+ script: number;
77
+ /** Heap offset in its script: identifies the object. */
78
+ offset: number;
79
+ superclass: number;
80
+ className: string;
81
+ props: PropValue[];
82
+ methods: string[];
83
+ }
84
+
85
+ export interface ScriptInfo {
86
+ number: number;
87
+ isRoom: boolean;
88
+ objects: ObjectInfo[];
89
+ /** Other scripts referenced by ScriptID or export calls. */
90
+ references: number[];
91
+ }
92
+
93
+ export interface AnalyzeOptions {
94
+ /** Globals the room logic uses; SCI's system scripts fix these. */
95
+ curRoomGlobal?: number;
96
+ prevRoomGlobal?: number;
97
+ /** Class every room inherits from. */
98
+ roomClass?: string;
99
+ /** Reuse a world (and its loaded scripts), e.g. to disassemble on demand afterwards. */
100
+ world?: ScriptWorld;
101
+ onProgress?: (done: number, total: number) => void;
102
+ }
103
+
104
+ /** The objects' header slots aren't interesting to show as properties. */
105
+ const HEADER_SLOTS = ObjectSlot.Name + 1;
106
+ const DIRECTIONS = ["north", "east", "south", "west"];
107
+
108
+ export async function analyzeGame(rm: ResourceManager, options: AnalyzeOptions = {}): Promise<GameIndex> {
109
+ const { prevRoomGlobal = 12, roomClass = "Room", onProgress } = options;
110
+ const world = options.world ?? (await ScriptWorld.open(rm));
111
+ await world.loadAllClasses();
112
+ const ctx = world.context();
113
+ const selector = (name: string) => world.selectorNames.indexOf(name);
114
+ const selName = (n: number) => world.selectorNames[n] ?? `sel_${n}`;
115
+
116
+ const numbers = rm.list(ResourceType.Script).map((r) => r.number).filter((n) => rm.has({ type: ResourceType.Heap, number: n }));
117
+ const scripts: Script[] = [];
118
+ for (const [i, n] of numbers.entries()) {
119
+ scripts.push(await world.script(n));
120
+ onProgress?.(i + 1, numbers.length);
121
+ }
122
+
123
+ // Classes, with property names from their own dictionaries.
124
+ const classList: ClassInfo[] = [];
125
+ const classBySpecies = new Map<number, ScriptObject>();
126
+ const classScript = new Map<number, number>();
127
+ for (const s of scripts) {
128
+ for (const o of s.objects) {
129
+ if (!o.isClass) continue;
130
+ classBySpecies.set(o.species, o);
131
+ classScript.set(o.species, s.number);
132
+ }
133
+ }
134
+ const propsOf = (obj: ScriptObject): PropValue[] => {
135
+ const names = ctx.propSelectors(obj) ?? [];
136
+ return obj.properties.slice(HEADER_SLOTS).map((value, i) => ({ name: selName(names[i + HEADER_SLOTS] ?? -1), value }));
137
+ };
138
+ const methodsOf = (obj: ScriptObject) => obj.methods.map((m) => selName(m.selector));
139
+ for (const [species, o] of classBySpecies) {
140
+ classList.push({
141
+ species,
142
+ name: o.name,
143
+ script: classScript.get(species)!,
144
+ superclass: o.superclass === 0xffff ? -1 : o.superclass,
145
+ props: propsOf(o),
146
+ methods: methodsOf(o),
147
+ });
148
+ }
149
+ classList.sort((a, b) => a.species - b.species);
150
+
151
+ const inherits = (species: number, name: string): boolean => {
152
+ for (let s = species, guard = 0; s !== 0xffff && guard < 64; guard++) {
153
+ const c = classBySpecies.get(s);
154
+ if (!c) return false;
155
+ if (c.name === name) return true;
156
+ s = c.superclass;
157
+ }
158
+ return false;
159
+ };
160
+ const prop = (obj: ScriptObject, name: string) => propsOf(obj).find((p) => p.name === name)?.value;
161
+
162
+ // Disassemble everything once: the facts drive the rest.
163
+ const functions = new Map<number, DisasmFunction[]>();
164
+ for (const s of scripts) functions.set(s.number, disassemble(s, ctx));
165
+
166
+ const scriptIdKernel = kernelNames.indexOf("ScriptID");
167
+ const scriptInfos: ScriptInfo[] = [];
168
+ const rooms: RoomInfo[] = [];
169
+ for (const s of scripts) {
170
+ const refs = new Set<number>();
171
+ for (const fn of functions.get(s.number)!) {
172
+ for (const c of fn.facts.calls) {
173
+ if (c.kind === "export" && c.target !== 0) refs.add(c.target);
174
+ if (c.kind === "kernel" && c.target === scriptIdKernel && c.args[0]?.const !== undefined) refs.add(c.args[0].const);
175
+ }
176
+ }
177
+ refs.delete(s.number);
178
+ const room = roomObject(s);
179
+ scriptInfos.push({
180
+ number: s.number,
181
+ isRoom: !!room,
182
+ references: [...refs].sort((a, b) => a - b),
183
+ objects: s.objects.filter((o) => !o.isClass).map((o) => ({
184
+ name: o.name,
185
+ script: s.number,
186
+ offset: o.offset,
187
+ superclass: o.superclass,
188
+ className: classBySpecies.get(o.superclass)?.name ?? `class ${o.superclass}`,
189
+ props: propsOf(o),
190
+ methods: methodsOf(o),
191
+ })),
192
+ });
193
+ if (room) {
194
+ rooms.push({
195
+ number: s.number,
196
+ name: room.name,
197
+ className: classBySpecies.get(room.superclass)?.name ?? "",
198
+ picture: prop(room, "picture") ?? -1,
199
+ noun: prop(room, "noun") ?? 0,
200
+ uses: [...refs].sort((a, b) => a - b),
201
+ });
202
+ }
203
+ }
204
+
205
+ function roomObject(s: Script): ScriptObject | undefined {
206
+ const e0 = s.exports[0];
207
+ const obj = e0?.kind === "object" ? s.objects.find((o) => o.offset === e0.offset) : undefined;
208
+ return obj && !obj.isClass && inherits(obj.superclass, roomClass) ? obj : undefined;
209
+ }
210
+
211
+ const roomNumbers = new Set(rooms.map((r) => r.number));
212
+ // Helper scripts (like 711 for 710) act on behalf of the rooms that load them.
213
+ const usedBy = new Map<number, number[]>();
214
+ for (const r of rooms) for (const u of r.uses) if (!roomNumbers.has(u)) usedBy.set(u, [...(usedBy.get(u) ?? []), r.number]);
215
+ const ownersOf = (script: number) => (roomNumbers.has(script) ? [script] : usedBy.get(script) ?? []);
216
+
217
+ const exits: Exit[] = [];
218
+ const seen = new Set<string>();
219
+ const addExit = (from: number, to: number, kind: ExitKind, script: number, where: string) => {
220
+ if (to <= 0 || to >= 0xffff || from === to) return;
221
+ const key = `${from}>${to}>${kind}`;
222
+ if (seen.has(key)) return;
223
+ seen.add(key);
224
+ exits.push({ from, to, kind, script, where });
225
+ };
226
+
227
+ const newRoom = selector("newRoom");
228
+ const setScript = selector("setScript");
229
+ const register = selector("register");
230
+ // Script objects whose code calls `newRoom: register`: keyed "script:heapOffset".
231
+ const registerExits = new Map<string, string>();
232
+ // Classes whose methods call `newRoom:` with one of the object's own properties.
233
+ const propertyExits: { species: number; propName: string; where: string; script: number }[] = [];
234
+
235
+ for (const s of scripts) {
236
+ for (const fn of functions.get(s.number)!) {
237
+ for (const send of fn.facts.sends) {
238
+ if (send.selector !== newRoom) continue;
239
+ const arg: ValueFact = send.args[0] ?? {};
240
+ if (arg.const !== undefined) {
241
+ for (const from of ownersOf(s.number)) addExit(from, arg.const, "newRoom", s.number, fn.label);
242
+ } else if (arg.prop === register && fn.owner) {
243
+ registerExits.set(`${s.number}:${fn.owner.offset}`, fn.label);
244
+ } else if (arg.prop !== undefined && fn.owner) {
245
+ const propName = selName(arg.prop);
246
+ if (fn.owner.isClass) propertyExits.push({ species: fn.owner.species, propName, where: fn.label, script: s.number });
247
+ else {
248
+ const to = prop(fn.owner, propName);
249
+ if (to !== undefined) for (const from of ownersOf(s.number)) addExit(from, to, "property", s.number, fn.label);
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ for (const s of scripts) {
257
+ for (const fn of functions.get(s.number)!) {
258
+ for (const send of fn.facts.sends) {
259
+ // `setScript: sExit 0 N` → sExit runs `newRoom: register` with N.
260
+ if (send.selector === setScript && send.args[0]?.heap !== undefined && send.args[2]?.const !== undefined) {
261
+ const where = registerExits.get(`${s.number}:${send.args[0].heap}`);
262
+ if (where) for (const from of ownersOf(s.number)) addExit(from, send.args[2].const, "script", s.number, where);
263
+ }
264
+ }
265
+ // Rooms that check where the hero came from can be entered from there.
266
+ if (roomNumbers.has(s.number)) {
267
+ for (const c of fn.facts.compares) {
268
+ if (c.global === prevRoomGlobal && c.op === "eq?" && roomNumbers.has(c.value)) addExit(c.value, s.number, "entry", s.number, fn.label);
269
+ }
270
+ }
271
+ }
272
+ for (const o of s.objects) {
273
+ for (const pe of propertyExits) {
274
+ if (o.isClass || !inherits(o.superclass, classBySpecies.get(pe.species)!.name)) continue;
275
+ const to = prop(o, pe.propName);
276
+ if (to !== undefined) for (const from of ownersOf(s.number)) addExit(from, to, "property", pe.script, `${o.name} (${pe.where})`);
277
+ }
278
+ }
279
+ const room = roomObject(s);
280
+ if (room) {
281
+ for (const d of DIRECTIONS) {
282
+ const to = prop(room, d);
283
+ if (to) addExit(s.number, to, "direction", s.number, `${room.name}.${d}`);
284
+ }
285
+ }
286
+ }
287
+
288
+ const verbs: Record<number, string> = {};
289
+ const rank = (name: string) => (/^icon[A-Z]/.test(name) ? 0 : /Spell$/.test(name) ? 1 : /^the[A-Z]/.test(name) ? 2 : 3);
290
+ const pretty = (name: string) => name.replace(/^icon|^the|Spell$/g, "").replace(/^./, (c) => c.toUpperCase());
291
+ const best = new Map<number, string>();
292
+ for (const info of scriptInfos) {
293
+ for (const o of info.objects) {
294
+ const verb = o.props.find((p) => p.name === "message")?.value;
295
+ if (!verb || verb >= 0x8000) continue;
296
+ const current = best.get(verb);
297
+ if (current === undefined || rank(o.name) < rank(current)) best.set(verb, o.name);
298
+ }
299
+ }
300
+ for (const [verb, name] of best) verbs[verb] = pretty(name);
301
+
302
+ const resolved = exits.filter((e) => roomNumbers.has(e.to));
303
+ const unresolved = exits.filter((e) => !roomNumbers.has(e.to));
304
+ return { rooms, exits: resolved, unresolved, classes: classList, scripts: scriptInfos, verbs };
305
+ }
@@ -0,0 +1,58 @@
1
+ import type { SciObject } from "../vm/memory.ts";
2
+ import type { Value } from "../vm/value.ts";
3
+ import type { Vm } from "../vm/vm.ts";
4
+ /**
5
+ * Reading a running game for tools (the explorer's live view): globals, the system
6
+ * collections, polygons and what's drawn where. Read-only: nothing here sends messages or
7
+ * changes state.
8
+ */
9
+ /**
10
+ * The globals tools need: the hero, the room object and its number, and the room the game
11
+ * should go to next (the game changes rooms when it's set).
12
+ */
13
+ export type SystemGlobal = "ego" | "curRoom" | "curRoomNum" | "newRoomNum";
14
+ /** Games built by sci-ts name their globals in this vocab (the selector list format). */
15
+ export declare const GLOBAL_NAMES_VOCAB = 990;
16
+ /** Script 0's variables are the game's globals: one by number, or a system global by name. */
17
+ export declare const gameGlobal: (vm: Vm, g: number | SystemGlobal) => Value | undefined;
18
+ export declare const objectAt: (vm: Vm, v: Value | undefined) => SciObject | undefined;
19
+ /** A property as a signed 16-bit number (object references come back unchanged). */
20
+ export declare const signedProp: (vm: Vm, o: SciObject | Value, name: string) => number;
21
+ /** The first instance with this name, e.g. the system collections cast, features, regions. */
22
+ export declare function namedObject(vm: Vm, name: string): SciObject | undefined;
23
+ /** The objects in a collection's `elements` list (cast, features, obstacles...). */
24
+ export declare function collectionElements(vm: Vm, collection: SciObject | Value | undefined): SciObject[];
25
+ /** Numbers from an array, or an IntArray object's `data`. */
26
+ export declare function readInts(vm: Vm, ref: Value, count: number): number[];
27
+ /** Polygon types: 0 total access, 1 near point, 2 barred, 3 contained. */
28
+ export declare function polygonOf(vm: Vm, poly: SciObject): {
29
+ type: number;
30
+ points: [number, number][];
31
+ };
32
+ export declare function isKindOf(vm: Vm, o: SciObject, className: string): boolean;
33
+ /** The room's plane (below the status bar): room objects' coordinates are relative to it. */
34
+ export declare function roomPlane(vm: Vm): {
35
+ left: number;
36
+ top: number;
37
+ address: Value;
38
+ } | undefined;
39
+ export interface DrawnItem {
40
+ item: SciObject;
41
+ plane: Value;
42
+ /** Screen rectangle. */
43
+ x: number;
44
+ y: number;
45
+ w: number;
46
+ h: number;
47
+ }
48
+ /** View screen items in visible planes, frontmost first, where they're drawn on screen. */
49
+ export declare function drawnItems(vm: Vm): DrawnItem[];
50
+ /**
51
+ * Asks the game to go to `room`, the way it changes rooms itself: `Game::doit` sees
52
+ * `newRoomNum` (global 13) differ from the current room and calls `newRoom:` (transitions and
53
+ * all). Call between frames. Rooms may assume story state (flags, items, where you came
54
+ * from); Sierra's debug keys (Alt-G flags, Alt-I items) can set that up.
55
+ */
56
+ export declare function requestRoom(vm: Vm, room: number): void;
57
+ /** True once there's a hero in play (after character creation or a restore). */
58
+ export declare function heroInPlay(vm: Vm): boolean;
@@ -0,0 +1,133 @@
1
+ import { ResourceType } from "../resource/types.js";
2
+ import { parseSelectorNames } from "../script/vocab.js";
3
+ import { graphics } from "../vm/kernels/graphics.js";
4
+ /** Where Sierra's system scripts keep them. */
5
+ const SIERRA_GLOBALS = { ego: 0, curRoom: 2, curRoomNum: 11, newRoomNum: 13 };
6
+ /** Games built by sci-ts name their globals in this vocab (the selector list format). */
7
+ export const GLOBAL_NAMES_VOCAB = 990;
8
+ const globalNumbers = new WeakMap();
9
+ function globalNumber(vm, name) {
10
+ let lookup = globalNumbers.get(vm);
11
+ if (!lookup) {
12
+ const id = { type: ResourceType.Vocab, number: GLOBAL_NAMES_VOCAB };
13
+ const names = vm.resources.has(id) ? parseSelectorNames(vm.resources.loadSync(id).data) : undefined;
14
+ lookup = (n) => (names ? names.indexOf(n) : SIERRA_GLOBALS[n]);
15
+ globalNumbers.set(vm, lookup);
16
+ }
17
+ return lookup(name);
18
+ }
19
+ /** Script 0's variables are the game's globals: one by number, or a system global by name. */
20
+ export const gameGlobal = (vm, g) => {
21
+ const n = typeof g === "number" ? g : globalNumber(vm, g);
22
+ return n < 0 ? undefined : vm.loadedScripts.find((s) => s.number === 0)?.locals[n];
23
+ };
24
+ export const objectAt = (vm, v) => (v ? vm.memory.object(v) : undefined);
25
+ /** A property as a signed 16-bit number (object references come back unchanged). */
26
+ export const signedProp = (vm, o, name) => {
27
+ const v = vm.getProp(o, name) ?? 0;
28
+ return v >= 0x8000 && v < 0x10000 ? v - 0x10000 : v;
29
+ };
30
+ /** The first instance with this name, e.g. the system collections cast, features, regions. */
31
+ export function namedObject(vm, name) {
32
+ for (const o of vm.memory.objects.values())
33
+ if (!o.isClass && o.name === name)
34
+ return o;
35
+ return undefined;
36
+ }
37
+ /** The objects in a collection's `elements` list (cast, features, obstacles...). */
38
+ export function collectionElements(vm, collection) {
39
+ const c = typeof collection === "number" ? objectAt(vm, collection) : collection;
40
+ const elements = c && vm.getProp(c, "elements");
41
+ const list = elements ? vm.memory.list(elements) : undefined;
42
+ const out = [];
43
+ // Guard against a corrupted (cyclic) list: a tool must never hang the page.
44
+ for (let n = list?.first, guard = 0; n && guard < 10_000; n = n.next, guard++) {
45
+ const o = objectAt(vm, n.value);
46
+ if (o)
47
+ out.push(o);
48
+ }
49
+ return out;
50
+ }
51
+ /** Numbers from an array, or an IntArray object's `data`. */
52
+ export function readInts(vm, ref, count) {
53
+ if (!ref)
54
+ return [];
55
+ const direct = vm.memory.array(ref);
56
+ const data = direct ? undefined : vm.memory.object(ref) ? vm.getProp(ref, "data") : undefined;
57
+ const array = direct ?? (data ? vm.memory.array(data) : undefined);
58
+ return (array?.data ?? []).slice(0, count).map((v) => (v >= 0x8000 ? v - 0x10000 : v));
59
+ }
60
+ /** Polygon types: 0 total access, 1 near point, 2 barred, 3 contained. */
61
+ export function polygonOf(vm, poly) {
62
+ const size = Math.max(0, Math.min(signedProp(vm, poly, "size"), 1000));
63
+ const flat = readInts(vm, vm.getProp(poly, "points") ?? 0, size * 2);
64
+ return {
65
+ type: signedProp(vm, poly, "type"),
66
+ points: Array.from({ length: Math.floor(flat.length / 2) }, (_, i) => [flat[i * 2], flat[i * 2 + 1]]),
67
+ };
68
+ }
69
+ export function isKindOf(vm, o, className) {
70
+ for (let species = o.isClass ? o.species : o.superclass, guard = 0; species !== 0xffff && guard < 64; guard++) {
71
+ let c;
72
+ try {
73
+ c = vm.classObject(species);
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ if (c.name === className)
79
+ return true;
80
+ species = c.superclass;
81
+ }
82
+ return false;
83
+ }
84
+ /** The room's plane (below the status bar): room objects' coordinates are relative to it. */
85
+ export function roomPlane(vm) {
86
+ const g = graphics(vm);
87
+ const room = objectAt(vm, gameGlobal(vm, "curRoom"));
88
+ let plane = room ? vm.getProp(room, "plane") : undefined;
89
+ if (!plane || !vm.memory.object(plane)) {
90
+ plane = [...g.planes].find((p) => vm.memory.object(p) && g.prop(p, "picture") >= 0 && g.prop(p, "picture") < 0x8000);
91
+ }
92
+ return plane ? { left: g.prop(plane, "inLeft"), top: g.prop(plane, "inTop"), address: plane } : undefined;
93
+ }
94
+ /** View screen items in visible planes, frontmost first, where they're drawn on screen. */
95
+ export function drawnItems(vm) {
96
+ const g = graphics(vm);
97
+ const out = [];
98
+ for (const item of g.items) {
99
+ const o = objectAt(vm, item);
100
+ const plane = o && vm.getProp(item, "plane");
101
+ if (!o || !plane || !vm.memory.object(plane))
102
+ continue;
103
+ const planePriority = g.prop(plane, "priority");
104
+ if (planePriority === -1)
105
+ continue;
106
+ const bitmap = vm.getProp(item, "bitmap");
107
+ if (bitmap && vm.memory.bitmap(bitmap))
108
+ continue;
109
+ const r = g.viewRect(item);
110
+ if (!r)
111
+ continue;
112
+ const priority = g.prop(item, "fixPriority") ? g.prop(item, "priority") : g.prop(item, "y");
113
+ out.push({ item: o, plane, x: g.prop(plane, "inLeft") + r.x, y: g.prop(plane, "inTop") + r.y, w: r.width, h: r.height, order: planePriority * 100_000 + priority });
114
+ }
115
+ return out.sort((a, b) => b.order - a.order);
116
+ }
117
+ /**
118
+ * Asks the game to go to `room`, the way it changes rooms itself: `Game::doit` sees
119
+ * `newRoomNum` (global 13) differ from the current room and calls `newRoom:` (transitions and
120
+ * all). Call between frames. Rooms may assume story state (flags, items, where you came
121
+ * from); Sierra's debug keys (Alt-G flags, Alt-I items) can set that up.
122
+ */
123
+ export function requestRoom(vm, room) {
124
+ const script0 = vm.loadedScripts.find((s) => s.number === 0);
125
+ if (!script0)
126
+ throw new Error("The game hasn't started");
127
+ script0.locals[globalNumber(vm, "newRoomNum")] = room & 0xffff;
128
+ }
129
+ /** True once there's a hero in play (after character creation or a restore). */
130
+ export function heroInPlay(vm) {
131
+ const ego = objectAt(vm, gameGlobal(vm, "ego"));
132
+ return !!ego && signedProp(vm, ego, "view") > 0;
133
+ }
@@ -0,0 +1,163 @@
1
+ import { ResourceType } from "../resource/types.ts";
2
+ import { parseSelectorNames } from "../script/vocab.ts";
3
+ import type { SciObject } from "../vm/memory.ts";
4
+ import { graphics } from "../vm/kernels/graphics.ts";
5
+ import type { Value } from "../vm/value.ts";
6
+ import type { Vm } from "../vm/vm.ts";
7
+
8
+ /**
9
+ * Reading a running game for tools (the explorer's live view): globals, the system
10
+ * collections, polygons and what's drawn where. Read-only: nothing here sends messages or
11
+ * changes state.
12
+ */
13
+
14
+ /**
15
+ * The globals tools need: the hero, the room object and its number, and the room the game
16
+ * should go to next (the game changes rooms when it's set).
17
+ */
18
+ export type SystemGlobal = "ego" | "curRoom" | "curRoomNum" | "newRoomNum";
19
+
20
+ /** Where Sierra's system scripts keep them. */
21
+ const SIERRA_GLOBALS: Record<SystemGlobal, number> = { ego: 0, curRoom: 2, curRoomNum: 11, newRoomNum: 13 };
22
+
23
+ /** Games built by sci-ts name their globals in this vocab (the selector list format). */
24
+ export const GLOBAL_NAMES_VOCAB = 990;
25
+
26
+ const globalNumbers = new WeakMap<Vm, (name: SystemGlobal) => number>();
27
+
28
+ function globalNumber(vm: Vm, name: SystemGlobal): number {
29
+ let lookup = globalNumbers.get(vm);
30
+ if (!lookup) {
31
+ const id = { type: ResourceType.Vocab, number: GLOBAL_NAMES_VOCAB };
32
+ const names = vm.resources.has(id) ? parseSelectorNames(vm.resources.loadSync(id).data) : undefined;
33
+ lookup = (n) => (names ? names.indexOf(n) : SIERRA_GLOBALS[n]);
34
+ globalNumbers.set(vm, lookup);
35
+ }
36
+ return lookup(name);
37
+ }
38
+
39
+ /** Script 0's variables are the game's globals: one by number, or a system global by name. */
40
+ export const gameGlobal = (vm: Vm, g: number | SystemGlobal): Value | undefined => {
41
+ const n = typeof g === "number" ? g : globalNumber(vm, g);
42
+ return n < 0 ? undefined : vm.loadedScripts.find((s) => s.number === 0)?.locals[n];
43
+ };
44
+
45
+ export const objectAt = (vm: Vm, v: Value | undefined): SciObject | undefined => (v ? vm.memory.object(v) : undefined);
46
+
47
+ /** A property as a signed 16-bit number (object references come back unchanged). */
48
+ export const signedProp = (vm: Vm, o: SciObject | Value, name: string): number => {
49
+ const v = vm.getProp(o, name) ?? 0;
50
+ return v >= 0x8000 && v < 0x10000 ? v - 0x10000 : v;
51
+ };
52
+
53
+ /** The first instance with this name, e.g. the system collections cast, features, regions. */
54
+ export function namedObject(vm: Vm, name: string): SciObject | undefined {
55
+ for (const o of vm.memory.objects.values()) if (!o.isClass && o.name === name) return o;
56
+ return undefined;
57
+ }
58
+
59
+ /** The objects in a collection's `elements` list (cast, features, obstacles...). */
60
+ export function collectionElements(vm: Vm, collection: SciObject | Value | undefined): SciObject[] {
61
+ const c = typeof collection === "number" ? objectAt(vm, collection) : collection;
62
+ const elements = c && vm.getProp(c, "elements");
63
+ const list = elements ? vm.memory.list(elements) : undefined;
64
+ const out: SciObject[] = [];
65
+ // Guard against a corrupted (cyclic) list: a tool must never hang the page.
66
+ for (let n = list?.first, guard = 0; n && guard < 10_000; n = n.next, guard++) {
67
+ const o = objectAt(vm, n.value);
68
+ if (o) out.push(o);
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /** Numbers from an array, or an IntArray object's `data`. */
74
+ export function readInts(vm: Vm, ref: Value, count: number): number[] {
75
+ if (!ref) return [];
76
+ const direct = vm.memory.array(ref);
77
+ const data = direct ? undefined : vm.memory.object(ref) ? vm.getProp(ref, "data") : undefined;
78
+ const array = direct ?? (data ? vm.memory.array(data) : undefined);
79
+ return (array?.data ?? []).slice(0, count).map((v) => (v >= 0x8000 ? v - 0x10000 : v));
80
+ }
81
+
82
+ /** Polygon types: 0 total access, 1 near point, 2 barred, 3 contained. */
83
+ export function polygonOf(vm: Vm, poly: SciObject): { type: number; points: [number, number][] } {
84
+ const size = Math.max(0, Math.min(signedProp(vm, poly, "size"), 1000));
85
+ const flat = readInts(vm, vm.getProp(poly, "points") ?? 0, size * 2);
86
+ return {
87
+ type: signedProp(vm, poly, "type"),
88
+ points: Array.from({ length: Math.floor(flat.length / 2) }, (_, i) => [flat[i * 2]!, flat[i * 2 + 1]!] as [number, number]),
89
+ };
90
+ }
91
+
92
+ export function isKindOf(vm: Vm, o: SciObject, className: string): boolean {
93
+ for (let species = o.isClass ? o.species : o.superclass, guard = 0; species !== 0xffff && guard < 64; guard++) {
94
+ let c: SciObject | undefined;
95
+ try {
96
+ c = vm.classObject(species);
97
+ } catch {
98
+ return false;
99
+ }
100
+ if (c.name === className) return true;
101
+ species = c.superclass;
102
+ }
103
+ return false;
104
+ }
105
+
106
+ /** The room's plane (below the status bar): room objects' coordinates are relative to it. */
107
+ export function roomPlane(vm: Vm): { left: number; top: number; address: Value } | undefined {
108
+ const g = graphics(vm);
109
+ const room = objectAt(vm, gameGlobal(vm, "curRoom"));
110
+ let plane = room ? vm.getProp(room, "plane") : undefined;
111
+ if (!plane || !vm.memory.object(plane)) {
112
+ plane = [...g.planes].find((p) => vm.memory.object(p) && g.prop(p, "picture") >= 0 && g.prop(p, "picture") < 0x8000);
113
+ }
114
+ return plane ? { left: g.prop(plane, "inLeft"), top: g.prop(plane, "inTop"), address: plane } : undefined;
115
+ }
116
+
117
+ export interface DrawnItem {
118
+ item: SciObject;
119
+ plane: Value;
120
+ /** Screen rectangle. */
121
+ x: number;
122
+ y: number;
123
+ w: number;
124
+ h: number;
125
+ }
126
+
127
+ /** View screen items in visible planes, frontmost first, where they're drawn on screen. */
128
+ export function drawnItems(vm: Vm): DrawnItem[] {
129
+ const g = graphics(vm);
130
+ const out: (DrawnItem & { order: number })[] = [];
131
+ for (const item of g.items) {
132
+ const o = objectAt(vm, item);
133
+ const plane = o && vm.getProp(item, "plane");
134
+ if (!o || !plane || !vm.memory.object(plane)) continue;
135
+ const planePriority = g.prop(plane, "priority");
136
+ if (planePriority === -1) continue;
137
+ const bitmap = vm.getProp(item, "bitmap");
138
+ if (bitmap && vm.memory.bitmap(bitmap)) continue;
139
+ const r = g.viewRect(item);
140
+ if (!r) continue;
141
+ const priority = g.prop(item, "fixPriority") ? g.prop(item, "priority") : g.prop(item, "y");
142
+ out.push({ item: o, plane, x: g.prop(plane, "inLeft") + r.x, y: g.prop(plane, "inTop") + r.y, w: r.width, h: r.height, order: planePriority * 100_000 + priority });
143
+ }
144
+ return out.sort((a, b) => b.order - a.order);
145
+ }
146
+
147
+ /**
148
+ * Asks the game to go to `room`, the way it changes rooms itself: `Game::doit` sees
149
+ * `newRoomNum` (global 13) differ from the current room and calls `newRoom:` (transitions and
150
+ * all). Call between frames. Rooms may assume story state (flags, items, where you came
151
+ * from); Sierra's debug keys (Alt-G flags, Alt-I items) can set that up.
152
+ */
153
+ export function requestRoom(vm: Vm, room: number): void {
154
+ const script0 = vm.loadedScripts.find((s) => s.number === 0);
155
+ if (!script0) throw new Error("The game hasn't started");
156
+ script0.locals[globalNumber(vm, "newRoomNum")] = room & 0xffff;
157
+ }
158
+
159
+ /** True once there's a hero in play (after character creation or a restore). */
160
+ export function heroInPlay(vm: Vm): boolean {
161
+ const ego = objectAt(vm, gameGlobal(vm, "ego"));
162
+ return !!ego && signedProp(vm, ego, "view") > 0;
163
+ }