sci2-ts 0.0.0-stage → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -2
  4. package/apps/viewer/editor.html +43 -0
  5. package/apps/viewer/explore.html +27 -0
  6. package/apps/viewer/index.html +26 -0
  7. package/apps/viewer/package.json +27 -0
  8. package/apps/viewer/play.html +26 -0
  9. package/apps/viewer/plugin.d.ts +18 -0
  10. package/apps/viewer/plugin.js +75 -0
  11. package/apps/viewer/plugin.ts +84 -0
  12. package/apps/viewer/src/adlib.d.ts +20 -0
  13. package/apps/viewer/src/adlib.js +55 -0
  14. package/apps/viewer/src/adlib.ts +62 -0
  15. package/apps/viewer/src/app.d.ts +5 -0
  16. package/apps/viewer/src/app.js +5 -0
  17. package/apps/viewer/src/app.ts +5 -0
  18. package/apps/viewer/src/asset-index.d.ts +22 -0
  19. package/apps/viewer/src/asset-index.js +74 -0
  20. package/apps/viewer/src/asset-index.ts +90 -0
  21. package/apps/viewer/src/audio.d.ts +28 -0
  22. package/apps/viewer/src/audio.js +102 -0
  23. package/apps/viewer/src/audio.ts +110 -0
  24. package/apps/viewer/src/editor-handles.d.ts +63 -0
  25. package/apps/viewer/src/editor-handles.js +332 -0
  26. package/apps/viewer/src/editor-handles.ts +368 -0
  27. package/apps/viewer/src/editor-languages.d.ts +23 -0
  28. package/apps/viewer/src/editor-languages.js +157 -0
  29. package/apps/viewer/src/editor-languages.ts +135 -0
  30. package/apps/viewer/src/editor.css +213 -0
  31. package/apps/viewer/src/editor.d.ts +1 -0
  32. package/apps/viewer/src/editor.js +393 -0
  33. package/apps/viewer/src/editor.ts +412 -0
  34. package/apps/viewer/src/explore/classes.d.ts +7 -0
  35. package/apps/viewer/src/explore/classes.js +63 -0
  36. package/apps/viewer/src/explore/classes.ts +61 -0
  37. package/apps/viewer/src/explore/context.d.ts +33 -0
  38. package/apps/viewer/src/explore/context.js +96 -0
  39. package/apps/viewer/src/explore/context.ts +124 -0
  40. package/apps/viewer/src/explore/explore.css +93 -0
  41. package/apps/viewer/src/explore/explore.d.ts +1 -0
  42. package/apps/viewer/src/explore/explore.js +102 -0
  43. package/apps/viewer/src/explore/explore.ts +104 -0
  44. package/apps/viewer/src/explore/inspect.d.ts +5 -0
  45. package/apps/viewer/src/explore/inspect.js +208 -0
  46. package/apps/viewer/src/explore/inspect.ts +279 -0
  47. package/apps/viewer/src/explore/live.d.ts +22 -0
  48. package/apps/viewer/src/explore/live.js +318 -0
  49. package/apps/viewer/src/explore/live.ts +365 -0
  50. package/apps/viewer/src/explore/map.d.ts +11 -0
  51. package/apps/viewer/src/explore/map.js +182 -0
  52. package/apps/viewer/src/explore/map.ts +193 -0
  53. package/apps/viewer/src/files.d.ts +21 -0
  54. package/apps/viewer/src/files.js +69 -0
  55. package/apps/viewer/src/files.ts +72 -0
  56. package/apps/viewer/src/game-profile.d.ts +19 -0
  57. package/apps/viewer/src/game-profile.js +4 -0
  58. package/apps/viewer/src/game-profile.ts +26 -0
  59. package/apps/viewer/src/main.d.ts +1 -0
  60. package/apps/viewer/src/main.js +473 -0
  61. package/apps/viewer/src/main.ts +527 -0
  62. package/apps/viewer/src/music.d.ts +14 -0
  63. package/apps/viewer/src/music.js +32 -0
  64. package/apps/viewer/src/music.ts +34 -0
  65. package/apps/viewer/src/opl-worklet.d.ts +1 -0
  66. package/apps/viewer/src/opl-worklet.js +47 -0
  67. package/apps/viewer/src/opl-worklet.ts +55 -0
  68. package/apps/viewer/src/overlay.d.ts +31 -0
  69. package/apps/viewer/src/overlay.js +103 -0
  70. package/apps/viewer/src/overlay.ts +123 -0
  71. package/apps/viewer/src/play.css +14 -0
  72. package/apps/viewer/src/play.d.ts +1 -0
  73. package/apps/viewer/src/play.js +179 -0
  74. package/apps/viewer/src/play.ts +177 -0
  75. package/apps/viewer/src/saves.d.ts +10 -0
  76. package/apps/viewer/src/saves.js +50 -0
  77. package/apps/viewer/src/saves.ts +51 -0
  78. package/apps/viewer/src/session.d.ts +91 -0
  79. package/apps/viewer/src/session.js +301 -0
  80. package/apps/viewer/src/session.ts +321 -0
  81. package/apps/viewer/src/sound-preview.d.ts +7 -0
  82. package/apps/viewer/src/sound-preview.js +214 -0
  83. package/apps/viewer/src/sound-preview.ts +228 -0
  84. package/apps/viewer/src/style.css +64 -0
  85. package/apps/viewer/vite.config.ts +25 -0
  86. package/bin/sci-ts.mjs +9 -0
  87. package/lib/994.sc +49 -0
  88. package/lib/995.sc +53 -0
  89. package/lib/996.sc +299 -0
  90. package/lib/997.sc +198 -0
  91. package/lib/998.sc +338 -0
  92. package/lib/999.sc +189 -0
  93. package/lib/resources.d.ts +2 -0
  94. package/lib/resources.js +84 -0
  95. package/lib/resources.ts +88 -0
  96. package/lib/system.sh +62 -0
  97. package/package.json +75 -4
  98. package/packages/content/room.schema.json +267 -0
  99. package/packages/content/src/index.d.ts +3 -0
  100. package/packages/content/src/index.js +3 -0
  101. package/packages/content/src/index.ts +3 -0
  102. package/packages/content/src/room.d.ts +111 -0
  103. package/packages/content/src/room.js +886 -0
  104. package/packages/content/src/room.ts +1011 -0
  105. package/packages/content/src/target.d.ts +80 -0
  106. package/packages/content/src/target.js +1 -0
  107. package/packages/content/src/target.ts +60 -0
  108. package/packages/content/src/yarn.d.ts +90 -0
  109. package/packages/content/src/yarn.js +214 -0
  110. package/packages/content/src/yarn.ts +261 -0
  111. package/packages/sci/src/analysis/game.d.ts +90 -0
  112. package/packages/sci/src/analysis/game.js +221 -0
  113. package/packages/sci/src/analysis/game.ts +305 -0
  114. package/packages/sci/src/analysis/runtime.d.ts +58 -0
  115. package/packages/sci/src/analysis/runtime.js +133 -0
  116. package/packages/sci/src/analysis/runtime.ts +163 -0
  117. package/packages/sci/src/audio/adlib.d.ts +73 -0
  118. package/packages/sci/src/audio/adlib.js +280 -0
  119. package/packages/sci/src/audio/adlib.ts +329 -0
  120. package/packages/sci/src/audio/index.d.ts +43 -0
  121. package/packages/sci/src/audio/index.js +99 -0
  122. package/packages/sci/src/audio/index.ts +122 -0
  123. package/packages/sci/src/audio/midi.d.ts +92 -0
  124. package/packages/sci/src/audio/midi.js +242 -0
  125. package/packages/sci/src/audio/midi.ts +262 -0
  126. package/packages/sci/src/audio/opl2.d.ts +74 -0
  127. package/packages/sci/src/audio/opl2.js +283 -0
  128. package/packages/sci/src/audio/opl2.ts +283 -0
  129. package/packages/sci/src/audio/sol.d.ts +32 -0
  130. package/packages/sci/src/audio/sol.js +81 -0
  131. package/packages/sci/src/audio/sol.ts +100 -0
  132. package/packages/sci/src/audio/write.d.ts +35 -0
  133. package/packages/sci/src/audio/write.js +91 -0
  134. package/packages/sci/src/audio/write.ts +108 -0
  135. package/packages/sci/src/gfx/cel.d.ts +36 -0
  136. package/packages/sci/src/gfx/cel.js +82 -0
  137. package/packages/sci/src/gfx/cel.ts +110 -0
  138. package/packages/sci/src/gfx/palette.d.ts +24 -0
  139. package/packages/sci/src/gfx/palette.js +49 -0
  140. package/packages/sci/src/gfx/palette.ts +56 -0
  141. package/packages/sci/src/gfx/pic-file.d.ts +46 -0
  142. package/packages/sci/src/gfx/pic-file.js +114 -0
  143. package/packages/sci/src/gfx/pic-file.ts +155 -0
  144. package/packages/sci/src/gfx/pic.d.ts +27 -0
  145. package/packages/sci/src/gfx/pic.js +34 -0
  146. package/packages/sci/src/gfx/pic.ts +53 -0
  147. package/packages/sci/src/gfx/quantize.d.ts +42 -0
  148. package/packages/sci/src/gfx/quantize.js +199 -0
  149. package/packages/sci/src/gfx/quantize.ts +218 -0
  150. package/packages/sci/src/gfx/view-file.d.ts +62 -0
  151. package/packages/sci/src/gfx/view-file.js +182 -0
  152. package/packages/sci/src/gfx/view-file.ts +233 -0
  153. package/packages/sci/src/gfx/view.d.ts +28 -0
  154. package/packages/sci/src/gfx/view.js +45 -0
  155. package/packages/sci/src/gfx/view.ts +62 -0
  156. package/packages/sci/src/index.d.ts +38 -0
  157. package/packages/sci/src/index.js +38 -0
  158. package/packages/sci/src/index.ts +38 -0
  159. package/packages/sci/src/motion/pathfinding.d.ts +40 -0
  160. package/packages/sci/src/motion/pathfinding.js +140 -0
  161. package/packages/sci/src/motion/pathfinding.ts +147 -0
  162. package/packages/sci/src/resource/archive.d.ts +16 -0
  163. package/packages/sci/src/resource/archive.js +51 -0
  164. package/packages/sci/src/resource/archive.ts +56 -0
  165. package/packages/sci/src/resource/lzs.d.ts +12 -0
  166. package/packages/sci/src/resource/lzs.js +64 -0
  167. package/packages/sci/src/resource/lzs.ts +66 -0
  168. package/packages/sci/src/resource/manager.d.ts +45 -0
  169. package/packages/sci/src/resource/manager.js +158 -0
  170. package/packages/sci/src/resource/manager.ts +184 -0
  171. package/packages/sci/src/resource/map.d.ts +18 -0
  172. package/packages/sci/src/resource/map.js +34 -0
  173. package/packages/sci/src/resource/map.ts +44 -0
  174. package/packages/sci/src/resource/types.d.ts +37 -0
  175. package/packages/sci/src/resource/types.js +45 -0
  176. package/packages/sci/src/resource/types.ts +57 -0
  177. package/packages/sci/src/script/assembly.d.ts +67 -0
  178. package/packages/sci/src/script/assembly.js +748 -0
  179. package/packages/sci/src/script/assembly.ts +714 -0
  180. package/packages/sci/src/script/compiler.d.ts +75 -0
  181. package/packages/sci/src/script/compiler.js +993 -0
  182. package/packages/sci/src/script/compiler.ts +1040 -0
  183. package/packages/sci/src/script/disasm.d.ts +81 -0
  184. package/packages/sci/src/script/disasm.js +318 -0
  185. package/packages/sci/src/script/disasm.ts +323 -0
  186. package/packages/sci/src/script/kernel-names.d.ts +6 -0
  187. package/packages/sci/src/script/kernel-names.js +165 -0
  188. package/packages/sci/src/script/kernel-names.ts +165 -0
  189. package/packages/sci/src/script/opcodes.d.ts +22 -0
  190. package/packages/sci/src/script/opcodes.js +45 -0
  191. package/packages/sci/src/script/opcodes.ts +72 -0
  192. package/packages/sci/src/script/script.d.ts +56 -0
  193. package/packages/sci/src/script/script.js +63 -0
  194. package/packages/sci/src/script/script.ts +111 -0
  195. package/packages/sci/src/script/sexpr.d.ts +45 -0
  196. package/packages/sci/src/script/sexpr.js +151 -0
  197. package/packages/sci/src/script/sexpr.ts +139 -0
  198. package/packages/sci/src/script/vocab.d.ts +15 -0
  199. package/packages/sci/src/script/vocab.js +44 -0
  200. package/packages/sci/src/script/vocab.ts +48 -0
  201. package/packages/sci/src/script/world.d.ts +25 -0
  202. package/packages/sci/src/script/world.js +65 -0
  203. package/packages/sci/src/script/world.ts +70 -0
  204. package/packages/sci/src/text/font.d.ts +37 -0
  205. package/packages/sci/src/text/font.js +103 -0
  206. package/packages/sci/src/text/font.ts +125 -0
  207. package/packages/sci/src/text/message.d.ts +69 -0
  208. package/packages/sci/src/text/message.js +171 -0
  209. package/packages/sci/src/text/message.ts +205 -0
  210. package/packages/sci/src/vm/kernels/arrays.d.ts +19 -0
  211. package/packages/sci/src/vm/kernels/arrays.js +182 -0
  212. package/packages/sci/src/vm/kernels/arrays.ts +164 -0
  213. package/packages/sci/src/vm/kernels/audio.d.ts +70 -0
  214. package/packages/sci/src/vm/kernels/audio.js +347 -0
  215. package/packages/sci/src/vm/kernels/audio.ts +349 -0
  216. package/packages/sci/src/vm/kernels/core.d.ts +5 -0
  217. package/packages/sci/src/vm/kernels/core.js +87 -0
  218. package/packages/sci/src/vm/kernels/core.ts +91 -0
  219. package/packages/sci/src/vm/kernels/graphics.d.ts +141 -0
  220. package/packages/sci/src/vm/kernels/graphics.js +467 -0
  221. package/packages/sci/src/vm/kernels/graphics.ts +502 -0
  222. package/packages/sci/src/vm/kernels/index.d.ts +8 -0
  223. package/packages/sci/src/vm/kernels/index.js +29 -0
  224. package/packages/sci/src/vm/kernels/index.ts +32 -0
  225. package/packages/sci/src/vm/kernels/lists.d.ts +6 -0
  226. package/packages/sci/src/vm/kernels/lists.js +127 -0
  227. package/packages/sci/src/vm/kernels/lists.ts +111 -0
  228. package/packages/sci/src/vm/kernels/motion.d.ts +7 -0
  229. package/packages/sci/src/vm/kernels/motion.js +204 -0
  230. package/packages/sci/src/vm/kernels/motion.ts +194 -0
  231. package/packages/sci/src/vm/kernels/palette.d.ts +82 -0
  232. package/packages/sci/src/vm/kernels/palette.js +292 -0
  233. package/packages/sci/src/vm/kernels/palette.ts +303 -0
  234. package/packages/sci/src/vm/kernels/saves.d.ts +45 -0
  235. package/packages/sci/src/vm/kernels/saves.js +123 -0
  236. package/packages/sci/src/vm/kernels/saves.ts +149 -0
  237. package/packages/sci/src/vm/kernels/system.d.ts +62 -0
  238. package/packages/sci/src/vm/kernels/system.js +110 -0
  239. package/packages/sci/src/vm/kernels/system.ts +128 -0
  240. package/packages/sci/src/vm/kernels/text.d.ts +13 -0
  241. package/packages/sci/src/vm/kernels/text.js +338 -0
  242. package/packages/sci/src/vm/kernels/text.ts +328 -0
  243. package/packages/sci/src/vm/memory.d.ts +138 -0
  244. package/packages/sci/src/vm/memory.js +168 -0
  245. package/packages/sci/src/vm/memory.ts +266 -0
  246. package/packages/sci/src/vm/replace.d.ts +11 -0
  247. package/packages/sci/src/vm/replace.js +14 -0
  248. package/packages/sci/src/vm/replace.ts +16 -0
  249. package/packages/sci/src/vm/savegame.d.ts +89 -0
  250. package/packages/sci/src/vm/savegame.js +161 -0
  251. package/packages/sci/src/vm/savegame.ts +188 -0
  252. package/packages/sci/src/vm/value.d.ts +17 -0
  253. package/packages/sci/src/vm/value.js +12 -0
  254. package/packages/sci/src/vm/value.ts +25 -0
  255. package/packages/sci/src/vm/vm.d.ts +155 -0
  256. package/packages/sci/src/vm/vm.js +1053 -0
  257. package/packages/sci/src/vm/vm.ts +914 -0
  258. package/tools/adlib-render.d.ts +1 -0
  259. package/tools/adlib-render.js +49 -0
  260. package/tools/adlib-render.ts +42 -0
  261. package/tools/analyze.d.ts +1 -0
  262. package/tools/analyze.js +25 -0
  263. package/tools/analyze.ts +25 -0
  264. package/tools/art/build.d.ts +34 -0
  265. package/tools/art/build.js +175 -0
  266. package/tools/art/build.ts +153 -0
  267. package/tools/art.d.ts +1 -0
  268. package/tools/art.js +31 -0
  269. package/tools/art.ts +29 -0
  270. package/tools/asm.d.ts +1 -0
  271. package/tools/asm.js +61 -0
  272. package/tools/asm.ts +55 -0
  273. package/tools/cli.d.ts +1 -0
  274. package/tools/cli.js +99 -0
  275. package/tools/cli.ts +100 -0
  276. package/tools/editor-server.d.ts +29 -0
  277. package/tools/editor-server.js +162 -0
  278. package/tools/editor-server.ts +176 -0
  279. package/tools/explore.d.ts +1 -0
  280. package/tools/explore.js +305 -0
  281. package/tools/explore.ts +250 -0
  282. package/tools/game/audio.d.ts +19 -0
  283. package/tools/game/audio.js +143 -0
  284. package/tools/game/audio.ts +130 -0
  285. package/tools/game/build.d.ts +46 -0
  286. package/tools/game/build.js +259 -0
  287. package/tools/game/build.ts +283 -0
  288. package/tools/game/defaults.d.ts +26 -0
  289. package/tools/game/defaults.js +82 -0
  290. package/tools/game/defaults.ts +83 -0
  291. package/tools/game/font.d.ts +3 -0
  292. package/tools/game/font.js +124 -0
  293. package/tools/game/font.ts +126 -0
  294. package/tools/game/kit.d.ts +7 -0
  295. package/tools/game/kit.js +7 -0
  296. package/tools/game/kit.ts +10 -0
  297. package/tools/game/new.d.ts +8 -0
  298. package/tools/game/new.js +39 -0
  299. package/tools/game/new.ts +38 -0
  300. package/tools/game/target.d.ts +7 -0
  301. package/tools/game/target.js +41 -0
  302. package/tools/game/target.ts +42 -0
  303. package/tools/game/template/resources.ts.tmpl +43 -0
  304. package/tools/game/template/rooms/1.room.yaml +9 -0
  305. package/tools/game/template/rooms/1.yarn +17 -0
  306. package/tools/game/template/scripts/0.sc +24 -0
  307. package/tools/game.d.ts +1 -0
  308. package/tools/game.js +77 -0
  309. package/tools/game.ts +70 -0
  310. package/tools/mod-build.d.ts +28 -0
  311. package/tools/mod-build.js +173 -0
  312. package/tools/mod-build.ts +179 -0
  313. package/tools/mod-target.d.ts +16 -0
  314. package/tools/mod-target.js +26 -0
  315. package/tools/mod-target.ts +32 -0
  316. package/tools/mod.d.ts +1 -0
  317. package/tools/mod.js +47 -0
  318. package/tools/mod.ts +45 -0
  319. package/tools/msg.d.ts +1 -0
  320. package/tools/msg.js +40 -0
  321. package/tools/msg.ts +36 -0
  322. package/tools/node-files.d.ts +3 -0
  323. package/tools/node-files.js +34 -0
  324. package/tools/node-files.ts +33 -0
  325. package/tools/patches.d.ts +3 -0
  326. package/tools/patches.js +13 -0
  327. package/tools/patches.ts +13 -0
  328. package/tools/pic-build.d.ts +9 -0
  329. package/tools/pic-build.js +26 -0
  330. package/tools/pic-build.ts +27 -0
  331. package/tools/pic.d.ts +1 -0
  332. package/tools/pic.js +64 -0
  333. package/tools/pic.ts +61 -0
  334. package/tools/play.d.ts +1 -0
  335. package/tools/play.js +54 -0
  336. package/tools/play.ts +54 -0
  337. package/tools/png.d.ts +16 -0
  338. package/tools/png.js +124 -0
  339. package/tools/png.ts +123 -0
  340. package/tools/res.d.ts +1 -0
  341. package/tools/res.js +48 -0
  342. package/tools/res.ts +50 -0
  343. package/tools/scenario.d.ts +1 -0
  344. package/tools/scenario.js +74 -0
  345. package/tools/scenario.ts +80 -0
  346. package/tools/site/build.d.ts +8 -0
  347. package/tools/site/build.js +110 -0
  348. package/tools/site/build.ts +116 -0
  349. package/tools/site/markdown.d.ts +11 -0
  350. package/tools/site/markdown.js +88 -0
  351. package/tools/site/markdown.ts +84 -0
  352. package/tools/view-build.d.ts +34 -0
  353. package/tools/view-build.js +47 -0
  354. package/tools/view-build.ts +71 -0
  355. package/tools/view.d.ts +1 -0
  356. package/tools/view.js +54 -0
  357. package/tools/view.ts +52 -0
