sci2-ts 0.0.0-stage → 0.2.1

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 +72 -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,261 @@
1
+ /**
2
+ * Yarn Spinner's file format (https://docs.yarnspinner.dev), the part rooms use so far:
3
+ *
4
+ * title: wisp.talk <- headers, `key: value`, until ---
5
+ * ---
6
+ * Golden pods hang from the rock. <- a line; `Speaker: text` names who says it
7
+ * <<if $met_wisp>> <- conditions on true/false variables:
8
+ * They hum when you're near. not, and, or, ==, !=, (...), true, false
9
+ * <<else>>
10
+ * They're silent.
11
+ * <<endif>>
12
+ * <<set $looked to true>>
13
+ * -> Who are you? <- a choice; its body is indented below it
14
+ * Wisp: A memory of this place.
15
+ * -> Sing for me <<if $met_wisp>> <- a choice only offered when its condition holds
16
+ * Wisp: Not yet.
17
+ * // a comment
18
+ * ===
19
+ *
20
+ * Other commands (<<walk hero 110 160>>, <<wait 2>>...) are kept as name + arguments for
21
+ * the compiler to check. Choices inside choices or inside <<if>> are rejected with the
22
+ * line they're on, until the compiler supports them.
23
+ */
24
+ export interface YarnNode {
25
+ title: string;
26
+ headers: Record<string, string>;
27
+ body: YarnItem[];
28
+ /** Where the node starts, for error messages. */
29
+ line: number;
30
+ }
31
+
32
+ export type YarnItem = YarnLine | YarnChoice | YarnSet | YarnIf | YarnCommand;
33
+
34
+ /** Any other <<command arg arg ...>>; the compiler knows which exist (walk, wait, ...). */
35
+ export interface YarnCommand {
36
+ kind: "command";
37
+ name: string;
38
+ args: string[];
39
+ line: number;
40
+ }
41
+
42
+ export interface YarnLine {
43
+ kind: "line";
44
+ speaker?: string;
45
+ text: string;
46
+ line: number;
47
+ }
48
+
49
+ export interface YarnChoice {
50
+ kind: "choice";
51
+ text: string;
52
+ /** Only offered when this holds. */
53
+ when?: YarnExpr;
54
+ body: YarnItem[];
55
+ line: number;
56
+ }
57
+
58
+ export interface YarnSet {
59
+ kind: "set";
60
+ name: string;
61
+ value: YarnExpr;
62
+ line: number;
63
+ }
64
+
65
+ export interface YarnIf {
66
+ kind: "if";
67
+ /** `if` and `elseif` branches in order; a final `else` has no condition. */
68
+ branches: { when?: YarnExpr; body: YarnItem[] }[];
69
+ line: number;
70
+ }
71
+
72
+ /** Boolean expressions over variables (each a flag). */
73
+ export type YarnExpr =
74
+ | { kind: "var"; name: string }
75
+ | { kind: "const"; value: boolean }
76
+ | { kind: "not"; of: YarnExpr }
77
+ | { kind: "and" | "or"; a: YarnExpr; b: YarnExpr };
78
+
79
+ export class YarnError extends Error {
80
+ constructor(readonly file: string, readonly line: number, message: string) {
81
+ super(`${file}:${line}: ${message}`);
82
+ }
83
+ }
84
+
85
+ /** Trailing #tags (Yarn's line ids and metadata) don't belong to the text. */
86
+ const stripTags = (s: string) => s.replace(/(\s+#[^\s#]+)+\s*$/, "");
87
+ const indentOf = (s: string) => /^[ \t]*/.exec(s)![0].replace(/\t/g, " ").length;
88
+
89
+ export function parseYarn(source: string, file = "<yarn>"): YarnNode[] {
90
+ const nodes: YarnNode[] = [];
91
+ const lines = source.split(/\r?\n/);
92
+ let i = 0;
93
+ const fail = (line: number, why: string): never => {
94
+ throw new YarnError(file, line, why);
95
+ };
96
+ while (i < lines.length) {
97
+ // Headers.
98
+ const start = i + 1;
99
+ const headers: Record<string, string> = {};
100
+ for (; i < lines.length && lines[i]!.trim() !== "---"; i++) {
101
+ const raw = lines[i]!.trim();
102
+ if (!raw || raw.startsWith("//")) continue;
103
+ const m = /^([A-Za-z_][\w.-]*)\s*:\s*(.*)$/.exec(raw);
104
+ if (!m) fail(i + 1, `expected a header (key: value) or ---, got "${raw}"`);
105
+ headers[m![1]!] = m![2]!.trim();
106
+ }
107
+ if (i >= lines.length) {
108
+ if (Object.keys(headers).length) fail(start, "node has no body: expected --- after its headers");
109
+ break;
110
+ }
111
+ const title = headers.title;
112
+ if (!title) fail(start, "node has no title: header");
113
+ if (nodes.some((n) => n.title === title)) fail(start, `a node called "${title}" already exists`);
114
+ i++; // ---
115
+ // The body's lines up to ===, then parsed into a tree.
116
+ const body: { text: string; indent: number; line: number }[] = [];
117
+ let closed = false;
118
+ for (; i < lines.length; i++) {
119
+ const raw = lines[i]!.trim();
120
+ if (raw === "===") {
121
+ closed = true;
122
+ i++;
123
+ break;
124
+ }
125
+ if (!raw || raw.startsWith("//")) continue;
126
+ body.push({ text: raw, indent: indentOf(lines[i]!), line: i + 1 });
127
+ }
128
+ if (!closed) fail(start, `node "${title}" isn't closed with ===`);
129
+ const parser = new BodyParser(body, fail);
130
+ nodes.push({ title: title!, headers, body: parser.block(-1, false, false, []), line: start });
131
+ if (parser.at < body.length) {
132
+ const extra = body[parser.at]!;
133
+ fail(extra.line, `unexpected ${extra.text.split(/\s/)[0]} (no <<if>> is open)`);
134
+ }
135
+ }
136
+ return nodes;
137
+ }
138
+
139
+ class BodyParser {
140
+ at = 0;
141
+ constructor(
142
+ readonly lines: { text: string; indent: number; line: number }[],
143
+ readonly fail: (line: number, why: string) => never,
144
+ ) {}
145
+
146
+ /**
147
+ * Items until a line indented at most `parentIndent` (the end of a choice's body) or, in
148
+ * an <<if>>, one of `stopAt` (elseif/else/endif).
149
+ */
150
+ block(parentIndent: number, inChoice: boolean, inIf: boolean, stopAt: string[]): YarnItem[] {
151
+ const items: YarnItem[] = [];
152
+ let choiceIndent: number | undefined;
153
+ while (this.at < this.lines.length) {
154
+ const { text, indent, line } = this.lines[this.at]!;
155
+ if (indent <= parentIndent) break;
156
+ const command = /^<<\s*(\w+)\s*(.*?)\s*>>$/.exec(text);
157
+ if (command && stopAt.includes(command[1]!)) break;
158
+ this.at++;
159
+ if (text.startsWith("->")) {
160
+ if (inChoice) this.fail(line, "choices inside choices aren't supported yet");
161
+ if (inIf) this.fail(line, "choices inside <<if>> aren't supported yet (use -> text <<if $x>>)");
162
+ if (choiceIndent !== undefined && indent !== choiceIndent) this.fail(line, "choices in a group must line up");
163
+ choiceIndent = indent;
164
+ let rest = stripTags(text.slice(2).trim());
165
+ let when: YarnExpr | undefined;
166
+ const cond = /^(.*?)\s*<<\s*if\s+(.+?)\s*>>$/.exec(rest);
167
+ if (cond) {
168
+ rest = cond[1]!;
169
+ when = parseExpr(cond[2]!, (why) => this.fail(line, why));
170
+ }
171
+ if (!rest) this.fail(line, "a choice needs text after ->");
172
+ items.push({ kind: "choice", text: rest, when, body: this.block(indent, true, false, []), line });
173
+ continue;
174
+ }
175
+ if (command) {
176
+ const [, name, args] = command;
177
+ if (name === "set") {
178
+ const m = /^\$([A-Za-z_]\w*)\s*(?:to|=)\s*(.+)$/.exec(args!);
179
+ if (!m) this.fail(line, "expected <<set $name to true>> (or false, or a condition)");
180
+ items.push({ kind: "set", name: m![1]!, value: parseExpr(m![2]!, (why) => this.fail(line, why)), line });
181
+ } else if (name === "if") {
182
+ items.push(this.ifBlock(args!, line, indent, inChoice));
183
+ } else if (["elseif", "else", "endif"].includes(name!)) {
184
+ this.fail(line, `<<${name}>> without an <<if>>`);
185
+ } else {
186
+ items.push({ kind: "command", name: name!, args: args ? args.split(/[\s,]+/).filter(Boolean) : [], line });
187
+ }
188
+ continue;
189
+ }
190
+ if (text.startsWith("<<")) this.fail(line, `expected <<command ...>>, got "${text}"`);
191
+ const clean = stripTags(text);
192
+ const m = /^([A-Za-z][\w ]*?)\s*:\s+(.+)$/.exec(clean);
193
+ items.push(m ? { kind: "line", speaker: m[1]!, text: m[2]!, line } : { kind: "line", text: clean, line });
194
+ }
195
+ return items;
196
+ }
197
+
198
+ private ifBlock(firstCond: string, line: number, indent: number, inChoice: boolean): YarnIf {
199
+ const fail = (why: string) => this.fail(line, why);
200
+ const branches: YarnIf["branches"] = [{ when: parseExpr(firstCond, fail), body: [] }];
201
+ // An <<if>>'s body is whatever comes before its <<elseif>>/<<else>>/<<endif>>, at any
202
+ // indentation deeper than the enclosing choice (Yarn doesn't require indenting it).
203
+ for (;;) {
204
+ branches[branches.length - 1]!.body = this.block(inChoice ? indent - 1 : -1, inChoice, true, ["elseif", "else", "endif"]);
205
+ const next = this.lines[this.at];
206
+ const command = next && /^<<\s*(\w+)\s*(.*?)\s*>>$/.exec(next.text);
207
+ if (!command) this.fail(line, "<<if>> without <<endif>>");
208
+ this.at++;
209
+ if (command![1] === "endif") return { kind: "if", branches, line };
210
+ if (branches.at(-1)!.when === undefined) this.fail(next!.line, `<<${command![1]}>> after <<else>>`);
211
+ if (command![1] === "else") branches.push({ body: [] });
212
+ else branches.push({ when: parseExpr(command![2]!, (why) => this.fail(next!.line, why)), body: [] });
213
+ }
214
+ }
215
+ }
216
+
217
+ /** `$a and not ($b or $c == false)`: Yarn's boolean operators, both spellings. */
218
+ export function parseExpr(source: string, fail: (why: string) => never): YarnExpr {
219
+ const tokens = source.match(/\$[A-Za-z_]\w*|&&|\|\||==|!=|[()!]|[A-Za-z_]\w*|\S/g) ?? [];
220
+ let p = 0;
221
+ const peek = () => tokens[p];
222
+ const take = () => tokens[p++];
223
+ const word = (t: string | undefined, ...names: string[]) => t !== undefined && names.includes(t.toLowerCase());
224
+ const or = (): YarnExpr => {
225
+ let a = and();
226
+ while (word(peek(), "or", "||")) (take(), (a = { kind: "or", a, b: and() }));
227
+ return a;
228
+ };
229
+ const and = (): YarnExpr => {
230
+ let a = compare();
231
+ while (word(peek(), "and", "&&")) (take(), (a = { kind: "and", a, b: compare() }));
232
+ return a;
233
+ };
234
+ const compare = (): YarnExpr => {
235
+ const a = unary();
236
+ if (word(peek(), "==", "!=", "is", "eq", "neq")) {
237
+ const op = take()!.toLowerCase();
238
+ const b = unary();
239
+ // a == b is (a and b) or (not a and not b).
240
+ const same: YarnExpr = { kind: "or", a: { kind: "and", a, b }, b: { kind: "and", a: { kind: "not", of: a }, b: { kind: "not", of: b } } };
241
+ return op === "!=" || op === "neq" ? { kind: "not", of: same } : same;
242
+ }
243
+ return a;
244
+ };
245
+ const unary = (): YarnExpr => {
246
+ const t = take();
247
+ if (t === undefined) fail(`incomplete condition "${source}"`);
248
+ if (word(t, "not", "!")) return { kind: "not", of: unary() };
249
+ if (t === "(") {
250
+ const e = or();
251
+ if (take() !== ")") fail(`missing ) in "${source}"`);
252
+ return e;
253
+ }
254
+ if (word(t, "true", "false")) return { kind: "const", value: t!.toLowerCase() === "true" };
255
+ if (t!.startsWith("$")) return { kind: "var", name: t!.slice(1) };
256
+ return fail(`unexpected "${t}" in "${source}" (variables are $name; numbers and text aren't supported yet)`);
257
+ };
258
+ const e = or();
259
+ if (p < tokens.length) fail(`unexpected "${tokens[p]}" in "${source}"`);
260
+ return e;
261
+ }
@@ -0,0 +1,90 @@
1
+ import type { ResourceManager } from "../resource/manager.ts";
2
+ import { ScriptWorld } from "../script/world.ts";
3
+ /**
4
+ * A static map of a game, built from its scripts alone: which scripts are rooms, how rooms
5
+ * connect, the class hierarchy and every object. Nothing runs; facts come from the
6
+ * disassembler's stack simulation, so anything computed at run time is missed (and marked
7
+ * as such where we can tell).
8
+ */
9
+ export interface GameIndex {
10
+ rooms: RoomInfo[];
11
+ exits: Exit[];
12
+ /** Exits whose target isn't a room script (maze codes, cut rooms). */
13
+ unresolved: Exit[];
14
+ classes: ClassInfo[];
15
+ scripts: ScriptInfo[];
16
+ /**
17
+ * Verb names, guessed from the objects that send them: icons, inventory items and spells
18
+ * keep their verb in `message` (iconLook → 1, theCloth → 54).
19
+ */
20
+ verbs: Record<number, string>;
21
+ }
22
+ export interface RoomInfo {
23
+ number: number;
24
+ /** The room object's name, e.g. "rm710". */
25
+ name: string;
26
+ className: string;
27
+ picture: number;
28
+ noun: number;
29
+ /** Scripts this room loads (ScriptID and export calls), which may hold more of its logic. */
30
+ uses: number[];
31
+ }
32
+ /**
33
+ * How we know two rooms connect:
34
+ * newRoom `curRoom newRoom: N` with a constant
35
+ * script a script object calls `newRoom: register`, started with `setScript: s 0 N`
36
+ * property `newRoom:` with one of the object's own properties (exit features)
37
+ * direction the room's north/south/east/west property
38
+ * entry the target room checks `prevRoomNum == N`: it can be entered from N
39
+ */
40
+ export type ExitKind = "newRoom" | "script" | "property" | "direction" | "entry";
41
+ export interface Exit {
42
+ from: number;
43
+ to: number;
44
+ kind: ExitKind;
45
+ /** The script whose code holds the evidence (may be a helper, not `from` itself). */
46
+ script: number;
47
+ /** Method or procedure label, e.g. "sExit::changeState". */
48
+ where: string;
49
+ }
50
+ export interface PropValue {
51
+ name: string;
52
+ value: number;
53
+ }
54
+ export interface ClassInfo {
55
+ species: number;
56
+ name: string;
57
+ script: number;
58
+ /** Superclass species, or -1 for a root class. */
59
+ superclass: number;
60
+ props: PropValue[];
61
+ methods: string[];
62
+ }
63
+ export interface ObjectInfo {
64
+ name: string;
65
+ script: number;
66
+ /** Heap offset in its script: identifies the object. */
67
+ offset: number;
68
+ superclass: number;
69
+ className: string;
70
+ props: PropValue[];
71
+ methods: string[];
72
+ }
73
+ export interface ScriptInfo {
74
+ number: number;
75
+ isRoom: boolean;
76
+ objects: ObjectInfo[];
77
+ /** Other scripts referenced by ScriptID or export calls. */
78
+ references: number[];
79
+ }
80
+ export interface AnalyzeOptions {
81
+ /** Globals the room logic uses; SCI's system scripts fix these. */
82
+ curRoomGlobal?: number;
83
+ prevRoomGlobal?: number;
84
+ /** Class every room inherits from. */
85
+ roomClass?: string;
86
+ /** Reuse a world (and its loaded scripts), e.g. to disassemble on demand afterwards. */
87
+ world?: ScriptWorld;
88
+ onProgress?: (done: number, total: number) => void;
89
+ }
90
+ export declare function analyzeGame(rm: ResourceManager, options?: AnalyzeOptions): Promise<GameIndex>;
@@ -0,0 +1,221 @@
1
+ import { ResourceType } from "../resource/types.js";
2
+ import { disassemble } from "../script/disasm.js";
3
+ import { kernelNames } from "../script/kernel-names.js";
4
+ import { ObjectSlot } from "../script/script.js";
5
+ import { ScriptWorld } from "../script/world.js";
6
+ /** The objects' header slots aren't interesting to show as properties. */
7
+ const HEADER_SLOTS = ObjectSlot.Name + 1;
8
+ const DIRECTIONS = ["north", "east", "south", "west"];
9
+ export async function analyzeGame(rm, options = {}) {
10
+ const { prevRoomGlobal = 12, roomClass = "Room", onProgress } = options;
11
+ const world = options.world ?? (await ScriptWorld.open(rm));
12
+ await world.loadAllClasses();
13
+ const ctx = world.context();
14
+ const selector = (name) => world.selectorNames.indexOf(name);
15
+ const selName = (n) => world.selectorNames[n] ?? `sel_${n}`;
16
+ const numbers = rm.list(ResourceType.Script).map((r) => r.number).filter((n) => rm.has({ type: ResourceType.Heap, number: n }));
17
+ const scripts = [];
18
+ for (const [i, n] of numbers.entries()) {
19
+ scripts.push(await world.script(n));
20
+ onProgress?.(i + 1, numbers.length);
21
+ }
22
+ // Classes, with property names from their own dictionaries.
23
+ const classList = [];
24
+ const classBySpecies = new Map();
25
+ const classScript = new Map();
26
+ for (const s of scripts) {
27
+ for (const o of s.objects) {
28
+ if (!o.isClass)
29
+ continue;
30
+ classBySpecies.set(o.species, o);
31
+ classScript.set(o.species, s.number);
32
+ }
33
+ }
34
+ const propsOf = (obj) => {
35
+ const names = ctx.propSelectors(obj) ?? [];
36
+ return obj.properties.slice(HEADER_SLOTS).map((value, i) => ({ name: selName(names[i + HEADER_SLOTS] ?? -1), value }));
37
+ };
38
+ const methodsOf = (obj) => obj.methods.map((m) => selName(m.selector));
39
+ for (const [species, o] of classBySpecies) {
40
+ classList.push({
41
+ species,
42
+ name: o.name,
43
+ script: classScript.get(species),
44
+ superclass: o.superclass === 0xffff ? -1 : o.superclass,
45
+ props: propsOf(o),
46
+ methods: methodsOf(o),
47
+ });
48
+ }
49
+ classList.sort((a, b) => a.species - b.species);
50
+ const inherits = (species, name) => {
51
+ for (let s = species, guard = 0; s !== 0xffff && guard < 64; guard++) {
52
+ const c = classBySpecies.get(s);
53
+ if (!c)
54
+ return false;
55
+ if (c.name === name)
56
+ return true;
57
+ s = c.superclass;
58
+ }
59
+ return false;
60
+ };
61
+ const prop = (obj, name) => propsOf(obj).find((p) => p.name === name)?.value;
62
+ // Disassemble everything once: the facts drive the rest.
63
+ const functions = new Map();
64
+ for (const s of scripts)
65
+ functions.set(s.number, disassemble(s, ctx));
66
+ const scriptIdKernel = kernelNames.indexOf("ScriptID");
67
+ const scriptInfos = [];
68
+ const rooms = [];
69
+ for (const s of scripts) {
70
+ const refs = new Set();
71
+ for (const fn of functions.get(s.number)) {
72
+ for (const c of fn.facts.calls) {
73
+ if (c.kind === "export" && c.target !== 0)
74
+ refs.add(c.target);
75
+ if (c.kind === "kernel" && c.target === scriptIdKernel && c.args[0]?.const !== undefined)
76
+ refs.add(c.args[0].const);
77
+ }
78
+ }
79
+ refs.delete(s.number);
80
+ const room = roomObject(s);
81
+ scriptInfos.push({
82
+ number: s.number,
83
+ isRoom: !!room,
84
+ references: [...refs].sort((a, b) => a - b),
85
+ objects: s.objects.filter((o) => !o.isClass).map((o) => ({
86
+ name: o.name,
87
+ script: s.number,
88
+ offset: o.offset,
89
+ superclass: o.superclass,
90
+ className: classBySpecies.get(o.superclass)?.name ?? `class ${o.superclass}`,
91
+ props: propsOf(o),
92
+ methods: methodsOf(o),
93
+ })),
94
+ });
95
+ if (room) {
96
+ rooms.push({
97
+ number: s.number,
98
+ name: room.name,
99
+ className: classBySpecies.get(room.superclass)?.name ?? "",
100
+ picture: prop(room, "picture") ?? -1,
101
+ noun: prop(room, "noun") ?? 0,
102
+ uses: [...refs].sort((a, b) => a - b),
103
+ });
104
+ }
105
+ }
106
+ function roomObject(s) {
107
+ const e0 = s.exports[0];
108
+ const obj = e0?.kind === "object" ? s.objects.find((o) => o.offset === e0.offset) : undefined;
109
+ return obj && !obj.isClass && inherits(obj.superclass, roomClass) ? obj : undefined;
110
+ }
111
+ const roomNumbers = new Set(rooms.map((r) => r.number));
112
+ // Helper scripts (like 711 for 710) act on behalf of the rooms that load them.
113
+ const usedBy = new Map();
114
+ for (const r of rooms)
115
+ for (const u of r.uses)
116
+ if (!roomNumbers.has(u))
117
+ usedBy.set(u, [...(usedBy.get(u) ?? []), r.number]);
118
+ const ownersOf = (script) => (roomNumbers.has(script) ? [script] : usedBy.get(script) ?? []);
119
+ const exits = [];
120
+ const seen = new Set();
121
+ const addExit = (from, to, kind, script, where) => {
122
+ if (to <= 0 || to >= 0xffff || from === to)
123
+ return;
124
+ const key = `${from}>${to}>${kind}`;
125
+ if (seen.has(key))
126
+ return;
127
+ seen.add(key);
128
+ exits.push({ from, to, kind, script, where });
129
+ };
130
+ const newRoom = selector("newRoom");
131
+ const setScript = selector("setScript");
132
+ const register = selector("register");
133
+ // Script objects whose code calls `newRoom: register`: keyed "script:heapOffset".
134
+ const registerExits = new Map();
135
+ // Classes whose methods call `newRoom:` with one of the object's own properties.
136
+ const propertyExits = [];
137
+ for (const s of scripts) {
138
+ for (const fn of functions.get(s.number)) {
139
+ for (const send of fn.facts.sends) {
140
+ if (send.selector !== newRoom)
141
+ continue;
142
+ const arg = send.args[0] ?? {};
143
+ if (arg.const !== undefined) {
144
+ for (const from of ownersOf(s.number))
145
+ addExit(from, arg.const, "newRoom", s.number, fn.label);
146
+ }
147
+ else if (arg.prop === register && fn.owner) {
148
+ registerExits.set(`${s.number}:${fn.owner.offset}`, fn.label);
149
+ }
150
+ else if (arg.prop !== undefined && fn.owner) {
151
+ const propName = selName(arg.prop);
152
+ if (fn.owner.isClass)
153
+ propertyExits.push({ species: fn.owner.species, propName, where: fn.label, script: s.number });
154
+ else {
155
+ const to = prop(fn.owner, propName);
156
+ if (to !== undefined)
157
+ for (const from of ownersOf(s.number))
158
+ addExit(from, to, "property", s.number, fn.label);
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ for (const s of scripts) {
165
+ for (const fn of functions.get(s.number)) {
166
+ for (const send of fn.facts.sends) {
167
+ // `setScript: sExit 0 N` → sExit runs `newRoom: register` with N.
168
+ if (send.selector === setScript && send.args[0]?.heap !== undefined && send.args[2]?.const !== undefined) {
169
+ const where = registerExits.get(`${s.number}:${send.args[0].heap}`);
170
+ if (where)
171
+ for (const from of ownersOf(s.number))
172
+ addExit(from, send.args[2].const, "script", s.number, where);
173
+ }
174
+ }
175
+ // Rooms that check where the hero came from can be entered from there.
176
+ if (roomNumbers.has(s.number)) {
177
+ for (const c of fn.facts.compares) {
178
+ if (c.global === prevRoomGlobal && c.op === "eq?" && roomNumbers.has(c.value))
179
+ addExit(c.value, s.number, "entry", s.number, fn.label);
180
+ }
181
+ }
182
+ }
183
+ for (const o of s.objects) {
184
+ for (const pe of propertyExits) {
185
+ if (o.isClass || !inherits(o.superclass, classBySpecies.get(pe.species).name))
186
+ continue;
187
+ const to = prop(o, pe.propName);
188
+ if (to !== undefined)
189
+ for (const from of ownersOf(s.number))
190
+ addExit(from, to, "property", pe.script, `${o.name} (${pe.where})`);
191
+ }
192
+ }
193
+ const room = roomObject(s);
194
+ if (room) {
195
+ for (const d of DIRECTIONS) {
196
+ const to = prop(room, d);
197
+ if (to)
198
+ addExit(s.number, to, "direction", s.number, `${room.name}.${d}`);
199
+ }
200
+ }
201
+ }
202
+ const verbs = {};
203
+ const rank = (name) => (/^icon[A-Z]/.test(name) ? 0 : /Spell$/.test(name) ? 1 : /^the[A-Z]/.test(name) ? 2 : 3);
204
+ const pretty = (name) => name.replace(/^icon|^the|Spell$/g, "").replace(/^./, (c) => c.toUpperCase());
205
+ const best = new Map();
206
+ for (const info of scriptInfos) {
207
+ for (const o of info.objects) {
208
+ const verb = o.props.find((p) => p.name === "message")?.value;
209
+ if (!verb || verb >= 0x8000)
210
+ continue;
211
+ const current = best.get(verb);
212
+ if (current === undefined || rank(o.name) < rank(current))
213
+ best.set(verb, o.name);
214
+ }
215
+ }
216
+ for (const [verb, name] of best)
217
+ verbs[verb] = pretty(name);
218
+ const resolved = exits.filter((e) => roomNumbers.has(e.to));
219
+ const unresolved = exits.filter((e) => !roomNumbers.has(e.to));
220
+ return { rooms, exits: resolved, unresolved, classes: classList, scripts: scriptInfos, verbs };
221
+ }