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,69 @@
1
+ /**
2
+ * A message is addressed by (noun, verb, condition, sequence), all scoped to a room/module
3
+ * number (the resource number):
4
+ * noun the thing involved (an object, a character, the room itself)
5
+ * verb what the player did to it (look, do, talk, use an inventory item...)
6
+ * condition a game-state variant (e.g. day vs night, class, puzzle solved)
7
+ * sequence 1, 2, 3... for multi-line exchanges
8
+ */
9
+ export interface MessageTuple {
10
+ noun: number;
11
+ verb: number;
12
+ cond: number;
13
+ seq: number;
14
+ }
15
+ export interface MessageRecord extends MessageTuple {
16
+ /** Who says it. Talker ids are defined by the game scripts (e.g. 99 narrator, 97 hero). */
17
+ talker: number;
18
+ text: string;
19
+ /** Another message to use instead of this one. See `hasRef`. */
20
+ ref: MessageTuple;
21
+ }
22
+ export interface MessageFile {
23
+ version: number;
24
+ records: MessageRecord[];
25
+ /**
26
+ * Bytes after the strings, kept only so files round-trip: the engine never reads them.
27
+ * `trailerCount` is the header's entry count; the entries look like editor bookkeeping
28
+ * (many start with a 4-byte timestamp) and aren't fixed-size.
29
+ */
30
+ trailer?: Uint8Array;
31
+ trailerCount?: number;
32
+ }
33
+ /**
34
+ * SCI1.1/SCI32 message resource (versions 4000–5000 share this layout):
35
+ *
36
+ * +0 u32 version
37
+ * +4 u16 end of the strings, counted from +6 +6 u16 trailer entry count
38
+ * +8 u16 record count
39
+ * +10 records, 11 bytes each:
40
+ * noun, verb, cond, seq, talker, u16 string offset, refNoun, refVerb, refCond, refSeq
41
+ * NUL-terminated Latin-1 strings, one per record, in record order
42
+ * trailer (unused by the engine; editor bookkeeping)
43
+ *
44
+ * Measured on all 152 message files of an SCI2 game (versions 4210, 4321, 4341, 5000): no gaps, no
45
+ * shared strings, so `writeMessages` reproduces them byte for byte.
46
+ */
47
+ export declare function parseMessages(data: Uint8Array): MessageFile;
48
+ /** The binary resource for a message file (the inverse of `parseMessages`). */
49
+ export declare function writeMessages(file: MessageFile): Uint8Array;
50
+ /**
51
+ * Message files as text, one record per line, for editing and mods:
52
+ *
53
+ * messages 770 version 4321
54
+ * ; noun verb cond seq talker "text" [-> noun verb cond seq]
55
+ * 4 1 0 1 99 "You see an old stone altar."
56
+ * 5 1 0 1 99 "" -> 4 1 0 1
57
+ * trailer 134 005e7c962c2c0000...
58
+ *
59
+ * `labels` can annotate lines with comments (e.g. verb names), which parsing ignores.
60
+ */
61
+ export declare function messagesToText(number: number, file: MessageFile, labels?: (r: MessageRecord) => string | undefined): string;
62
+ export declare function messagesFromText(text: string): {
63
+ number: number;
64
+ file: MessageFile;
65
+ };
66
+ /** Noun 0 means "the room itself", so a reference is any non-zero noun, verb or condition. */
67
+ export declare const hasRef: (r: MessageRecord) => boolean;
68
+ /** Finds a message, following references (bounded, in case of cycles). */
69
+ export declare function findMessage(file: MessageFile, tuple: MessageTuple): MessageRecord | undefined;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * SCI1.1/SCI32 message resource (versions 4000–5000 share this layout):
3
+ *
4
+ * +0 u32 version
5
+ * +4 u16 end of the strings, counted from +6 +6 u16 trailer entry count
6
+ * +8 u16 record count
7
+ * +10 records, 11 bytes each:
8
+ * noun, verb, cond, seq, talker, u16 string offset, refNoun, refVerb, refCond, refSeq
9
+ * NUL-terminated Latin-1 strings, one per record, in record order
10
+ * trailer (unused by the engine; editor bookkeeping)
11
+ *
12
+ * Measured on all 152 message files of an SCI2 game (versions 4210, 4321, 4341, 5000): no gaps, no
13
+ * shared strings, so `writeMessages` reproduces them byte for byte.
14
+ */
15
+ export function parseMessages(data) {
16
+ const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
17
+ const version = v.getUint32(0, true);
18
+ if (version < 4000)
19
+ throw new Error(`Message version ${version} not supported`);
20
+ const decoder = new TextDecoder("latin1");
21
+ const count = v.getUint16(8, true);
22
+ const records = [];
23
+ let stringsEnd = 10 + count * 11;
24
+ for (let i = 0; i < count; i++) {
25
+ const r = 10 + i * 11;
26
+ const start = v.getUint16(r + 5, true);
27
+ let end = start;
28
+ while (end < data.length && data[end] !== 0)
29
+ end++;
30
+ stringsEnd = Math.max(stringsEnd, end + 1);
31
+ records.push({
32
+ noun: data[r],
33
+ verb: data[r + 1],
34
+ cond: data[r + 2],
35
+ seq: data[r + 3],
36
+ talker: data[r + 4],
37
+ text: decoder.decode(data.subarray(start, end)),
38
+ ref: { noun: data[r + 7], verb: data[r + 8], cond: data[r + 9], seq: data[r + 10] },
39
+ });
40
+ }
41
+ return { version, records, trailer: data.slice(stringsEnd), trailerCount: v.getUint16(6, true) };
42
+ }
43
+ const encoder = (text) => Uint8Array.from(text, (c) => c.charCodeAt(0) & 0xff);
44
+ /** The binary resource for a message file (the inverse of `parseMessages`). */
45
+ export function writeMessages(file) {
46
+ const strings = file.records.map((r) => encoder(r.text));
47
+ const recordsEnd = 10 + file.records.length * 11;
48
+ const stringsEnd = recordsEnd + strings.reduce((n, s) => n + s.length + 1, 0);
49
+ const trailer = file.trailer ?? new Uint8Array(0);
50
+ const out = new Uint8Array(stringsEnd + trailer.length);
51
+ const v = new DataView(out.buffer);
52
+ v.setUint32(0, file.version, true);
53
+ v.setUint16(4, stringsEnd - 6, true);
54
+ v.setUint16(6, file.trailerCount ?? 0, true);
55
+ v.setUint16(8, file.records.length, true);
56
+ let at = recordsEnd;
57
+ file.records.forEach((r, i) => {
58
+ const o = 10 + i * 11;
59
+ out.set([r.noun, r.verb, r.cond, r.seq, r.talker], o);
60
+ v.setUint16(o + 5, at, true);
61
+ out.set([r.ref.noun, r.ref.verb, r.ref.cond, r.ref.seq], o + 7);
62
+ out.set(strings[i], at);
63
+ at += strings[i].length + 1;
64
+ });
65
+ if (stringsEnd > 0xffff)
66
+ throw new Error("Message file too large (string offsets are 16-bit)");
67
+ out.set(trailer, stringsEnd);
68
+ return out;
69
+ }
70
+ /**
71
+ * Message files as text, one record per line, for editing and mods:
72
+ *
73
+ * messages 770 version 4321
74
+ * ; noun verb cond seq talker "text" [-> noun verb cond seq]
75
+ * 4 1 0 1 99 "You see an old stone altar."
76
+ * 5 1 0 1 99 "" -> 4 1 0 1
77
+ * trailer 134 005e7c962c2c0000...
78
+ *
79
+ * `labels` can annotate lines with comments (e.g. verb names), which parsing ignores.
80
+ */
81
+ export function messagesToText(number, file, labels) {
82
+ const lines = [`messages ${number} version ${file.version}`, "; noun verb cond seq talker \"text\" [-> noun verb cond seq]"];
83
+ for (const r of file.records) {
84
+ const ref = r.ref.noun || r.ref.verb || r.ref.cond || r.ref.seq ? ` -> ${r.ref.noun} ${r.ref.verb} ${r.ref.cond} ${r.ref.seq}` : "";
85
+ const note = labels?.(r);
86
+ lines.push(`${r.noun} ${r.verb} ${r.cond} ${r.seq} ${r.talker} ${quoteText(r.text)}${ref}${note ? ` ; ${note}` : ""}`);
87
+ }
88
+ if (file.trailer?.length || file.trailerCount) {
89
+ lines.push(`trailer ${file.trailerCount ?? 0} ${[...(file.trailer ?? [])].map((b) => b.toString(16).padStart(2, "0")).join("")}`);
90
+ }
91
+ return `${lines.join("\n")}\n`;
92
+ }
93
+ export function messagesFromText(text) {
94
+ let number;
95
+ const file = { version: 4321, records: [] };
96
+ text.split("\n").forEach((raw, i) => {
97
+ const line = i + 1;
98
+ const fail = (why) => {
99
+ throw new Error(`line ${line}: ${why}`);
100
+ };
101
+ const src = raw.trim();
102
+ if (!src || src.startsWith(";"))
103
+ return;
104
+ let m = /^messages\s+(\d+)\s+version\s+(\d+)$/.exec(src);
105
+ if (m) {
106
+ number = Number(m[1]);
107
+ file.version = Number(m[2]);
108
+ return;
109
+ }
110
+ m = /^trailer\s+(\d+)\s*([0-9a-f]*)$/i.exec(src);
111
+ if (m) {
112
+ file.trailerCount = Number(m[1]);
113
+ file.trailer = Uint8Array.from(m[2].match(/../g) ?? [], (h) => parseInt(h, 16));
114
+ return;
115
+ }
116
+ m = /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+"((?:[^"\\]|\\.)*)"\s*(?:->\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+))?\s*(?:;.*)?$/.exec(src);
117
+ if (!m)
118
+ fail("expected: noun verb cond seq talker \"text\" [-> noun verb cond seq]");
119
+ const [n, v, c, s, t, body, rn = "0", rv = "0", rc = "0", rs = "0"] = m.slice(1);
120
+ const byte = (x, what) => {
121
+ const value = Number(x);
122
+ if (value > 255)
123
+ fail(`${what} ${value} doesn't fit in a byte`);
124
+ return value;
125
+ };
126
+ file.records.push({
127
+ noun: byte(n, "noun"), verb: byte(v, "verb"), cond: byte(c, "cond"), seq: byte(s, "seq"), talker: byte(t, "talker"),
128
+ text: unquoteText(body),
129
+ ref: { noun: byte(rn, "ref noun"), verb: byte(rv, "ref verb"), cond: byte(rc, "ref cond"), seq: byte(rs, "ref seq") },
130
+ });
131
+ });
132
+ if (number === undefined)
133
+ throw new Error("missing `messages N version V`");
134
+ return { number, file };
135
+ }
136
+ function quoteText(text) {
137
+ let s = '"';
138
+ for (const ch of text) {
139
+ const c = ch.charCodeAt(0);
140
+ if (ch === '"')
141
+ s += '\\"';
142
+ else if (ch === "\\")
143
+ s += "\\\\";
144
+ else if (ch === "\n")
145
+ s += "\\n";
146
+ else if (ch === "\r")
147
+ s += "\\r";
148
+ else if (c >= 0x20 && c < 0x7f)
149
+ s += ch;
150
+ else
151
+ s += `\\x${c.toString(16).padStart(2, "0")}`;
152
+ }
153
+ return `${s}"`;
154
+ }
155
+ function unquoteText(body) {
156
+ return body.replace(/\\(x[0-9a-fA-F]{2}|.)/g, (_, e) => e === "n" ? "\n" : e === "r" ? "\r" : e.startsWith("x") && e.length === 3 ? String.fromCharCode(parseInt(e.slice(1), 16)) : e);
157
+ }
158
+ /** Noun 0 means "the room itself", so a reference is any non-zero noun, verb or condition. */
159
+ export const hasRef = (r) => r.ref.noun !== 0 || r.ref.verb !== 0 || r.ref.cond !== 0;
160
+ const same = (a, b) => a.noun === b.noun && a.verb === b.verb && a.cond === b.cond && a.seq === b.seq;
161
+ /** Finds a message, following references (bounded, in case of cycles). */
162
+ export function findMessage(file, tuple) {
163
+ let t = tuple;
164
+ for (let hops = 0; hops < 8; hops++) {
165
+ const rec = file.records.find((r) => same(r, t));
166
+ if (!rec || !hasRef(rec))
167
+ return rec;
168
+ t = rec.ref;
169
+ }
170
+ return undefined;
171
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * A message is addressed by (noun, verb, condition, sequence), all scoped to a room/module
3
+ * number (the resource number):
4
+ * noun the thing involved (an object, a character, the room itself)
5
+ * verb what the player did to it (look, do, talk, use an inventory item...)
6
+ * condition a game-state variant (e.g. day vs night, class, puzzle solved)
7
+ * sequence 1, 2, 3... for multi-line exchanges
8
+ */
9
+ export interface MessageTuple {
10
+ noun: number;
11
+ verb: number;
12
+ cond: number;
13
+ seq: number;
14
+ }
15
+
16
+ export interface MessageRecord extends MessageTuple {
17
+ /** Who says it. Talker ids are defined by the game scripts (e.g. 99 narrator, 97 hero). */
18
+ talker: number;
19
+ text: string;
20
+ /** Another message to use instead of this one. See `hasRef`. */
21
+ ref: MessageTuple;
22
+ }
23
+
24
+ export interface MessageFile {
25
+ version: number;
26
+ records: MessageRecord[];
27
+ /**
28
+ * Bytes after the strings, kept only so files round-trip: the engine never reads them.
29
+ * `trailerCount` is the header's entry count; the entries look like editor bookkeeping
30
+ * (many start with a 4-byte timestamp) and aren't fixed-size.
31
+ */
32
+ trailer?: Uint8Array;
33
+ trailerCount?: number;
34
+ }
35
+
36
+ /**
37
+ * SCI1.1/SCI32 message resource (versions 4000–5000 share this layout):
38
+ *
39
+ * +0 u32 version
40
+ * +4 u16 end of the strings, counted from +6 +6 u16 trailer entry count
41
+ * +8 u16 record count
42
+ * +10 records, 11 bytes each:
43
+ * noun, verb, cond, seq, talker, u16 string offset, refNoun, refVerb, refCond, refSeq
44
+ * NUL-terminated Latin-1 strings, one per record, in record order
45
+ * trailer (unused by the engine; editor bookkeeping)
46
+ *
47
+ * Measured on all 152 message files of an SCI2 game (versions 4210, 4321, 4341, 5000): no gaps, no
48
+ * shared strings, so `writeMessages` reproduces them byte for byte.
49
+ */
50
+ export function parseMessages(data: Uint8Array): MessageFile {
51
+ const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
52
+ const version = v.getUint32(0, true);
53
+ if (version < 4000) throw new Error(`Message version ${version} not supported`);
54
+
55
+ const decoder = new TextDecoder("latin1");
56
+ const count = v.getUint16(8, true);
57
+ const records: MessageRecord[] = [];
58
+
59
+ let stringsEnd = 10 + count * 11;
60
+ for (let i = 0; i < count; i++) {
61
+ const r = 10 + i * 11;
62
+ const start = v.getUint16(r + 5, true);
63
+ let end = start;
64
+ while (end < data.length && data[end] !== 0) end++;
65
+ stringsEnd = Math.max(stringsEnd, end + 1);
66
+ records.push({
67
+ noun: data[r]!,
68
+ verb: data[r + 1]!,
69
+ cond: data[r + 2]!,
70
+ seq: data[r + 3]!,
71
+ talker: data[r + 4]!,
72
+ text: decoder.decode(data.subarray(start, end)),
73
+ ref: { noun: data[r + 7]!, verb: data[r + 8]!, cond: data[r + 9]!, seq: data[r + 10]! },
74
+ });
75
+ }
76
+ return { version, records, trailer: data.slice(stringsEnd), trailerCount: v.getUint16(6, true) };
77
+ }
78
+
79
+ const encoder = (text: string) => Uint8Array.from(text, (c) => c.charCodeAt(0) & 0xff);
80
+
81
+ /** The binary resource for a message file (the inverse of `parseMessages`). */
82
+ export function writeMessages(file: MessageFile): Uint8Array {
83
+ const strings = file.records.map((r) => encoder(r.text));
84
+ const recordsEnd = 10 + file.records.length * 11;
85
+ const stringsEnd = recordsEnd + strings.reduce((n, s) => n + s.length + 1, 0);
86
+ const trailer = file.trailer ?? new Uint8Array(0);
87
+ const out = new Uint8Array(stringsEnd + trailer.length);
88
+ const v = new DataView(out.buffer);
89
+ v.setUint32(0, file.version, true);
90
+ v.setUint16(4, stringsEnd - 6, true);
91
+ v.setUint16(6, file.trailerCount ?? 0, true);
92
+ v.setUint16(8, file.records.length, true);
93
+ let at = recordsEnd;
94
+ file.records.forEach((r, i) => {
95
+ const o = 10 + i * 11;
96
+ out.set([r.noun, r.verb, r.cond, r.seq, r.talker], o);
97
+ v.setUint16(o + 5, at, true);
98
+ out.set([r.ref.noun, r.ref.verb, r.ref.cond, r.ref.seq], o + 7);
99
+ out.set(strings[i]!, at);
100
+ at += strings[i]!.length + 1;
101
+ });
102
+ if (stringsEnd > 0xffff) throw new Error("Message file too large (string offsets are 16-bit)");
103
+ out.set(trailer, stringsEnd);
104
+ return out;
105
+ }
106
+
107
+ /**
108
+ * Message files as text, one record per line, for editing and mods:
109
+ *
110
+ * messages 770 version 4321
111
+ * ; noun verb cond seq talker "text" [-> noun verb cond seq]
112
+ * 4 1 0 1 99 "You see an old stone altar."
113
+ * 5 1 0 1 99 "" -> 4 1 0 1
114
+ * trailer 134 005e7c962c2c0000...
115
+ *
116
+ * `labels` can annotate lines with comments (e.g. verb names), which parsing ignores.
117
+ */
118
+ export function messagesToText(number: number, file: MessageFile, labels?: (r: MessageRecord) => string | undefined): string {
119
+ const lines = [`messages ${number} version ${file.version}`, "; noun verb cond seq talker \"text\" [-> noun verb cond seq]"];
120
+ for (const r of file.records) {
121
+ const ref = r.ref.noun || r.ref.verb || r.ref.cond || r.ref.seq ? ` -> ${r.ref.noun} ${r.ref.verb} ${r.ref.cond} ${r.ref.seq}` : "";
122
+ const note = labels?.(r);
123
+ lines.push(`${r.noun} ${r.verb} ${r.cond} ${r.seq} ${r.talker} ${quoteText(r.text)}${ref}${note ? ` ; ${note}` : ""}`);
124
+ }
125
+ if (file.trailer?.length || file.trailerCount) {
126
+ lines.push(`trailer ${file.trailerCount ?? 0} ${[...(file.trailer ?? [])].map((b) => b.toString(16).padStart(2, "0")).join("")}`);
127
+ }
128
+ return `${lines.join("\n")}\n`;
129
+ }
130
+
131
+ export function messagesFromText(text: string): { number: number; file: MessageFile } {
132
+ let number: number | undefined;
133
+ const file: MessageFile = { version: 4321, records: [] };
134
+ text.split("\n").forEach((raw, i) => {
135
+ const line = i + 1;
136
+ const fail = (why: string): never => {
137
+ throw new Error(`line ${line}: ${why}`);
138
+ };
139
+ const src = raw.trim();
140
+ if (!src || src.startsWith(";")) return;
141
+ let m = /^messages\s+(\d+)\s+version\s+(\d+)$/.exec(src);
142
+ if (m) {
143
+ number = Number(m[1]);
144
+ file.version = Number(m[2]);
145
+ return;
146
+ }
147
+ m = /^trailer\s+(\d+)\s*([0-9a-f]*)$/i.exec(src);
148
+ if (m) {
149
+ file.trailerCount = Number(m[1]);
150
+ file.trailer = Uint8Array.from(m[2]!.match(/../g) ?? [], (h) => parseInt(h, 16));
151
+ return;
152
+ }
153
+ m = /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+"((?:[^"\\]|\\.)*)"\s*(?:->\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+))?\s*(?:;.*)?$/.exec(src);
154
+ if (!m) fail("expected: noun verb cond seq talker \"text\" [-> noun verb cond seq]");
155
+ const [n, v, c, s, t, body, rn = "0", rv = "0", rc = "0", rs = "0"] = m!.slice(1);
156
+ const byte = (x: string, what: string) => {
157
+ const value = Number(x);
158
+ if (value > 255) fail(`${what} ${value} doesn't fit in a byte`);
159
+ return value;
160
+ };
161
+ file.records.push({
162
+ noun: byte(n!, "noun"), verb: byte(v!, "verb"), cond: byte(c!, "cond"), seq: byte(s!, "seq"), talker: byte(t!, "talker"),
163
+ text: unquoteText(body!),
164
+ ref: { noun: byte(rn, "ref noun"), verb: byte(rv, "ref verb"), cond: byte(rc, "ref cond"), seq: byte(rs, "ref seq") },
165
+ });
166
+ });
167
+ if (number === undefined) throw new Error("missing `messages N version V`");
168
+ return { number, file };
169
+ }
170
+
171
+ function quoteText(text: string): string {
172
+ let s = '"';
173
+ for (const ch of text) {
174
+ const c = ch.charCodeAt(0);
175
+ if (ch === '"') s += '\\"';
176
+ else if (ch === "\\") s += "\\\\";
177
+ else if (ch === "\n") s += "\\n";
178
+ else if (ch === "\r") s += "\\r";
179
+ else if (c >= 0x20 && c < 0x7f) s += ch;
180
+ else s += `\\x${c.toString(16).padStart(2, "0")}`;
181
+ }
182
+ return `${s}"`;
183
+ }
184
+
185
+ function unquoteText(body: string): string {
186
+ return body.replace(/\\(x[0-9a-fA-F]{2}|.)/g, (_, e: string) =>
187
+ e === "n" ? "\n" : e === "r" ? "\r" : e.startsWith("x") && e.length === 3 ? String.fromCharCode(parseInt(e.slice(1), 16)) : e);
188
+ }
189
+
190
+ /** Noun 0 means "the room itself", so a reference is any non-zero noun, verb or condition. */
191
+ export const hasRef = (r: MessageRecord): boolean => r.ref.noun !== 0 || r.ref.verb !== 0 || r.ref.cond !== 0;
192
+
193
+ const same = (a: MessageTuple, b: MessageTuple) =>
194
+ a.noun === b.noun && a.verb === b.verb && a.cond === b.cond && a.seq === b.seq;
195
+
196
+ /** Finds a message, following references (bounded, in case of cycles). */
197
+ export function findMessage(file: MessageFile, tuple: MessageTuple): MessageRecord | undefined {
198
+ let t = tuple;
199
+ for (let hops = 0; hops < 8; hops++) {
200
+ const rec = file.records.find((r) => same(r, t));
201
+ if (!rec || !hasRef(rec)) return rec;
202
+ t = rec.ref;
203
+ }
204
+ return undefined;
205
+ }
@@ -0,0 +1,19 @@
1
+ import type { SciArray } from "../memory.ts";
2
+ import type { KernelFn, Vm } from "../vm.ts";
3
+ import { type Value } from "../value.ts";
4
+ /** Resolves an array argument: an array ref, or a String/Array object holding one in `data`. */
5
+ declare function resolve(vm: Vm, v: Value): SciArray | undefined;
6
+ declare const str: (vm: Vm, v: Value) => string;
7
+ declare function newString(vm: Vm, s: string): Value;
8
+ declare function writeString(vm: Vm, ref: Value, s: string): Value;
9
+ /** printf-style formatting, as SCI's StringFormat understands it. */
10
+ export declare function sciFormat(vm: Vm, format: string, args: Value[]): string;
11
+ /** SCI32 dynamic arrays and strings. */
12
+ export declare const arrayKernels: Record<string, KernelFn>;
13
+ export declare const stringHelpers: {
14
+ str: typeof str;
15
+ newString: typeof newString;
16
+ writeString: typeof writeString;
17
+ resolve: typeof resolve;
18
+ };
19
+ export {};
@@ -0,0 +1,182 @@
1
+ import { NULL, fromInt, isNumber, segmentOf, toSigned } from "../value.js";
2
+ const arrayTypes = ["int16", "id", "byte", "string"];
3
+ /** Resolves an array argument: an array ref, or a String/Array object holding one in `data`. */
4
+ function resolve(vm, v) {
5
+ const direct = vm.memory.array(v);
6
+ if (direct)
7
+ return direct;
8
+ const obj = vm.memory.object(v);
9
+ if (obj) {
10
+ const data = vm.getProp(obj, "data");
11
+ if (data !== undefined)
12
+ return vm.memory.array(data);
13
+ }
14
+ return undefined;
15
+ }
16
+ const store = (a, value) => (a.type === "byte" || a.type === "string" ? value & 0xff : value);
17
+ function setElements(vm, [ref = 0, index = 0, ...values]) {
18
+ const a = resolve(vm, ref);
19
+ if (!a)
20
+ return ref;
21
+ const start = toSigned(index);
22
+ values.forEach((v, i) => {
23
+ a.data[start + i] = store(a, v);
24
+ });
25
+ for (let i = 0; i < a.data.length; i++)
26
+ a.data[i] ??= 0;
27
+ return ref;
28
+ }
29
+ /** Byte values of a string argument: literal in a script heap or an array. */
30
+ function chars(vm, v) {
31
+ const a = resolve(vm, v);
32
+ if (a) {
33
+ const out = [];
34
+ for (const c of a.data) {
35
+ if ((c & 0xff) === 0)
36
+ break;
37
+ out.push(c & 0xff);
38
+ }
39
+ return out;
40
+ }
41
+ if (!v)
42
+ return [];
43
+ return [...vm.memory.string(v)].map((c) => c.charCodeAt(0));
44
+ }
45
+ const str = (vm, v) => String.fromCharCode(...chars(vm, v));
46
+ function newString(vm, s) {
47
+ const ref = vm.memory.newArray("string", s.length + 1);
48
+ const a = vm.memory.array(ref);
49
+ for (let i = 0; i < s.length; i++)
50
+ a.data[i] = s.charCodeAt(i) & 0xff;
51
+ return ref;
52
+ }
53
+ function writeString(vm, ref, s) {
54
+ const a = resolve(vm, ref);
55
+ if (!a)
56
+ return ref;
57
+ a.data.length = 0;
58
+ for (let i = 0; i < s.length; i++)
59
+ a.data.push(s.charCodeAt(i) & 0xff);
60
+ a.data.push(0);
61
+ return ref;
62
+ }
63
+ function copy(vm, [dst = 0, dstIndex = 0, src = 0, srcIndex = 0, count = 0xffff]) {
64
+ const d = resolve(vm, dst);
65
+ if (!d)
66
+ return dst;
67
+ const s = resolve(vm, src);
68
+ const from = s ? s.data : chars(vm, src).concat(0);
69
+ const start = toSigned(srcIndex);
70
+ let n = toSigned(count);
71
+ if (n < 0)
72
+ n = from.length - start;
73
+ for (let i = 0; i < n && start + i < from.length; i++)
74
+ d.data[toSigned(dstIndex) + i] = store(d, from[start + i]);
75
+ for (let i = 0; i < d.data.length; i++)
76
+ d.data[i] ??= 0;
77
+ return dst;
78
+ }
79
+ /** printf-style formatting, as SCI's StringFormat understands it. */
80
+ export function sciFormat(vm, format, args) {
81
+ let argi = 0;
82
+ return format.replace(/%(-?)(0?)(\d*)([dsuxXc%])/g, (_m, left, zero, width, conv) => {
83
+ if (conv === "%")
84
+ return "%";
85
+ const arg = args[argi++] ?? 0;
86
+ let out;
87
+ switch (conv) {
88
+ case "d":
89
+ out = String(toSigned(arg));
90
+ break;
91
+ case "u":
92
+ out = String(arg & 0xffff);
93
+ break;
94
+ case "x":
95
+ out = (arg & 0xffff).toString(16);
96
+ break;
97
+ case "X":
98
+ out = (arg & 0xffff).toString(16).toUpperCase();
99
+ break;
100
+ case "c":
101
+ out = String.fromCharCode(arg & 0xff);
102
+ break;
103
+ default: out = isNumber(arg) && arg !== 0 ? String(toSigned(arg)) : str(vm, arg);
104
+ }
105
+ const w = Number(width || 0);
106
+ return left ? out.padEnd(w) : out.padStart(w, zero ? "0" : " ");
107
+ });
108
+ }
109
+ const arraySubops = {
110
+ 0: (vm, [size = 0, type = 0]) => vm.memory.newArray(arrayTypes[type] ?? "int16", size),
111
+ 1: (vm, [ref = 0]) => resolve(vm, ref)?.data.length ?? 0,
112
+ 2: (vm, [ref = 0, index = 0]) => resolve(vm, ref)?.data[toSigned(index)] ?? 0,
113
+ 3: setElements,
114
+ 4: (vm, [ref = 0]) => {
115
+ if (vm.memory.array(ref))
116
+ vm.memory.free(segmentOf(ref));
117
+ return 0;
118
+ },
119
+ 5: (vm, [ref = 0, index = 0, count = 0, value = 0]) => {
120
+ const a = resolve(vm, ref);
121
+ if (a)
122
+ for (let i = 0; i < count; i++)
123
+ a.data[toSigned(index) + i] = store(a, value);
124
+ if (a)
125
+ for (let i = 0; i < a.data.length; i++)
126
+ a.data[i] ??= 0;
127
+ return ref;
128
+ },
129
+ 6: copy,
130
+ 8: (vm, [ref = 0]) => {
131
+ const a = resolve(vm, ref);
132
+ if (!a)
133
+ return NULL;
134
+ const dup = vm.memory.newArray(a.type, 0);
135
+ vm.memory.array(dup).data = a.data.slice();
136
+ return dup;
137
+ },
138
+ 9: (vm, [ref = 0]) => {
139
+ const obj = vm.memory.object(ref);
140
+ return obj ? (vm.getProp(obj, "data") ?? ref) : ref;
141
+ },
142
+ };
143
+ const stringSubops = {
144
+ ...arraySubops,
145
+ 0: (vm, [size = 0]) => vm.memory.newArray("string", size),
146
+ // Duplicate: also takes a literal string from a script's heap (e.g. `Prints "text"`).
147
+ 8: (vm, args) => (resolve(vm, args[0] ?? 0) ? arraySubops[8](vm, args) : newString(vm, str(vm, args[0] ?? 0))),
148
+ 7: (vm, [a = 0, b = 0, n]) => {
149
+ let x = str(vm, a), y = str(vm, b);
150
+ if (n !== undefined)
151
+ (x = x.slice(0, n), (y = y.slice(0, n)));
152
+ return fromInt(x < y ? -1 : x > y ? 1 : 0);
153
+ },
154
+ 10: (vm, [s = 0]) => chars(vm, s).length,
155
+ 11: (vm, [format = 0, ...args]) => newString(vm, sciFormat(vm, str(vm, format), args)),
156
+ 12: (vm, [dst = 0, format = 0, ...args]) => writeString(vm, dst, sciFormat(vm, str(vm, format), args)),
157
+ 13: (vm, [s = 0]) => fromInt(parseInt(str(vm, s), 10) || 0),
158
+ 14: (vm, [s = 0, flags = 0]) => {
159
+ let t = str(vm, s);
160
+ if (flags & 1)
161
+ t = t.trimStart();
162
+ if (flags & 2)
163
+ t = t.trimEnd();
164
+ if (flags & 4)
165
+ t = t.replace(/ /g, "");
166
+ return writeString(vm, s, t);
167
+ },
168
+ 15: (vm, [s = 0]) => writeString(vm, s, str(vm, s).toUpperCase()),
169
+ 16: (vm, [s = 0]) => writeString(vm, s, str(vm, s).toLowerCase()),
170
+ };
171
+ const dispatch = (name, table) => (vm, [op = 0, ...args]) => {
172
+ const fn = table[op];
173
+ if (!fn)
174
+ throw new Error(`${name} subop ${op} not implemented`);
175
+ return fn(vm, args);
176
+ };
177
+ /** SCI32 dynamic arrays and strings. */
178
+ export const arrayKernels = {
179
+ Array: dispatch("Array", arraySubops),
180
+ String: dispatch("String", stringSubops),
181
+ };
182
+ export const stringHelpers = { str, newString, writeString, resolve };