@@ -0,0 +1,33 @@
1
+ import { open, readFile, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { FileSource } from "../packages/sci/src/index.ts";
4
+
5
+ /** FileSource over a local game directory. */
6
+ export function nodeFiles(root: string): FileSource {
7
+ return {
8
+ async read(path) {
9
+ try {
10
+ return new Uint8Array(await readFile(join(root, path)));
11
+ } catch {
12
+ return undefined;
13
+ }
14
+ },
15
+ async readRange(path, offset, length) {
16
+ const fh = await open(join(root, path));
17
+ try {
18
+ const buf = new Uint8Array(length);
19
+ await fh.read(buf, 0, length, offset);
20
+ return buf;
21
+ } finally {
22
+ await fh.close();
23
+ }
24
+ },
25
+ async list(dir) {
26
+ try {
27
+ return await readdir(join(root, dir));
28
+ } catch {
29
+ return [];
30
+ }
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,3 @@
1
+ import { ResourceType } from "../packages/sci/src/index.ts";
2
+ /** Writes an SCI patch file: [type | 0x80, 0 extra header bytes] + data, e.g. 770.MSG. */
3
+ export declare function writePatch(dir: string, type: ResourceType, number: number, data: Uint8Array): string;
@@ -0,0 +1,13 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { ResourceType, patchExtensions } from "../packages/sci/src/index.js";
3
+ /** Writes an SCI patch file: [type | 0x80, 0 extra header bytes] + data, e.g. 770.MSG. */
4
+ export function writePatch(dir, type, number, data) {
5
+ const ext = Object.entries(patchExtensions).find(([, t]) => t === type)?.[0];
6
+ if (!ext)
7
+ throw new Error(`No patch extension for resource type ${type}`);
8
+ mkdirSync(dir, { recursive: true });
9
+ const path = `${dir}/${number}.${ext.toUpperCase()}`;
10
+ writeFileSync(path, Buffer.concat([Buffer.from([type | 0x80, 0]), data]));
11
+ console.log(`${path}: ${data.length} bytes`);
12
+ return path;
13
+ }
@@ -0,0 +1,13 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { ResourceType, patchExtensions } from "../packages/sci/src/index.ts";
3
+
4
+ /** Writes an SCI patch file: [type | 0x80, 0 extra header bytes] + data, e.g. 770.MSG. */
5
+ export function writePatch(dir: string, type: ResourceType, number: number, data: Uint8Array): string {
6
+ const ext = Object.entries(patchExtensions).find(([, t]) => t === type)?.[0];
7
+ if (!ext) throw new Error(`No patch extension for resource type ${type}`);
8
+ mkdirSync(dir, { recursive: true });
9
+ const path = `${dir}/${number}.${ext.toUpperCase()}`;
10
+ writeFileSync(path, Buffer.concat([Buffer.from([type | 0x80, 0]), data]));
11
+ console.log(`${path}: ${data.length} bytes`);
12
+ return path;
13
+ }
@@ -0,0 +1,9 @@
1
+ import { type PicFile, type ResourceManager } from "../packages/sci/src/index.ts";
2
+ /** The base palette (999): the colours every room keeps. */
3
+ export declare function basePalette(rm: ResourceManager): Promise<import("../packages/sci/src/index.ts").Palette>;
4
+ /**
5
+ * A room picture from PNGs in a directory: `<n>.png` is the background (320x190), and
6
+ * `<n>.layer-<priority>.png` files (same size, transparent where empty) are foreground
7
+ * pieces that actors with a lower y walk behind.
8
+ */
9
+ export declare function picFromPngs(dir: string, n: number, rm: ResourceManager, dither?: boolean): Promise<PicFile>;
@@ -0,0 +1,26 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { buildRoomPic, emptyPalette, parseHunkPalette, ResourceType } from "../packages/sci/src/index.js";
4
+ import { decodePng } from "./png.js";
5
+ /** The base palette (999): the colours every room keeps. */
6
+ export async function basePalette(rm) {
7
+ return parseHunkPalette((await rm.load({ type: ResourceType.Palette, number: 999 })).data) ?? emptyPalette();
8
+ }
9
+ /**
10
+ * A room picture from PNGs in a directory: `<n>.png` is the background (320x190), and
11
+ * `<n>.layer-<priority>.png` files (same size, transparent where empty) are foreground
12
+ * pieces that actors with a lower y walk behind.
13
+ */
14
+ export async function picFromPngs(dir, n, rm, dither = true) {
15
+ const layers = [];
16
+ const background = decodePng(readFileSync(join(dir, `${n}.png`)));
17
+ layers.push({ width: background.width, height: background.height, rgba: background.data, priority: -1000 });
18
+ for (const f of readdirSync(dir)) {
19
+ const m = new RegExp(`^${n}\\.layer-(-?\\d+)\\.png$`).exec(f);
20
+ if (!m)
21
+ continue;
22
+ const img = decodePng(readFileSync(join(dir, f)));
23
+ layers.push({ width: img.width, height: img.height, rgba: img.data, priority: Number(m[1]) });
24
+ }
25
+ return buildRoomPic(layers, await basePalette(rm), { dither });
26
+ }
@@ -0,0 +1,27 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { buildRoomPic, emptyPalette, parseHunkPalette, ResourceType, type PicFile, type PicLayer, type ResourceManager } from "../packages/sci/src/index.ts";
4
+ import { decodePng } from "./png.ts";
5
+
6
+ /** The base palette (999): the colours every room keeps. */
7
+ export async function basePalette(rm: ResourceManager) {
8
+ return parseHunkPalette((await rm.load({ type: ResourceType.Palette, number: 999 })).data) ?? emptyPalette();
9
+ }
10
+
11
+ /**
12
+ * A room picture from PNGs in a directory: `<n>.png` is the background (320x190), and
13
+ * `<n>.layer-<priority>.png` files (same size, transparent where empty) are foreground
14
+ * pieces that actors with a lower y walk behind.
15
+ */
16
+ export async function picFromPngs(dir: string, n: number, rm: ResourceManager, dither = true): Promise<PicFile> {
17
+ const layers: PicLayer[] = [];
18
+ const background = decodePng(readFileSync(join(dir, `${n}.png`)));
19
+ layers.push({ width: background.width, height: background.height, rgba: background.data, priority: -1000 });
20
+ for (const f of readdirSync(dir)) {
21
+ const m = new RegExp(`^${n}\\.layer-(-?\\d+)\\.png$`).exec(f);
22
+ if (!m) continue;
23
+ const img = decodePng(readFileSync(join(dir, f)));
24
+ layers.push({ width: img.width, height: img.height, rgba: img.data, priority: Number(m[1]) });
25
+ }
26
+ return buildRoomPic(layers, await basePalette(rm), { dither });
27
+ }
package/tools/pic.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/tools/pic.js ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Room pictures.
3
+ * pnpm pic export <n> [dir] <n>.png (background) and <n>.layer-<priority>.png pieces
4
+ * pnpm pic import <dir> <n> [outDir] build pic <n> from <dir>/<n>.png (+ layers) → <n>.P56
5
+ * pnpm pic roundtrip parse + write every pic, compare bytes
6
+ * Flags: --no-dither (import).
7
+ */
8
+ import { mkdirSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { ResourceManager, ResourceType, hunkToPalette, mergePalette, parsePicFile, writePic } from "../packages/sci/src/index.js";
11
+ import { nodeFiles } from "./node-files.js";
12
+ import { writePatch } from "./patches.js";
13
+ import { basePalette, picFromPngs } from "./pic-build.js";
14
+ import { rgbaPng } from "./png.js";
15
+ const argv = process.argv.slice(2);
16
+ const flags = new Set(argv.filter((a) => a.startsWith("--")));
17
+ const [command = "roundtrip", ...args] = argv.filter((a) => !a.startsWith("--"));
18
+ const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
19
+ /** Composites a pic's layers (or just some) to RGBA, 320x190. */
20
+ function render(pic, palette, only) {
21
+ const w = 320, h = 190;
22
+ const data = new Uint8Array(w * h * 4);
23
+ for (const cel of [...pic.cels].sort((a, b) => a.priority - b.priority)) {
24
+ if (only && !only(cel.priority))
25
+ continue;
26
+ for (let y = 0; y < cel.height; y++)
27
+ for (let x = 0; x < cel.width; x++) {
28
+ const c = cel.pixels[y * cel.width + x];
29
+ const X = cel.x + x, Y = cel.y + y;
30
+ if (c === cel.skipColor || X < 0 || Y < 0 || X >= w || Y >= h)
31
+ continue;
32
+ data.set([palette.rgb[c * 3], palette.rgb[c * 3 + 1], palette.rgb[c * 3 + 2], 255], (Y * w + X) * 4);
33
+ }
34
+ }
35
+ return { width: w, height: h, data };
36
+ }
37
+ if (command === "export") {
38
+ const n = Number(args[0]);
39
+ const dir = args[1] ?? "out/pics";
40
+ mkdirSync(dir, { recursive: true });
41
+ const pic = parsePicFile((await rm.load({ type: ResourceType.Pic, number: n })).data);
42
+ const palette = mergePalette(await basePalette(rm), hunkToPalette(pic.palette));
43
+ writeFileSync(join(dir, `${n}.png`), rgbaPng(render(pic, palette, (p) => p === -1000)));
44
+ for (const p of new Set(pic.cels.map((c) => c.priority).filter((p) => p > -1000))) {
45
+ writeFileSync(join(dir, `${n}.layer-${p}.png`), rgbaPng(render(pic, palette, (q) => q === p)));
46
+ }
47
+ console.log(`pic ${n}: ${pic.cels.length} cels → ${dir}/${n}.png${pic.cels.length > 1 ? ` + ${pic.cels.length - 1} layers` : ""}`);
48
+ }
49
+ else if (command === "import") {
50
+ const [dir, n, outDir = "out/pics"] = [args[0], Number(args[1]), args[2]];
51
+ const pic = await picFromPngs(dir, n, rm, !flags.has("--no-dither"));
52
+ writePatch(outDir, ResourceType.Pic, n, writePic(pic));
53
+ }
54
+ else {
55
+ const list = rm.list(ResourceType.Pic);
56
+ const bad = [];
57
+ for (const info of list) {
58
+ const d = Buffer.from((await rm.load(info)).data);
59
+ if (d.compare(Buffer.from(writePic(parsePicFile(d)))))
60
+ bad.push(info.number);
61
+ }
62
+ console.log(`${list.length - bad.length}/${list.length} pics round-trip byte-exact${bad.length ? `; differ: ${bad.join(" ")}` : ""}`);
63
+ process.exitCode = bad.length ? 1 : 0;
64
+ }
package/tools/pic.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Room pictures.
3
+ * pnpm pic export <n> [dir] <n>.png (background) and <n>.layer-<priority>.png pieces
4
+ * pnpm pic import <dir> <n> [outDir] build pic <n> from <dir>/<n>.png (+ layers) → <n>.P56
5
+ * pnpm pic roundtrip parse + write every pic, compare bytes
6
+ * Flags: --no-dither (import).
7
+ */
8
+ import { mkdirSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { ResourceManager, ResourceType, hunkToPalette, mergePalette, parsePicFile, writePic, type PicFile } from "../packages/sci/src/index.ts";
11
+ import { nodeFiles } from "./node-files.ts";
12
+ import { writePatch } from "./patches.ts";
13
+ import { basePalette, picFromPngs } from "./pic-build.ts";
14
+ import { rgbaPng } from "./png.ts";
15
+
16
+ const argv = process.argv.slice(2);
17
+ const flags = new Set(argv.filter((a) => a.startsWith("--")));
18
+ const [command = "roundtrip", ...args] = argv.filter((a) => !a.startsWith("--"));
19
+ const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
20
+
21
+ /** Composites a pic's layers (or just some) to RGBA, 320x190. */
22
+ function render(pic: PicFile, palette: ReturnType<typeof hunkToPalette>, only?: (priority: number) => boolean) {
23
+ const w = 320, h = 190;
24
+ const data = new Uint8Array(w * h * 4);
25
+ for (const cel of [...pic.cels].sort((a, b) => a.priority - b.priority)) {
26
+ if (only && !only(cel.priority)) continue;
27
+ for (let y = 0; y < cel.height; y++) for (let x = 0; x < cel.width; x++) {
28
+ const c = cel.pixels[y * cel.width + x]!;
29
+ const X = cel.x + x, Y = cel.y + y;
30
+ if (c === cel.skipColor || X < 0 || Y < 0 || X >= w || Y >= h) continue;
31
+ data.set([palette.rgb[c * 3]!, palette.rgb[c * 3 + 1]!, palette.rgb[c * 3 + 2]!, 255], (Y * w + X) * 4);
32
+ }
33
+ }
34
+ return { width: w, height: h, data };
35
+ }
36
+
37
+ if (command === "export") {
38
+ const n = Number(args[0]);
39
+ const dir = args[1] ?? "out/pics";
40
+ mkdirSync(dir, { recursive: true });
41
+ const pic = parsePicFile((await rm.load({ type: ResourceType.Pic, number: n })).data);
42
+ const palette = mergePalette(await basePalette(rm), hunkToPalette(pic.palette));
43
+ writeFileSync(join(dir, `${n}.png`), rgbaPng(render(pic, palette, (p) => p === -1000)));
44
+ for (const p of new Set(pic.cels.map((c) => c.priority).filter((p) => p > -1000))) {
45
+ writeFileSync(join(dir, `${n}.layer-${p}.png`), rgbaPng(render(pic, palette, (q) => q === p)));
46
+ }
47
+ console.log(`pic ${n}: ${pic.cels.length} cels → ${dir}/${n}.png${pic.cels.length > 1 ? ` + ${pic.cels.length - 1} layers` : ""}`);
48
+ } else if (command === "import") {
49
+ const [dir, n, outDir = "out/pics"] = [args[0]!, Number(args[1]), args[2]];
50
+ const pic = await picFromPngs(dir, n, rm, !flags.has("--no-dither"));
51
+ writePatch(outDir, ResourceType.Pic, n, writePic(pic));
52
+ } else {
53
+ const list = rm.list(ResourceType.Pic);
54
+ const bad: number[] = [];
55
+ for (const info of list) {
56
+ const d = Buffer.from((await rm.load(info)).data);
57
+ if (d.compare(Buffer.from(writePic(parsePicFile(d))))) bad.push(info.number);
58
+ }
59
+ console.log(`${list.length - bad.length}/${list.length} pics round-trip byte-exact${bad.length ? `; differ: ${bad.join(" ")}` : ""}`);
60
+ process.exitCode = bad.length ? 1 : 0;
61
+ }
@@ -0,0 +1 @@
1
+ export {};
package/tools/play.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Headless game runner for debugging the VM.
3
+ * pnpm play [--frames N] [--trace] [--png-every K]
4
+ * boot the game, run N FrameOut cycles, report kernel usage, save every Kth frame to out/
5
+ */
6
+ import { mkdirSync, writeFileSync } from "node:fs";
7
+ import { ResourceManager, Vm, allKernels, formatValue, graphics } from "../packages/sci/src/index.js";
8
+ import { nodeFiles } from "./node-files.js";
9
+ import { framePng } from "./png.js";
10
+ const argv = process.argv.slice(2);
11
+ const frames = Number(argv[argv.indexOf("--frames") + 1] || 1);
12
+ const trace = argv.includes("--trace");
13
+ const pngEvery = Number(argv[argv.indexOf("--png-every") + 1] || 0) || (argv.includes("--png-every") ? 1 : 0);
14
+ const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
15
+ await rm.preload();
16
+ const vm = new Vm(rm);
17
+ vm.registerKernels(allKernels);
18
+ // Deterministic time: each frame is one 60 Hz tick, however fast we actually run.
19
+ vm.clock = () => (graphics(vm).frames * 1000) / 60;
20
+ const calls = new Map();
21
+ vm.onKernelCall = (name, args, result) => {
22
+ calls.set(name, (calls.get(name) ?? 0) + 1);
23
+ if (trace)
24
+ console.log(` ${name}(${args.map(formatValue).join(", ")}) → ${formatValue(result)}`);
25
+ };
26
+ if (pngEvery)
27
+ mkdirSync("out/frames", { recursive: true });
28
+ graphics(vm).onFrame = (f) => {
29
+ const n = graphics(vm).frames;
30
+ if (pngEvery && n % pngEvery === 0)
31
+ writeFileSync(`out/frames/${String(n).padStart(5, "0")}.png`, framePng(f));
32
+ };
33
+ const game = vm.exportAddress(0, 0);
34
+ console.log(`game object: ${vm.object(game).name}`);
35
+ vm.start(game, "play");
36
+ let frame = 0;
37
+ const t0 = performance.now();
38
+ try {
39
+ while (vm.running && frame < frames) {
40
+ vm.run();
41
+ if (vm.yieldRequested)
42
+ frame++;
43
+ else if (vm.running)
44
+ throw new Error("instruction budget exhausted without a frame (infinite loop?)");
45
+ }
46
+ }
47
+ catch (e) {
48
+ console.log(`\nERROR: ${e.message}`);
49
+ console.log(vm.backtrace().map((l) => ` at ${l}`).join("\n"));
50
+ }
51
+ const ms = performance.now() - t0;
52
+ console.log(`\n${vm.instructions.toLocaleString()} instructions in ${ms.toFixed(0)} ms, ${frame} frame(s), running=${vm.running}`);
53
+ console.log("kernel calls:", Object.fromEntries([...calls].sort((a, b) => b[1] - a[1])));
54
+ console.log("missing kernels:", Object.fromEntries(vm.missingKernels));
package/tools/play.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Headless game runner for debugging the VM.
3
+ * pnpm play [--frames N] [--trace] [--png-every K]
4
+ * boot the game, run N FrameOut cycles, report kernel usage, save every Kth frame to out/
5
+ */
6
+ import { mkdirSync, writeFileSync } from "node:fs";
7
+ import { ResourceManager, Vm, allKernels, formatValue, graphics } from "../packages/sci/src/index.ts";
8
+ import { nodeFiles } from "./node-files.ts";
9
+ import { framePng } from "./png.ts";
10
+
11
+ const argv = process.argv.slice(2);
12
+ const frames = Number(argv[argv.indexOf("--frames") + 1] || 1);
13
+ const trace = argv.includes("--trace");
14
+ const pngEvery = Number(argv[argv.indexOf("--png-every") + 1] || 0) || (argv.includes("--png-every") ? 1 : 0);
15
+
16
+ const rm = await ResourceManager.open(nodeFiles(process.env.SCI_GAME ?? "original"));
17
+ await rm.preload();
18
+ const vm = new Vm(rm);
19
+ vm.registerKernels(allKernels);
20
+ // Deterministic time: each frame is one 60 Hz tick, however fast we actually run.
21
+ vm.clock = () => (graphics(vm).frames * 1000) / 60;
22
+
23
+ const calls = new Map<string, number>();
24
+ vm.onKernelCall = (name, args, result) => {
25
+ calls.set(name, (calls.get(name) ?? 0) + 1);
26
+ if (trace) console.log(` ${name}(${args.map(formatValue).join(", ")}) → ${formatValue(result)}`);
27
+ };
28
+
29
+ if (pngEvery) mkdirSync("out/frames", { recursive: true });
30
+ graphics(vm).onFrame = (f) => {
31
+ const n = graphics(vm).frames;
32
+ if (pngEvery && n % pngEvery === 0) writeFileSync(`out/frames/${String(n).padStart(5, "0")}.png`, framePng(f));
33
+ };
34
+
35
+ const game = vm.exportAddress(0, 0);
36
+ console.log(`game object: ${vm.object(game).name}`);
37
+ vm.start(game, "play");
38
+
39
+ let frame = 0;
40
+ const t0 = performance.now();
41
+ try {
42
+ while (vm.running && frame < frames) {
43
+ vm.run();
44
+ if (vm.yieldRequested) frame++;
45
+ else if (vm.running) throw new Error("instruction budget exhausted without a frame (infinite loop?)");
46
+ }
47
+ } catch (e) {
48
+ console.log(`\nERROR: ${(e as Error).message}`);
49
+ console.log(vm.backtrace().map((l) => ` at ${l}`).join("\n"));
50
+ }
51
+ const ms = performance.now() - t0;
52
+ console.log(`\n${vm.instructions.toLocaleString()} instructions in ${ms.toFixed(0)} ms, ${frame} frame(s), running=${vm.running}`);
53
+ console.log("kernel calls:", Object.fromEntries([...calls].sort((a, b) => b[1] - a[1])));
54
+ console.log("missing kernels:", Object.fromEntries(vm.missingKernels));
package/tools/png.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type Frame } from "../packages/sci/src/index.ts";
2
+ /** Encodes an indexed frame as an RGB PNG, optionally scaled up (with 4:3 aspect correction). */
3
+ export declare function framePng(frame: Frame, scale?: number): Buffer;
4
+ /** An RGBA image. */
5
+ export interface Rgba {
6
+ width: number;
7
+ height: number;
8
+ data: Uint8Array;
9
+ }
10
+ /** Encodes RGBA pixels as a PNG, 1:1. */
11
+ export declare function rgbaPng(img: Rgba): Buffer;
12
+ /**
13
+ * Decodes an 8-bit, non-interlaced PNG (greyscale, RGB, palette, grey+alpha or RGBA) to RGBA.
14
+ * Enough for art made in any paint program.
15
+ */
16
+ export declare function decodePng(file: Uint8Array): Rgba;
package/tools/png.js ADDED
@@ -0,0 +1,124 @@
1
+ import { deflateSync, inflateSync } from "node:zlib";
2
+ import { frameRgb } from "../packages/sci/src/index.js";
3
+ const crcTable = Array.from({ length: 256 }, (_, n) => {
4
+ let c = n;
5
+ for (let k = 0; k < 8; k++)
6
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
7
+ return c >>> 0;
8
+ });
9
+ const crc32 = (buf) => {
10
+ let c = 0xffffffff;
11
+ for (const b of buf)
12
+ c = crcTable[(c ^ b) & 0xff] ^ (c >>> 8);
13
+ return (c ^ 0xffffffff) >>> 0;
14
+ };
15
+ function chunk(type, data) {
16
+ const out = Buffer.alloc(12 + data.length);
17
+ out.writeUInt32BE(data.length, 0);
18
+ out.write(type, 4, "ascii");
19
+ out.set(data, 8);
20
+ out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length);
21
+ return out;
22
+ }
23
+ /** Encodes an indexed frame as an RGB PNG, optionally scaled up (with 4:3 aspect correction). */
24
+ export function framePng(frame, scale = 2) {
25
+ const w = frame.width * scale, h = Math.round(frame.height * scale * 1.2);
26
+ const raw = Buffer.alloc((w * 3 + 1) * h);
27
+ const rgb = frameRgb(frame);
28
+ for (let y = 0; y < h; y++) {
29
+ const sy = Math.min(frame.height - 1, Math.floor(y / (scale * 1.2)));
30
+ raw[y * (w * 3 + 1)] = 0;
31
+ for (let x = 0; x < w; x++) {
32
+ const i = (sy * frame.width + Math.floor(x / scale)) * 3;
33
+ raw.set(rgb.subarray(i, i + 3), y * (w * 3 + 1) + 1 + x * 3);
34
+ }
35
+ }
36
+ const ihdr = Buffer.alloc(13);
37
+ ihdr.writeUInt32BE(w, 0);
38
+ ihdr.writeUInt32BE(h, 4);
39
+ ihdr[8] = 8; // bit depth
40
+ ihdr[9] = 2; // RGB
41
+ return Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk("IHDR", ihdr), chunk("IDAT", deflateSync(raw)), chunk("IEND", new Uint8Array())]);
42
+ }
43
+ /** Encodes RGBA pixels as a PNG, 1:1. */
44
+ export function rgbaPng(img) {
45
+ const { width: w, height: h } = img;
46
+ const raw = Buffer.alloc((w * 4 + 1) * h);
47
+ for (let y = 0; y < h; y++)
48
+ raw.set(img.data.subarray(y * w * 4, (y + 1) * w * 4), y * (w * 4 + 1) + 1);
49
+ const ihdr = Buffer.alloc(13);
50
+ ihdr.writeUInt32BE(w, 0);
51
+ ihdr.writeUInt32BE(h, 4);
52
+ ihdr[8] = 8;
53
+ ihdr[9] = 6; // RGBA
54
+ return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk("IHDR", ihdr), chunk("IDAT", deflateSync(raw)), chunk("IEND", new Uint8Array(0))]);
55
+ }
56
+ /**
57
+ * Decodes an 8-bit, non-interlaced PNG (greyscale, RGB, palette, grey+alpha or RGBA) to RGBA.
58
+ * Enough for art made in any paint program.
59
+ */
60
+ export function decodePng(file) {
61
+ const b = Buffer.from(file);
62
+ if (b.readUInt32BE(0) !== 0x89504e47)
63
+ throw new Error("Not a PNG");
64
+ let width = 0, height = 0, depth = 0, type = 0, interlace = 0;
65
+ let plte, trns;
66
+ const idat = [];
67
+ for (let p = 8; p < b.length;) {
68
+ const len = b.readUInt32BE(p), kind = b.toString("ascii", p + 4, p + 8), data = b.subarray(p + 8, p + 8 + len);
69
+ if (kind === "IHDR")
70
+ [width, height, depth, type, interlace] = [data.readUInt32BE(0), data.readUInt32BE(4), data[8], data[9], data[12]];
71
+ else if (kind === "PLTE")
72
+ plte = data;
73
+ else if (kind === "tRNS")
74
+ trns = data;
75
+ else if (kind === "IDAT")
76
+ idat.push(data);
77
+ p += 12 + len;
78
+ }
79
+ if (depth !== 8 || interlace)
80
+ throw new Error(`Only 8-bit non-interlaced PNGs are supported (bit depth ${depth}, interlace ${interlace})`);
81
+ const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[type];
82
+ if (!channels)
83
+ throw new Error(`Unsupported PNG colour type ${type}`);
84
+ const raw = inflateSync(Buffer.concat(idat));
85
+ const stride = width * channels;
86
+ const px = new Uint8Array(stride * height);
87
+ for (let y = 0; y < height; y++) {
88
+ const filter = raw[y * (stride + 1)];
89
+ const src = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
90
+ for (let x = 0; x < stride; x++) {
91
+ const a = x >= channels ? px[y * stride + x - channels] : 0;
92
+ const up = y ? px[(y - 1) * stride + x] : 0;
93
+ const c = y && x >= channels ? px[(y - 1) * stride + x - channels] : 0;
94
+ const pred = filter === 1 ? a : filter === 2 ? up : filter === 3 ? (a + up) >> 1 : filter === 4 ? paeth(a, up, c) : 0;
95
+ px[y * stride + x] = (src[x] + pred) & 0xff;
96
+ }
97
+ }
98
+ const data = new Uint8Array(width * height * 4);
99
+ for (let i = 0; i < width * height; i++) {
100
+ const s = px.subarray(i * channels, i * channels + channels);
101
+ let r, g, bl, al = 255;
102
+ if (type === 3) {
103
+ const k = s[0];
104
+ [r, g, bl] = [plte[k * 3], plte[k * 3 + 1], plte[k * 3 + 2]];
105
+ al = trns && k < trns.length ? trns[k] : 255;
106
+ }
107
+ else if (type === 0 || type === 4) {
108
+ r = g = bl = s[0];
109
+ if (type === 4)
110
+ al = s[1];
111
+ }
112
+ else {
113
+ [r, g, bl] = [s[0], s[1], s[2]];
114
+ if (type === 6)
115
+ al = s[3];
116
+ }
117
+ data.set([r, g, bl, al], i * 4);
118
+ }
119
+ return { width, height, data };
120
+ }
121
+ function paeth(a, b, c) {
122
+ const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
123
+ return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
124
+ }
package/tools/png.ts ADDED
@@ -0,0 +1,123 @@
1
+ import { deflateSync, inflateSync } from "node:zlib";
2
+ import { frameRgb, type Frame } from "../packages/sci/src/index.ts";
3
+
4
+ const crcTable = Array.from({ length: 256 }, (_, n) => {
5
+ let c = n;
6
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
7
+ return c >>> 0;
8
+ });
9
+ const crc32 = (buf: Uint8Array) => {
10
+ let c = 0xffffffff;
11
+ for (const b of buf) c = crcTable[(c ^ b) & 0xff]! ^ (c >>> 8);
12
+ return (c ^ 0xffffffff) >>> 0;
13
+ };
14
+
15
+ function chunk(type: string, data: Uint8Array): Buffer {
16
+ const out = Buffer.alloc(12 + data.length);
17
+ out.writeUInt32BE(data.length, 0);
18
+ out.write(type, 4, "ascii");
19
+ out.set(data, 8);
20
+ out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length);
21
+ return out;
22
+ }
23
+
24
+ /** Encodes an indexed frame as an RGB PNG, optionally scaled up (with 4:3 aspect correction). */
25
+ export function framePng(frame: Frame, scale = 2): Buffer {
26
+ const w = frame.width * scale, h = Math.round(frame.height * scale * 1.2);
27
+ const raw = Buffer.alloc((w * 3 + 1) * h);
28
+ const rgb = frameRgb(frame);
29
+ for (let y = 0; y < h; y++) {
30
+ const sy = Math.min(frame.height - 1, Math.floor(y / (scale * 1.2)));
31
+ raw[y * (w * 3 + 1)] = 0;
32
+ for (let x = 0; x < w; x++) {
33
+ const i = (sy * frame.width + Math.floor(x / scale)) * 3;
34
+ raw.set(rgb.subarray(i, i + 3), y * (w * 3 + 1) + 1 + x * 3);
35
+ }
36
+ }
37
+ const ihdr = Buffer.alloc(13);
38
+ ihdr.writeUInt32BE(w, 0);
39
+ ihdr.writeUInt32BE(h, 4);
40
+ ihdr[8] = 8; // bit depth
41
+ ihdr[9] = 2; // RGB
42
+ return Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk("IHDR", ihdr), chunk("IDAT", deflateSync(raw)), chunk("IEND", new Uint8Array())]);
43
+ }
44
+
45
+ /** An RGBA image. */
46
+ export interface Rgba {
47
+ width: number;
48
+ height: number;
49
+ data: Uint8Array; // width * height * 4
50
+ }
51
+
52
+ /** Encodes RGBA pixels as a PNG, 1:1. */
53
+ export function rgbaPng(img: Rgba): Buffer {
54
+ const { width: w, height: h } = img;
55
+ const raw = Buffer.alloc((w * 4 + 1) * h);
56
+ for (let y = 0; y < h; y++) raw.set(img.data.subarray(y * w * 4, (y + 1) * w * 4), y * (w * 4 + 1) + 1);
57
+ const ihdr = Buffer.alloc(13);
58
+ ihdr.writeUInt32BE(w, 0);
59
+ ihdr.writeUInt32BE(h, 4);
60
+ ihdr[8] = 8;
61
+ ihdr[9] = 6; // RGBA
62
+ return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk("IHDR", ihdr), chunk("IDAT", deflateSync(raw)), chunk("IEND", new Uint8Array(0))]);
63
+ }
64
+
65
+ /**
66
+ * Decodes an 8-bit, non-interlaced PNG (greyscale, RGB, palette, grey+alpha or RGBA) to RGBA.
67
+ * Enough for art made in any paint program.
68
+ */
69
+ export function decodePng(file: Uint8Array): Rgba {
70
+ const b = Buffer.from(file);
71
+ if (b.readUInt32BE(0) !== 0x89504e47) throw new Error("Not a PNG");
72
+ let width = 0, height = 0, depth = 0, type = 0, interlace = 0;
73
+ let plte: Uint8Array | undefined, trns: Uint8Array | undefined;
74
+ const idat: Buffer[] = [];
75
+ for (let p = 8; p < b.length; ) {
76
+ const len = b.readUInt32BE(p), kind = b.toString("ascii", p + 4, p + 8), data = b.subarray(p + 8, p + 8 + len);
77
+ if (kind === "IHDR") [width, height, depth, type, interlace] = [data.readUInt32BE(0), data.readUInt32BE(4), data[8]!, data[9]!, data[12]!];
78
+ else if (kind === "PLTE") plte = data;
79
+ else if (kind === "tRNS") trns = data;
80
+ else if (kind === "IDAT") idat.push(data);
81
+ p += 12 + len;
82
+ }
83
+ if (depth !== 8 || interlace) throw new Error(`Only 8-bit non-interlaced PNGs are supported (bit depth ${depth}, interlace ${interlace})`);
84
+ const channels = ({ 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 } as Record<number, number>)[type];
85
+ if (!channels) throw new Error(`Unsupported PNG colour type ${type}`);
86
+ const raw = inflateSync(Buffer.concat(idat));
87
+ const stride = width * channels;
88
+ const px = new Uint8Array(stride * height);
89
+ for (let y = 0; y < height; y++) {
90
+ const filter = raw[y * (stride + 1)]!;
91
+ const src = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
92
+ for (let x = 0; x < stride; x++) {
93
+ const a = x >= channels ? px[y * stride + x - channels]! : 0;
94
+ const up = y ? px[(y - 1) * stride + x]! : 0;
95
+ const c = y && x >= channels ? px[(y - 1) * stride + x - channels]! : 0;
96
+ const pred = filter === 1 ? a : filter === 2 ? up : filter === 3 ? (a + up) >> 1 : filter === 4 ? paeth(a, up, c) : 0;
97
+ px[y * stride + x] = (src[x]! + pred) & 0xff;
98
+ }
99
+ }
100
+ const data = new Uint8Array(width * height * 4);
101
+ for (let i = 0; i < width * height; i++) {
102
+ const s = px.subarray(i * channels, i * channels + channels);
103
+ let r: number, g: number, bl: number, al = 255;
104
+ if (type === 3) {
105
+ const k = s[0]!;
106
+ [r, g, bl] = [plte![k * 3]!, plte![k * 3 + 1]!, plte![k * 3 + 2]!];
107
+ al = trns && k < trns.length ? trns[k]! : 255;
108
+ } else if (type === 0 || type === 4) {
109
+ r = g = bl = s[0]!;
110
+ if (type === 4) al = s[1]!;
111
+ } else {
112
+ [r, g, bl] = [s[0]!, s[1]!, s[2]!];
113
+ if (type === 6) al = s[3]!;
114
+ }
115
+ data.set([r, g, bl, al], i * 4);
116
+ }
117
+ return { width, height, data };
118
+ }
119
+
120
+ function paeth(a: number, b: number, c: number): number {
121
+ const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
122
+ return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
123
+ }
package/tools/res.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};