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,173 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { compileRoom } from "../packages/content/src/index.js";
6
+ import { picFromPngs } from "./pic-build.js";
7
+ import { loadTarget } from "./mod-target.js";
8
+ import { viewFromManifest } from "./view-build.js";
9
+ import { ResourceType, analyzeGame, assemble, messagesFromText, messagesToText, parseMessages, toAssembly, writeMessages, writePic, writeView, } from "../packages/sci/src/index.js";
10
+ /** Message file text with each record annotated: verb name, and the room objects using the noun. */
11
+ export async function dumpMessages(rm, world, n) {
12
+ const file = parseMessages((await rm.load({ type: ResourceType.Message, number: n })).data);
13
+ const index = await analyzeGame(rm, { world });
14
+ const nounOwners = new Map();
15
+ for (const o of index.scripts.find((s) => s.number === n)?.objects ?? []) {
16
+ const noun = o.props.find((p) => p.name === "noun")?.value;
17
+ if (noun)
18
+ nounOwners.set(noun, [...(nounOwners.get(noun) ?? []), o.name]);
19
+ }
20
+ return messagesToText(n, file, (r) => [index.verbs[r.verb], nounOwners.get(r.noun)?.join("/")].filter(Boolean).join(" · ") || undefined);
21
+ }
22
+ /**
23
+ * Builds a mod directory: <n>.sca / <n>.msg are whole new files (a new room);
24
+ * <n>.sca.diff / <n>.msg.diff change the game's own, applied to fresh dumps with `patch`;
25
+ * <n>.png (+ <n>.layer-<priority>.png) is a room picture; *.view.json is a view;
26
+ * <n>.room.yaml (+ <n>.yarn) is a room described as data, compiled to <n>.sca and <n>.msg
27
+ * (@sci-ts/content). `generated` receives that compiled text, for reading. Their Yarn
28
+ * variables are flags numbered in the mod's flags.yaml (see modFlags). Rooms are compiled for
29
+ * the game named in the mod's mod.yaml (see tools/mod-target.ts).
30
+ */
31
+ export async function buildMod(modDir, rm, world, ctx, generated) {
32
+ const work = mkdtempSync(join(tmpdir(), "sci-mod-"));
33
+ const all = readdirSync(modDir);
34
+ const files = all.filter((f) => /^\d+\.(sca|msg)(\.diff)?$/.test(f));
35
+ const pictures = all.filter((f) => /^\d+\.png$/.test(f));
36
+ const views = all.filter((f) => /\.view\.json$/.test(f));
37
+ const rooms = all.filter((f) => /^\d+\.room\.yaml$/.test(f));
38
+ if (!files.length && !pictures.length && !views.length && !rooms.length) {
39
+ throw new Error(`No <n>.room.yaml, <n>.sca, <n>.msg, .diff, <n>.png or .view.json files in ${modDir}`);
40
+ }
41
+ // <n>.room.yaml + <n>.yarn: compiled to the same text a hand-written room would be.
42
+ const target = rooms.length ? await loadTarget(modDir) : undefined;
43
+ const flags = target && modFlags(modDir, target);
44
+ const sources = new Map();
45
+ // Diffs to apply per generated file: the mod's own, plus the target's support patches.
46
+ const diffs = new Map();
47
+ for (const f of files.filter((x) => x.endsWith(".diff")))
48
+ diffs.set(f.slice(0, -5), [join(modDir, f)]);
49
+ for (const file of rooms) {
50
+ const n = file.split(".")[0];
51
+ for (const kind of ["sca", "msg"]) {
52
+ if (files.includes(`${n}.${kind}`))
53
+ throw new Error(`${modDir}: both ${file} and ${n}.${kind} define room ${n}`);
54
+ }
55
+ const yarnFile = `${n}.yarn`;
56
+ const room = compileRoom(readFileSync(join(modDir, file), "utf8"), all.includes(yarnFile) ? readFileSync(join(modDir, yarnFile), "utf8") : undefined, target.content, { yaml: join(modDir, file), yarn: join(modDir, yarnFile) }, { flag: flags.number });
57
+ sources.set(`${n}.sca`, room.sca).set(`${n}.msg`, room.msg);
58
+ if (room.roomTalkers) {
59
+ // Characters: the target's patches that let the game ask the current room for talkers.
60
+ for (const [patched, diff] of Object.entries(target.roomTalkerPatches ?? {})) {
61
+ const list = diffs.get(patched) ?? [];
62
+ if (!list.includes(diff))
63
+ diffs.set(patched, [diff, ...list]);
64
+ }
65
+ }
66
+ generated?.(`${n}.sca`, room.sca);
67
+ generated?.(`${n}.msg`, room.msg);
68
+ files.push(`${n}.sca`, `${n}.msg`);
69
+ }
70
+ flags?.save();
71
+ const out = [];
72
+ // <n>.png (+ <n>.layer-<priority>.png): a room picture.
73
+ for (const file of pictures) {
74
+ const n = Number(file.split(".")[0]);
75
+ out.push({ type: ResourceType.Pic, number: n, data: writePic(await picFromPngs(modDir, n, rm)) });
76
+ }
77
+ // *.view.json: a view (sprite/animation) from PNGs. Built after pictures, which it may use.
78
+ for (const file of views) {
79
+ const { number, view } = await viewFromManifest(join(modDir, file), rmWith(rm, out));
80
+ out.push({ type: ResourceType.View, number, data: writeView(view) });
81
+ }
82
+ const targets = [...files.filter((f) => !f.endsWith(".diff")), ...[...diffs.keys()].map((f) => `${f}.diff`)];
83
+ for (const file of new Set(targets)) {
84
+ const [n, kind] = file.split(".");
85
+ let text;
86
+ if (file.endsWith(".diff")) {
87
+ const source = join(work, `${n}.${kind}`);
88
+ writeFileSync(source, kind === "sca" ? toAssembly(await world.script(Number(n)), ctx) : await dumpMessages(rm, world, Number(n)));
89
+ for (const diff of diffs.get(`${n}.${kind}`))
90
+ execFileSync("patch", ["--quiet", "--forward", source, diff], { stdio: "inherit" });
91
+ text = readFileSync(source, "utf8");
92
+ }
93
+ else
94
+ text = sources.get(file) ?? readFileSync(join(modDir, file), "utf8");
95
+ if (kind === "sca") {
96
+ const built = assemble(text, ctx);
97
+ out.push({ type: ResourceType.Script, number: built.number, data: built.code }, { type: ResourceType.Heap, number: built.number, data: built.heap });
98
+ }
99
+ else {
100
+ const { number, file: messages } = messagesFromText(text);
101
+ out.push({ type: ResourceType.Message, number, data: writeMessages(messages) });
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ /**
107
+ * A mod's flags.yaml: Yarn variable -> flag number. Saved games store flags by number, so
108
+ * a name keeps its number once given: new names get the next free one (the build adds them
109
+ * to the file; commit it with the mod). `$game<n>` is the game's own flag n.
110
+ */
111
+ export function modFlags(modDir, target) {
112
+ const file = join(modDir, "flags.yaml");
113
+ const { first, last } = target.content.flags;
114
+ const numbers = new Map();
115
+ if (existsSync(file)) {
116
+ readFileSync(file, "utf8").split("\n").forEach((raw, i) => {
117
+ const line = raw.replace(/#.*/, "").trim();
118
+ if (!line)
119
+ return;
120
+ const m = /^([A-Za-z_]\w*)\s*:\s*(\d+)$/.exec(line);
121
+ if (!m)
122
+ throw new Error(`${file}:${i + 1}: expected name: number`);
123
+ const n = Number(m[2]);
124
+ if (n < first || n > last)
125
+ throw new Error(`${file}:${i + 1}: ${n} is outside the flags mods may use (${first}-${last})`);
126
+ if ([...numbers.values()].includes(n))
127
+ throw new Error(`${file}:${i + 1}: flag ${n} is used twice`);
128
+ numbers.set(m[1], n);
129
+ });
130
+ }
131
+ let added = false;
132
+ return {
133
+ number(name) {
134
+ const game = /^game(\d+)$/.exec(name);
135
+ if (game) {
136
+ const n = Number(game[1]);
137
+ if (n > last)
138
+ throw new Error(`the game's flags go up to ${last}`);
139
+ return n;
140
+ }
141
+ let n = numbers.get(name);
142
+ if (n === undefined) {
143
+ const used = new Set(numbers.values());
144
+ for (n = first; used.has(n); n++)
145
+ ;
146
+ if (n > last)
147
+ throw new Error(`no free flags left (${first}-${last})`);
148
+ numbers.set(name, n);
149
+ added = true;
150
+ }
151
+ return n;
152
+ },
153
+ save() {
154
+ if (!added)
155
+ return;
156
+ const lines = [...numbers].sort((a, b) => a[1] - b[1]).map(([name, n]) => `${name}: ${n}`);
157
+ writeFileSync(file, [
158
+ "# Flags for the Yarn variables ($name) in these rooms, kept by the build: new names get the",
159
+ "# next free number. Saved games store flags by number, so don't renumber once people play.",
160
+ ...lines, "",
161
+ ].join("\n"));
162
+ },
163
+ };
164
+ }
165
+ /** The game's resources plus what the mod has built so far (a view may use the mod's picture). */
166
+ function rmWith(rm, built) {
167
+ return {
168
+ async load(id) {
169
+ const b = built.find((r) => r.type === id.type && r.number === id.number);
170
+ return b ? { ...id, data: b.data } : rm.load(id);
171
+ },
172
+ };
173
+ }
@@ -0,0 +1,179 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { compileRoom } from "../packages/content/src/index.ts";
6
+ import { picFromPngs } from "./pic-build.ts";
7
+ import { loadTarget, type ModTarget } from "./mod-target.ts";
8
+ import { viewFromManifest } from "./view-build.ts";
9
+ import {
10
+ ResourceType, analyzeGame, assemble, messagesFromText, messagesToText, parseMessages, toAssembly, writeMessages, writePic, writeView,
11
+ type AsmContext, type ResourceManager, type ScriptWorld,
12
+ } from "../packages/sci/src/index.ts";
13
+
14
+ export interface BuiltResource {
15
+ type: ResourceType;
16
+ number: number;
17
+ data: Uint8Array;
18
+ }
19
+
20
+ /** Message file text with each record annotated: verb name, and the room objects using the noun. */
21
+ export async function dumpMessages(rm: ResourceManager, world: ScriptWorld, n: number): Promise<string> {
22
+ const file = parseMessages((await rm.load({ type: ResourceType.Message, number: n })).data);
23
+ const index = await analyzeGame(rm, { world });
24
+ const nounOwners = new Map<number, string[]>();
25
+ for (const o of index.scripts.find((s) => s.number === n)?.objects ?? []) {
26
+ const noun = o.props.find((p) => p.name === "noun")?.value;
27
+ if (noun) nounOwners.set(noun, [...(nounOwners.get(noun) ?? []), o.name]);
28
+ }
29
+ return messagesToText(n, file, (r) => [index.verbs[r.verb], nounOwners.get(r.noun)?.join("/")].filter(Boolean).join(" · ") || undefined);
30
+ }
31
+
32
+ /**
33
+ * Builds a mod directory: <n>.sca / <n>.msg are whole new files (a new room);
34
+ * <n>.sca.diff / <n>.msg.diff change the game's own, applied to fresh dumps with `patch`;
35
+ * <n>.png (+ <n>.layer-<priority>.png) is a room picture; *.view.json is a view;
36
+ * <n>.room.yaml (+ <n>.yarn) is a room described as data, compiled to <n>.sca and <n>.msg
37
+ * (@sci-ts/content). `generated` receives that compiled text, for reading. Their Yarn
38
+ * variables are flags numbered in the mod's flags.yaml (see modFlags). Rooms are compiled for
39
+ * the game named in the mod's mod.yaml (see tools/mod-target.ts).
40
+ */
41
+ export async function buildMod(
42
+ modDir: string, rm: ResourceManager, world: ScriptWorld, ctx: AsmContext,
43
+ generated?: (file: string, text: string) => void,
44
+ ): Promise<BuiltResource[]> {
45
+ const work = mkdtempSync(join(tmpdir(), "sci-mod-"));
46
+ const all = readdirSync(modDir);
47
+ const files = all.filter((f) => /^\d+\.(sca|msg)(\.diff)?$/.test(f));
48
+ const pictures = all.filter((f) => /^\d+\.png$/.test(f));
49
+ const views = all.filter((f) => /\.view\.json$/.test(f));
50
+ const rooms = all.filter((f) => /^\d+\.room\.yaml$/.test(f));
51
+ if (!files.length && !pictures.length && !views.length && !rooms.length) {
52
+ throw new Error(`No <n>.room.yaml, <n>.sca, <n>.msg, .diff, <n>.png or .view.json files in ${modDir}`);
53
+ }
54
+ // <n>.room.yaml + <n>.yarn: compiled to the same text a hand-written room would be.
55
+ const target: ModTarget | undefined = rooms.length ? await loadTarget(modDir) : undefined;
56
+ const flags = target && modFlags(modDir, target);
57
+ const sources = new Map<string, string>();
58
+ // Diffs to apply per generated file: the mod's own, plus the target's support patches.
59
+ const diffs = new Map<string, string[]>();
60
+ for (const f of files.filter((x) => x.endsWith(".diff"))) diffs.set(f.slice(0, -5), [join(modDir, f)]);
61
+ for (const file of rooms) {
62
+ const n = file.split(".")[0]!;
63
+ for (const kind of ["sca", "msg"]) {
64
+ if (files.includes(`${n}.${kind}`)) throw new Error(`${modDir}: both ${file} and ${n}.${kind} define room ${n}`);
65
+ }
66
+ const yarnFile = `${n}.yarn`;
67
+ const room = compileRoom(
68
+ readFileSync(join(modDir, file), "utf8"),
69
+ all.includes(yarnFile) ? readFileSync(join(modDir, yarnFile), "utf8") : undefined,
70
+ target!.content,
71
+ { yaml: join(modDir, file), yarn: join(modDir, yarnFile) },
72
+ { flag: flags!.number },
73
+ );
74
+ sources.set(`${n}.sca`, room.sca).set(`${n}.msg`, room.msg);
75
+ if (room.roomTalkers) {
76
+ // Characters: the target's patches that let the game ask the current room for talkers.
77
+ for (const [patched, diff] of Object.entries(target!.roomTalkerPatches ?? {})) {
78
+ const list = diffs.get(patched) ?? [];
79
+ if (!list.includes(diff)) diffs.set(patched, [diff, ...list]);
80
+ }
81
+ }
82
+ generated?.(`${n}.sca`, room.sca);
83
+ generated?.(`${n}.msg`, room.msg);
84
+ files.push(`${n}.sca`, `${n}.msg`);
85
+ }
86
+ flags?.save();
87
+ const out: BuiltResource[] = [];
88
+ // <n>.png (+ <n>.layer-<priority>.png): a room picture.
89
+ for (const file of pictures) {
90
+ const n = Number(file.split(".")[0]);
91
+ out.push({ type: ResourceType.Pic, number: n, data: writePic(await picFromPngs(modDir, n, rm)) });
92
+ }
93
+ // *.view.json: a view (sprite/animation) from PNGs. Built after pictures, which it may use.
94
+ for (const file of views) {
95
+ const { number, view } = await viewFromManifest(join(modDir, file), rmWith(rm, out));
96
+ out.push({ type: ResourceType.View, number, data: writeView(view) });
97
+ }
98
+ const targets = [...files.filter((f) => !f.endsWith(".diff")), ...[...diffs.keys()].map((f) => `${f}.diff`)];
99
+ for (const file of new Set(targets)) {
100
+ const [n, kind] = file.split(".") as [string, "sca" | "msg"];
101
+ let text: string;
102
+ if (file.endsWith(".diff")) {
103
+ const source = join(work, `${n}.${kind}`);
104
+ writeFileSync(source, kind === "sca" ? toAssembly(await world.script(Number(n)), ctx) : await dumpMessages(rm, world, Number(n)));
105
+ for (const diff of diffs.get(`${n}.${kind}`)!) execFileSync("patch", ["--quiet", "--forward", source, diff], { stdio: "inherit" });
106
+ text = readFileSync(source, "utf8");
107
+ } else text = sources.get(file) ?? readFileSync(join(modDir, file), "utf8");
108
+ if (kind === "sca") {
109
+ const built = assemble(text, ctx);
110
+ out.push({ type: ResourceType.Script, number: built.number, data: built.code }, { type: ResourceType.Heap, number: built.number, data: built.heap });
111
+ } else {
112
+ const { number, file: messages } = messagesFromText(text);
113
+ out.push({ type: ResourceType.Message, number, data: writeMessages(messages) });
114
+ }
115
+ }
116
+ return out;
117
+ }
118
+
119
+ /**
120
+ * A mod's flags.yaml: Yarn variable -> flag number. Saved games store flags by number, so
121
+ * a name keeps its number once given: new names get the next free one (the build adds them
122
+ * to the file; commit it with the mod). `$game<n>` is the game's own flag n.
123
+ */
124
+ export function modFlags(modDir: string, target: ModTarget): { number: (name: string) => number; save: () => void } {
125
+ const file = join(modDir, "flags.yaml");
126
+ const { first, last } = target.content.flags;
127
+ const numbers = new Map<string, number>();
128
+ if (existsSync(file)) {
129
+ readFileSync(file, "utf8").split("\n").forEach((raw, i) => {
130
+ const line = raw.replace(/#.*/, "").trim();
131
+ if (!line) return;
132
+ const m = /^([A-Za-z_]\w*)\s*:\s*(\d+)$/.exec(line);
133
+ if (!m) throw new Error(`${file}:${i + 1}: expected name: number`);
134
+ const n = Number(m[2]);
135
+ if (n < first || n > last) throw new Error(`${file}:${i + 1}: ${n} is outside the flags mods may use (${first}-${last})`);
136
+ if ([...numbers.values()].includes(n)) throw new Error(`${file}:${i + 1}: flag ${n} is used twice`);
137
+ numbers.set(m[1]!, n);
138
+ });
139
+ }
140
+ let added = false;
141
+ return {
142
+ number(name) {
143
+ const game = /^game(\d+)$/.exec(name);
144
+ if (game) {
145
+ const n = Number(game[1]);
146
+ if (n > last) throw new Error(`the game's flags go up to ${last}`);
147
+ return n;
148
+ }
149
+ let n = numbers.get(name);
150
+ if (n === undefined) {
151
+ const used = new Set(numbers.values());
152
+ for (n = first; used.has(n); n++);
153
+ if (n > last) throw new Error(`no free flags left (${first}-${last})`);
154
+ numbers.set(name, n);
155
+ added = true;
156
+ }
157
+ return n;
158
+ },
159
+ save() {
160
+ if (!added) return;
161
+ const lines = [...numbers].sort((a, b) => a[1] - b[1]).map(([name, n]) => `${name}: ${n}`);
162
+ writeFileSync(file, [
163
+ "# Flags for the Yarn variables ($name) in these rooms, kept by the build: new names get the",
164
+ "# next free number. Saved games store flags by number, so don't renumber once people play.",
165
+ ...lines, "",
166
+ ].join("\n"));
167
+ },
168
+ };
169
+ }
170
+
171
+ /** The game's resources plus what the mod has built so far (a view may use the mod's picture). */
172
+ function rmWith(rm: ResourceManager, built: BuiltResource[]): ResourceManager {
173
+ return {
174
+ async load(id: { type: ResourceType; number: number }) {
175
+ const b = built.find((r) => r.type === id.type && r.number === id.number);
176
+ return b ? { ...id, data: b.data } : rm.load(id);
177
+ },
178
+ } as unknown as ResourceManager;
179
+ }
@@ -0,0 +1,16 @@
1
+ import type { Target } from "../packages/content/src/index.ts";
2
+ /**
3
+ * A game that mods can add rooms to: how the content compiler should write for it, plus
4
+ * patches to the game's own scripts that new rooms may need.
5
+ */
6
+ export interface ModTarget {
7
+ content: Target;
8
+ /**
9
+ * Applied when a room brings its own characters: generated file (e.g. "0.sca") -> diff
10
+ * that teaches the game to ask the current room for talkers numbered from
11
+ * `content.firstRoomTalker`.
12
+ */
13
+ roomTalkerPatches?: Record<string, string>;
14
+ }
15
+ /** The target named in a mod's mod.yaml (`target: <name>`): tools/targets/<name>.ts. */
16
+ export declare function loadTarget(modDir: string): Promise<ModTarget>;
@@ -0,0 +1,26 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ const TARGETS = join(import.meta.dirname, "targets");
12
+ /** The target named in a mod's mod.yaml (`target: <name>`): tools/targets/<name>.ts. */
13
+ export async function loadTarget(modDir) {
14
+ const file = join(modDir, "mod.yaml");
15
+ const known = existsSync(TARGETS) ? readdirSync(TARGETS).filter((f) => f.endsWith(".ts")).map((f) => f.slice(0, -3)) : [];
16
+ const listing = known.length ? `available: ${known.join(", ")}` : "none in tools/targets";
17
+ if (!existsSync(file))
18
+ throw new Error(`${modDir}: rooms need a mod.yaml naming the game (target: <name>; ${listing})`);
19
+ const name = /^target:\s*([\w-]+)\s*$/m.exec(readFileSync(file, "utf8"))?.[1];
20
+ if (!name)
21
+ throw new Error(`${file}: expected a line "target: <name>" (${listing})`);
22
+ if (!known.includes(name))
23
+ throw new Error(`${file}: unknown target "${name}" (${listing})`);
24
+ const mod = (await import(__rewriteRelativeImportExtension(join(TARGETS, `${name}.ts`))));
25
+ return mod.default;
26
+ }
@@ -0,0 +1,32 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { Target } from "../packages/content/src/index.ts";
4
+
5
+ /**
6
+ * A game that mods can add rooms to: how the content compiler should write for it, plus
7
+ * patches to the game's own scripts that new rooms may need.
8
+ */
9
+ export interface ModTarget {
10
+ content: Target;
11
+ /**
12
+ * Applied when a room brings its own characters: generated file (e.g. "0.sca") -> diff
13
+ * that teaches the game to ask the current room for talkers numbered from
14
+ * `content.firstRoomTalker`.
15
+ */
16
+ roomTalkerPatches?: Record<string, string>;
17
+ }
18
+
19
+ const TARGETS = join(import.meta.dirname, "targets");
20
+
21
+ /** The target named in a mod's mod.yaml (`target: <name>`): tools/targets/<name>.ts. */
22
+ export async function loadTarget(modDir: string): Promise<ModTarget> {
23
+ const file = join(modDir, "mod.yaml");
24
+ const known = existsSync(TARGETS) ? readdirSync(TARGETS).filter((f) => f.endsWith(".ts")).map((f) => f.slice(0, -3)) : [];
25
+ const listing = known.length ? `available: ${known.join(", ")}` : "none in tools/targets";
26
+ if (!existsSync(file)) throw new Error(`${modDir}: rooms need a mod.yaml naming the game (target: <name>; ${listing})`);
27
+ const name = /^target:\s*([\w-]+)\s*$/m.exec(readFileSync(file, "utf8"))?.[1];
28
+ if (!name) throw new Error(`${file}: expected a line "target: <name>" (${listing})`);
29
+ if (!known.includes(name)) throw new Error(`${file}: unknown target "${name}" (${listing})`);
30
+ const mod = (await import(join(TARGETS, `${name}.ts`))) as { default: ModTarget };
31
+ return mod.default;
32
+ }
package/tools/mod.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/tools/mod.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Mods: directories of new files and diffs against generated text.
3
+ * pnpm mod build <modDir> [outDir] build <n>.sca / <n>.msg (new files) and apply <n>.sca.diff /
4
+ * <n>.msg.diff (changes) to fresh dumps; patch files go to
5
+ * out/mods/<name> by default
6
+ * (<n>.room.yaml + <n>.yarn rooms are compiled first; the
7
+ * generated .sca/.msg land in <outDir>/generated to read)
8
+ * pnpm mod dump <n> <dir> write <n>.sca and <n>.msg to start editing
9
+ *
10
+ * The repo holds only our own files and diffs, never Sierra's scripts or text. Tools load
11
+ * built mods with SCI_MODS=out/mods/<name>[,...] and the browser pages with ?mods=<name>,
12
+ * the way the game loads Sierra's own patch files.
13
+ */
14
+ import { mkdirSync, writeFileSync } from "node:fs";
15
+ import { basename, join } from "node:path";
16
+ import { ResourceManager, ResourceType, ScriptWorld, toAssembly } from "../packages/sci/src/index.js";
17
+ import { buildMod, dumpMessages } from "./mod-build.js";
18
+ import { nodeFiles } from "./node-files.js";
19
+ import { writePatch } from "./patches.js";
20
+ const [command, ...args] = process.argv.slice(2);
21
+ const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
22
+ const world = await ScriptWorld.open(rm);
23
+ await world.loadAllClasses();
24
+ const ctx = world.asmContext();
25
+ if (command === "dump") {
26
+ const [n, dir = "."] = [Number(args[0]), args[1]];
27
+ mkdirSync(dir, { recursive: true });
28
+ writeFileSync(join(dir, `${n}.sca`), toAssembly(await world.script(n), ctx));
29
+ const hasMessages = rm.has({ type: ResourceType.Message, number: n });
30
+ if (hasMessages)
31
+ writeFileSync(join(dir, `${n}.msg`), await dumpMessages(rm, world, n));
32
+ console.log(`wrote ${n}.sca${hasMessages ? ` and ${n}.msg` : ""} to ${dir}`);
33
+ }
34
+ else if (command === "build") {
35
+ const modDir = args[0].replace(/\/$/, "");
36
+ const outDir = args[1] ?? join("out/mods", basename(modDir));
37
+ const generated = join(outDir, "generated");
38
+ const built = await buildMod(modDir, rm, world, ctx, (file, text) => {
39
+ mkdirSync(generated, { recursive: true });
40
+ writeFileSync(join(generated, file), text);
41
+ });
42
+ for (const r of built)
43
+ writePatch(outDir, r.type, r.number, r.data);
44
+ }
45
+ else {
46
+ console.log("usage: pnpm mod build <modDir> [outDir] | pnpm mod dump <n> <dir>");
47
+ }
package/tools/mod.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Mods: directories of new files and diffs against generated text.
3
+ * pnpm mod build <modDir> [outDir] build <n>.sca / <n>.msg (new files) and apply <n>.sca.diff /
4
+ * <n>.msg.diff (changes) to fresh dumps; patch files go to
5
+ * out/mods/<name> by default
6
+ * (<n>.room.yaml + <n>.yarn rooms are compiled first; the
7
+ * generated .sca/.msg land in <outDir>/generated to read)
8
+ * pnpm mod dump <n> <dir> write <n>.sca and <n>.msg to start editing
9
+ *
10
+ * The repo holds only our own files and diffs, never Sierra's scripts or text. Tools load
11
+ * built mods with SCI_MODS=out/mods/<name>[,...] and the browser pages with ?mods=<name>,
12
+ * the way the game loads Sierra's own patch files.
13
+ */
14
+ import { mkdirSync, writeFileSync } from "node:fs";
15
+ import { basename, join } from "node:path";
16
+ import { ResourceManager, ResourceType, ScriptWorld, toAssembly } from "../packages/sci/src/index.ts";
17
+ import { buildMod, dumpMessages } from "./mod-build.ts";
18
+ import { nodeFiles } from "./node-files.ts";
19
+ import { writePatch } from "./patches.ts";
20
+
21
+ const [command, ...args] = process.argv.slice(2);
22
+ const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
23
+ const world = await ScriptWorld.open(rm);
24
+ await world.loadAllClasses();
25
+ const ctx = world.asmContext();
26
+
27
+ if (command === "dump") {
28
+ const [n, dir = "."] = [Number(args[0]), args[1]];
29
+ mkdirSync(dir, { recursive: true });
30
+ writeFileSync(join(dir, `${n}.sca`), toAssembly(await world.script(n), ctx));
31
+ const hasMessages = rm.has({ type: ResourceType.Message, number: n });
32
+ if (hasMessages) writeFileSync(join(dir, `${n}.msg`), await dumpMessages(rm, world, n));
33
+ console.log(`wrote ${n}.sca${hasMessages ? ` and ${n}.msg` : ""} to ${dir}`);
34
+ } else if (command === "build") {
35
+ const modDir = args[0]!.replace(/\/$/, "");
36
+ const outDir = args[1] ?? join("out/mods", basename(modDir));
37
+ const generated = join(outDir, "generated");
38
+ const built = await buildMod(modDir, rm, world, ctx, (file, text) => {
39
+ mkdirSync(generated, { recursive: true });
40
+ writeFileSync(join(generated, file), text);
41
+ });
42
+ for (const r of built) writePatch(outDir, r.type, r.number, r.data);
43
+ } else {
44
+ console.log("usage: pnpm mod build <modDir> [outDir] | pnpm mod dump <n> <dir>");
45
+ }
package/tools/msg.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/tools/msg.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Message resources as text.
3
+ * pnpm msg dump <n> [file.msg] write a message file as text (verbs and nouns annotated)
4
+ * pnpm msg build <file.msg> [dir] build a <n>.MSG patch file (default out/mods)
5
+ * pnpm msg roundtrip parse + write every message file, compare bytes
6
+ */
7
+ import { readFileSync, writeFileSync } from "node:fs";
8
+ import { ResourceManager, ResourceType, ScriptWorld, messagesFromText, messagesToText, parseMessages, writeMessages } from "../packages/sci/src/index.js";
9
+ import { dumpMessages } from "./mod-build.js";
10
+ import { nodeFiles } from "./node-files.js";
11
+ import { writePatch } from "./patches.js";
12
+ const [command = "roundtrip", ...args] = process.argv.slice(2);
13
+ const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
14
+ if (command === "dump") {
15
+ const text = await dumpMessages(rm, await ScriptWorld.open(rm), Number(args[0]));
16
+ if (args[1])
17
+ writeFileSync(args[1], text);
18
+ else
19
+ process.stdout.write(text);
20
+ }
21
+ else if (command === "build") {
22
+ const { number, file } = messagesFromText(readFileSync(args[0], "utf8"));
23
+ writePatch(args[1] ?? "out/mods", ResourceType.Message, number, writeMessages(file));
24
+ }
25
+ else {
26
+ let ok = 0;
27
+ const bad = [];
28
+ const list = rm.list(ResourceType.Message);
29
+ for (const info of list) {
30
+ const data = (await rm.load(info)).data;
31
+ const direct = writeMessages(parseMessages(data));
32
+ const viaText = writeMessages(messagesFromText(messagesToText(info.number, parseMessages(data))).file);
33
+ if (Buffer.compare(Buffer.from(data), Buffer.from(direct)) === 0 && Buffer.compare(Buffer.from(data), Buffer.from(viaText)) === 0)
34
+ ok++;
35
+ else
36
+ bad.push(info.number);
37
+ }
38
+ console.log(`${ok}/${list.length} message files round-trip byte-exact (binary and via text)${bad.length ? `; differ: ${bad.join(" ")}` : ""}`);
39
+ process.exitCode = bad.length ? 1 : 0;
40
+ }
package/tools/msg.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Message resources as text.
3
+ * pnpm msg dump <n> [file.msg] write a message file as text (verbs and nouns annotated)
4
+ * pnpm msg build <file.msg> [dir] build a <n>.MSG patch file (default out/mods)
5
+ * pnpm msg roundtrip parse + write every message file, compare bytes
6
+ */
7
+ import { readFileSync, writeFileSync } from "node:fs";
8
+ import { ResourceManager, ResourceType, ScriptWorld, messagesFromText, messagesToText, parseMessages, writeMessages } from "../packages/sci/src/index.ts";
9
+ import { dumpMessages } from "./mod-build.ts";
10
+ import { nodeFiles } from "./node-files.ts";
11
+ import { writePatch } from "./patches.ts";
12
+
13
+ const [command = "roundtrip", ...args] = process.argv.slice(2);
14
+ const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
15
+
16
+ if (command === "dump") {
17
+ const text = await dumpMessages(rm, await ScriptWorld.open(rm), Number(args[0]));
18
+ if (args[1]) writeFileSync(args[1], text);
19
+ else process.stdout.write(text);
20
+ } else if (command === "build") {
21
+ const { number, file } = messagesFromText(readFileSync(args[0]!, "utf8"));
22
+ writePatch(args[1] ?? "out/mods", ResourceType.Message, number, writeMessages(file));
23
+ } else {
24
+ let ok = 0;
25
+ const bad: number[] = [];
26
+ const list = rm.list(ResourceType.Message);
27
+ for (const info of list) {
28
+ const data = (await rm.load(info)).data;
29
+ const direct = writeMessages(parseMessages(data));
30
+ const viaText = writeMessages(messagesFromText(messagesToText(info.number, parseMessages(data))).file);
31
+ if (Buffer.compare(Buffer.from(data), Buffer.from(direct)) === 0 && Buffer.compare(Buffer.from(data), Buffer.from(viaText)) === 0) ok++;
32
+ else bad.push(info.number);
33
+ }
34
+ console.log(`${ok}/${list.length} message files round-trip byte-exact (binary and via text)${bad.length ? `; differ: ${bad.join(" ")}` : ""}`);
35
+ process.exitCode = bad.length ? 1 : 0;
36
+ }
@@ -0,0 +1,3 @@
1
+ import type { FileSource } from "../packages/sci/src/index.ts";
2
+ /** FileSource over a local game directory. */
3
+ export declare function nodeFiles(root: string): FileSource;
@@ -0,0 +1,34 @@
1
+ import { open, readFile, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ /** FileSource over a local game directory. */
4
+ export function nodeFiles(root) {
5
+ return {
6
+ async read(path) {
7
+ try {
8
+ return new Uint8Array(await readFile(join(root, path)));
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ },
14
+ async readRange(path, offset, length) {
15
+ const fh = await open(join(root, path));
16
+ try {
17
+ const buf = new Uint8Array(length);
18
+ await fh.read(buf, 0, length, offset);
19
+ return buf;
20
+ }
21
+ finally {
22
+ await fh.close();
23
+ }
24
+ },
25
+ async list(dir) {
26
+ try {
27
+ return await readdir(join(root, dir));
28
+ }
29
+ catch {
30
+ return [];
31
+ }
32
+ },
33
+ };
34
+ }