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,139 @@
1
+ /**
2
+ * The reader for SCI script source: parenthesised lists, with a few kinds of atom.
3
+ *
4
+ * (send obj x: 1) a list [buf 3] an array element (or declaration)
5
+ * foo x: y? &rest symbols 42 -7 $1F %101 `a numbers
6
+ * "text" {text} strings #init a selector
7
+ * @buf @[buf i] a variable's address, an element's ; ... a comment, to the end of the line
8
+ */
9
+ export type Node =
10
+ | { kind: "list"; items: Node[]; line: number }
11
+ | { kind: "index"; items: Node[]; line: number }
12
+ | { kind: "sym"; name: string; line: number }
13
+ | { kind: "num"; value: number; line: number }
14
+ | { kind: "str"; value: string; line: number }
15
+ | { kind: "sel"; name: string; line: number }
16
+ | { kind: "addr"; name: string; index?: Node; line: number };
17
+
18
+ export class ReadError extends Error {
19
+ constructor(message: string, readonly line: number) {
20
+ super(`line ${line}: ${message}`);
21
+ }
22
+ }
23
+
24
+ const DELIMITERS = new Set([..."()[]{}\";"]);
25
+ const isSpace = (c: string) => c === " " || c === "\t" || c === "\r" || c === "\n";
26
+
27
+ export function read(text: string): Node[] {
28
+ let p = 0, line = 1;
29
+ const top: Node[] = [];
30
+ const stack: { node: Node & { items: Node[] }; close: string }[] = [];
31
+ const add = (n: Node) => (stack.length ? stack[stack.length - 1]!.node.items : top).push(n);
32
+
33
+ while (p < text.length) {
34
+ const c = text[p]!;
35
+ if (c === "\n") { line++; p++; continue; }
36
+ if (isSpace(c)) { p++; continue; }
37
+ if (c === ";") { while (p < text.length && text[p] !== "\n") p++; continue; }
38
+ if (c === "(" || c === "[") {
39
+ const node = { kind: c === "(" ? "list" : "index", items: [], line } as Node & { items: Node[] };
40
+ add(node);
41
+ stack.push({ node, close: c === "(" ? ")" : "]" });
42
+ p++;
43
+ continue;
44
+ }
45
+ if (c === ")" || c === "]") {
46
+ const open = stack.pop();
47
+ if (!open) throw new ReadError(`unexpected ${c}`, line);
48
+ if (open.close !== c) throw new ReadError(`expected ${open.close} to close the ${open.close === ")" ? "(" : "["} on line ${open.node.line}, got ${c}`, line);
49
+ p++;
50
+ continue;
51
+ }
52
+ if (c === '"' || c === "{") {
53
+ const end = c === '"' ? '"' : "}";
54
+ const start = line;
55
+ let value = "";
56
+ p++;
57
+ for (;;) {
58
+ if (p >= text.length) throw new ReadError("string never ends", start);
59
+ const ch = text[p]!;
60
+ if (ch === end) { p++; break; }
61
+ if (ch === "\n") line++;
62
+ if (ch === "\\") {
63
+ const n = text[p + 1] ?? "";
64
+ if (n === "n") value += "\n";
65
+ else if (n === "t") value += "\t";
66
+ else if (/[0-9a-fA-F]/.test(n) && /[0-9a-fA-F]/.test(text[p + 2] ?? "")) {
67
+ value += String.fromCharCode(parseInt(text.slice(p + 1, p + 3), 16));
68
+ p++;
69
+ } else value += n;
70
+ p += 2;
71
+ continue;
72
+ }
73
+ value += ch;
74
+ p++;
75
+ }
76
+ add({ kind: "str", value, line: start });
77
+ continue;
78
+ }
79
+ if (c === "`") {
80
+ // A character: `a is 97; `^a is control-A; `#4 is a key with a scan code (not supported).
81
+ const ch = text[p + 1];
82
+ if (ch === undefined) throw new ReadError("` needs a character", line);
83
+ if (ch === "^" && text[p + 2] && !isSpace(text[p + 2]!)) {
84
+ add({ kind: "num", value: text[p + 2]!.toUpperCase().charCodeAt(0) - 64, line });
85
+ p += 3;
86
+ } else {
87
+ add({ kind: "num", value: ch.charCodeAt(0), line });
88
+ p += 2;
89
+ }
90
+ continue;
91
+ }
92
+ let e = p;
93
+ while (e < text.length && !isSpace(text[e]!) && !DELIMITERS.has(text[e]!)) e++;
94
+ const token = text.slice(p, e);
95
+ p = e;
96
+ add(atom(token, line));
97
+ }
98
+ if (stack.length) throw new ReadError(`( never closed`, stack[stack.length - 1]!.node.line);
99
+ return addresses(top);
100
+ }
101
+
102
+ /** `@` then `[buf i]` is one thing: the address of an element. */
103
+ function addresses(nodes: Node[]): Node[] {
104
+ const out: Node[] = [];
105
+ for (let i = 0; i < nodes.length; i++) {
106
+ const n = nodes[i]!;
107
+ const next = nodes[i + 1];
108
+ if (n.kind === "sym" && n.name === "@" && next?.kind === "index" && next.items.length === 2 && next.items[0]!.kind === "sym") {
109
+ out.push({ kind: "addr", name: (next.items[0] as { name: string }).name, index: addresses([next.items[1]!])[0], line: n.line });
110
+ i++;
111
+ continue;
112
+ }
113
+ if (n.kind === "list" || n.kind === "index") out.push({ ...n, items: addresses(n.items) });
114
+ else out.push(n);
115
+ }
116
+ return out;
117
+ }
118
+
119
+ function atom(token: string, line: number): Node {
120
+ if (/^-?\d+$/.test(token)) return { kind: "num", value: Number(token), line };
121
+ if (/^-?\$[0-9a-fA-F]+$/.test(token)) return { kind: "num", value: (token.startsWith("-") ? -1 : 1) * parseInt(token.replace(/^-?\$/, ""), 16), line };
122
+ if (/^%[01]+$/.test(token)) return { kind: "num", value: parseInt(token.slice(1), 2), line };
123
+ if (token.startsWith("#") && token.length > 1) return { kind: "sel", name: token.slice(1), line };
124
+ if (token.startsWith("@") && token.length > 1) return { kind: "addr", name: token.slice(1), line };
125
+ return { kind: "sym", name: token, line };
126
+ }
127
+
128
+ /** A node as source text again, for error messages. */
129
+ export function show(n: Node): string {
130
+ switch (n.kind) {
131
+ case "list": return `(${n.items.map(show).join(" ")})`;
132
+ case "index": return `[${n.items.map(show).join(" ")}]`;
133
+ case "sym": return n.name;
134
+ case "num": return String(n.value);
135
+ case "str": return JSON.stringify(n.value);
136
+ case "sel": return `#${n.name}`;
137
+ case "addr": return n.index ? `@[${n.name} ${show(n.index)}]` : `@${n.name}`;
138
+ }
139
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Vocab 997: selector names, indexed by selector id. Selectors name every property and
3
+ * method across all classes (e.g. 0x93 = init, 0x45 = doit).
4
+ * u16 count, u16 offset per selector → { u16 length, chars }
5
+ */
6
+ export declare function parseSelectorNames(data: Uint8Array): string[];
7
+ /**
8
+ * Vocab 996: which script defines each class, indexed by class (species) number.
9
+ * 4 bytes per class: u16 (unused), u16 script number
10
+ */
11
+ export declare function parseClassTable(data: Uint8Array): number[];
12
+ /** Vocab 997 from selector names, in id order (the inverse of parseSelectorNames). */
13
+ export declare function writeSelectorNames(names: readonly string[]): Uint8Array;
14
+ /** Vocab 996 from the script defining each class, by species (the inverse of parseClassTable). */
15
+ export declare function writeClassTable(scripts: readonly number[]): Uint8Array;
@@ -0,0 +1,44 @@
1
+ const view = (d) => new DataView(d.buffer, d.byteOffset, d.byteLength);
2
+ /**
3
+ * Vocab 997: selector names, indexed by selector id. Selectors name every property and
4
+ * method across all classes (e.g. 0x93 = init, 0x45 = doit).
5
+ * u16 count, u16 offset per selector → { u16 length, chars }
6
+ */
7
+ export function parseSelectorNames(data) {
8
+ const v = view(data);
9
+ const count = v.getUint16(0, true);
10
+ const decoder = new TextDecoder("latin1");
11
+ return Array.from({ length: count }, (_, i) => {
12
+ const o = v.getUint16(2 + i * 2, true);
13
+ return decoder.decode(data.subarray(o + 2, o + 2 + v.getUint16(o, true)));
14
+ });
15
+ }
16
+ /**
17
+ * Vocab 996: which script defines each class, indexed by class (species) number.
18
+ * 4 bytes per class: u16 (unused), u16 script number
19
+ */
20
+ export function parseClassTable(data) {
21
+ const v = view(data);
22
+ return Array.from({ length: data.length / 4 }, (_, i) => v.getUint16(i * 4 + 2, true));
23
+ }
24
+ /** Vocab 997 from selector names, in id order (the inverse of parseSelectorNames). */
25
+ export function writeSelectorNames(names) {
26
+ const bytes = names.map((n) => Uint8Array.from(n, (ch) => ch.charCodeAt(0) & 0xff));
27
+ const out = new Uint8Array(2 + names.length * 2 + bytes.reduce((n, b) => n + 2 + b.length, 0));
28
+ const v = view(out);
29
+ v.setUint16(0, names.length, true);
30
+ let at = 2 + names.length * 2;
31
+ bytes.forEach((b, i) => {
32
+ v.setUint16(2 + i * 2, at, true);
33
+ v.setUint16(at, b.length, true);
34
+ out.set(b, at + 2);
35
+ at += 2 + b.length;
36
+ });
37
+ return out;
38
+ }
39
+ /** Vocab 996 from the script defining each class, by species (the inverse of parseClassTable). */
40
+ export function writeClassTable(scripts) {
41
+ const out = new Uint8Array(scripts.length * 4);
42
+ scripts.forEach((s, i) => view(out).setUint16(i * 4 + 2, s, true));
43
+ return out;
44
+ }
@@ -0,0 +1,48 @@
1
+ const view = (d: Uint8Array) => new DataView(d.buffer, d.byteOffset, d.byteLength);
2
+
3
+ /**
4
+ * Vocab 997: selector names, indexed by selector id. Selectors name every property and
5
+ * method across all classes (e.g. 0x93 = init, 0x45 = doit).
6
+ * u16 count, u16 offset per selector → { u16 length, chars }
7
+ */
8
+ export function parseSelectorNames(data: Uint8Array): string[] {
9
+ const v = view(data);
10
+ const count = v.getUint16(0, true);
11
+ const decoder = new TextDecoder("latin1");
12
+ return Array.from({ length: count }, (_, i) => {
13
+ const o = v.getUint16(2 + i * 2, true);
14
+ return decoder.decode(data.subarray(o + 2, o + 2 + v.getUint16(o, true)));
15
+ });
16
+ }
17
+
18
+ /**
19
+ * Vocab 996: which script defines each class, indexed by class (species) number.
20
+ * 4 bytes per class: u16 (unused), u16 script number
21
+ */
22
+ export function parseClassTable(data: Uint8Array): number[] {
23
+ const v = view(data);
24
+ return Array.from({ length: data.length / 4 }, (_, i) => v.getUint16(i * 4 + 2, true));
25
+ }
26
+
27
+ /** Vocab 997 from selector names, in id order (the inverse of parseSelectorNames). */
28
+ export function writeSelectorNames(names: readonly string[]): Uint8Array {
29
+ const bytes = names.map((n) => Uint8Array.from(n, (ch) => ch.charCodeAt(0) & 0xff));
30
+ const out = new Uint8Array(2 + names.length * 2 + bytes.reduce((n, b) => n + 2 + b.length, 0));
31
+ const v = view(out);
32
+ v.setUint16(0, names.length, true);
33
+ let at = 2 + names.length * 2;
34
+ bytes.forEach((b, i) => {
35
+ v.setUint16(2 + i * 2, at, true);
36
+ v.setUint16(at, b.length, true);
37
+ out.set(b, at + 2);
38
+ at += 2 + b.length;
39
+ });
40
+ return out;
41
+ }
42
+
43
+ /** Vocab 996 from the script defining each class, by species (the inverse of parseClassTable). */
44
+ export function writeClassTable(scripts: readonly number[]): Uint8Array {
45
+ const out = new Uint8Array(scripts.length * 4);
46
+ scripts.forEach((s, i) => view(out).setUint16(i * 4 + 2, s, true));
47
+ return out;
48
+ }
@@ -0,0 +1,25 @@
1
+ import type { ResourceManager } from "../resource/manager.ts";
2
+ import type { AsmContext } from "./assembly.ts";
3
+ import type { DisasmContext } from "./disasm.ts";
4
+ import { type Script, type ScriptObject } from "./script.ts";
5
+ /**
6
+ * Game-wide script knowledge: selector names, the class table, and lazily loaded scripts.
7
+ * Resolves class names and property layouts across script boundaries.
8
+ */
9
+ export declare class ScriptWorld {
10
+ private readonly rm;
11
+ readonly selectorNames: string[];
12
+ readonly classScripts: number[];
13
+ private readonly scripts;
14
+ private readonly classes;
15
+ private constructor();
16
+ static open(rm: ResourceManager): Promise<ScriptWorld>;
17
+ script(number: number): Promise<Script>;
18
+ /** Loads every script that defines a class, so class lookups are synchronous afterwards. */
19
+ loadAllClasses(): Promise<void>;
20
+ classObject(species: number): ScriptObject | undefined;
21
+ /** For the assembler: names both ways and class layouts. Requires `loadAllClasses()`. */
22
+ asmContext(): AsmContext;
23
+ /** Requires `loadAllClasses()` first. */
24
+ context(): DisasmContext;
25
+ }
@@ -0,0 +1,65 @@
1
+ import { ResourceType } from "../resource/types.js";
2
+ import { kernelNames } from "./kernel-names.js";
3
+ import { parseScript } from "./script.js";
4
+ import { parseClassTable, parseSelectorNames } from "./vocab.js";
5
+ /**
6
+ * Game-wide script knowledge: selector names, the class table, and lazily loaded scripts.
7
+ * Resolves class names and property layouts across script boundaries.
8
+ */
9
+ export class ScriptWorld {
10
+ rm;
11
+ selectorNames;
12
+ classScripts;
13
+ scripts = new Map();
14
+ classes = new Map();
15
+ constructor(rm, selectorNames, classScripts) {
16
+ this.rm = rm;
17
+ this.selectorNames = selectorNames;
18
+ this.classScripts = classScripts;
19
+ }
20
+ static async open(rm) {
21
+ const vocab = async (n) => (await rm.load({ type: ResourceType.Vocab, number: n })).data;
22
+ return new ScriptWorld(rm, parseSelectorNames(await vocab(997)), parseClassTable(await vocab(996)));
23
+ }
24
+ async script(number) {
25
+ let s = this.scripts.get(number);
26
+ if (!s) {
27
+ const code = (await this.rm.load({ type: ResourceType.Script, number })).data;
28
+ const heap = (await this.rm.load({ type: ResourceType.Heap, number })).data;
29
+ s = parseScript(number, code, heap);
30
+ this.scripts.set(number, s);
31
+ for (const obj of s.objects)
32
+ if (obj.isClass)
33
+ this.classes.set(obj.species, obj);
34
+ }
35
+ return s;
36
+ }
37
+ /** Loads every script that defines a class, so class lookups are synchronous afterwards. */
38
+ async loadAllClasses() {
39
+ const numbers = new Set(this.classScripts.filter((n) => this.rm.has({ type: ResourceType.Script, number: n })));
40
+ for (const n of numbers)
41
+ await this.script(n);
42
+ }
43
+ classObject(species) {
44
+ return this.classes.get(species);
45
+ }
46
+ /** For the assembler: names both ways and class layouts. Requires `loadAllClasses()`. */
47
+ asmContext() {
48
+ const byName = new Map([...this.classes.values()].map((c) => [c.name, c.species]));
49
+ return {
50
+ ...this.context(),
51
+ classSpecies: (name) => byName.get(name),
52
+ classPropSelectors: (species) => this.classes.get(species)?.propSelectors,
53
+ classDefaults: (species) => this.classes.get(species)?.properties,
54
+ };
55
+ }
56
+ /** Requires `loadAllClasses()` first. */
57
+ context() {
58
+ return {
59
+ selectorNames: this.selectorNames,
60
+ kernelNames,
61
+ className: (species) => this.classes.get(species)?.name,
62
+ propSelectors: (obj) => (obj.isClass ? obj.propSelectors : this.classes.get(obj.superclass)?.propSelectors),
63
+ };
64
+ }
65
+ }
@@ -0,0 +1,70 @@
1
+ import type { ResourceManager } from "../resource/manager.ts";
2
+ import { ResourceType } from "../resource/types.ts";
3
+ import type { AsmContext } from "./assembly.ts";
4
+ import type { DisasmContext } from "./disasm.ts";
5
+ import { kernelNames } from "./kernel-names.ts";
6
+ import { parseScript, type Script, type ScriptObject } from "./script.ts";
7
+ import { parseClassTable, parseSelectorNames } from "./vocab.ts";
8
+
9
+ /**
10
+ * Game-wide script knowledge: selector names, the class table, and lazily loaded scripts.
11
+ * Resolves class names and property layouts across script boundaries.
12
+ */
13
+ export class ScriptWorld {
14
+ private readonly scripts = new Map<number, Script>();
15
+ private readonly classes = new Map<number, ScriptObject>();
16
+
17
+ private constructor(
18
+ private readonly rm: ResourceManager,
19
+ readonly selectorNames: string[],
20
+ readonly classScripts: number[],
21
+ ) {}
22
+
23
+ static async open(rm: ResourceManager): Promise<ScriptWorld> {
24
+ const vocab = async (n: number) => (await rm.load({ type: ResourceType.Vocab, number: n })).data;
25
+ return new ScriptWorld(rm, parseSelectorNames(await vocab(997)), parseClassTable(await vocab(996)));
26
+ }
27
+
28
+ async script(number: number): Promise<Script> {
29
+ let s = this.scripts.get(number);
30
+ if (!s) {
31
+ const code = (await this.rm.load({ type: ResourceType.Script, number })).data;
32
+ const heap = (await this.rm.load({ type: ResourceType.Heap, number })).data;
33
+ s = parseScript(number, code, heap);
34
+ this.scripts.set(number, s);
35
+ for (const obj of s.objects) if (obj.isClass) this.classes.set(obj.species, obj);
36
+ }
37
+ return s;
38
+ }
39
+
40
+ /** Loads every script that defines a class, so class lookups are synchronous afterwards. */
41
+ async loadAllClasses(): Promise<void> {
42
+ const numbers = new Set(this.classScripts.filter((n) => this.rm.has({ type: ResourceType.Script, number: n })));
43
+ for (const n of numbers) await this.script(n);
44
+ }
45
+
46
+ classObject(species: number): ScriptObject | undefined {
47
+ return this.classes.get(species);
48
+ }
49
+
50
+ /** For the assembler: names both ways and class layouts. Requires `loadAllClasses()`. */
51
+ asmContext(): AsmContext {
52
+ const byName = new Map([...this.classes.values()].map((c) => [c.name, c.species] as const));
53
+ return {
54
+ ...this.context(),
55
+ classSpecies: (name) => byName.get(name),
56
+ classPropSelectors: (species) => this.classes.get(species)?.propSelectors,
57
+ classDefaults: (species) => this.classes.get(species)?.properties,
58
+ };
59
+ }
60
+
61
+ /** Requires `loadAllClasses()` first. */
62
+ context(): DisasmContext {
63
+ return {
64
+ selectorNames: this.selectorNames,
65
+ kernelNames,
66
+ className: (species) => this.classes.get(species)?.name,
67
+ propSelectors: (obj) => (obj.isClass ? obj.propSelectors : this.classes.get(obj.superclass)?.propSelectors),
68
+ };
69
+ }
70
+ }
@@ -0,0 +1,37 @@
1
+ export interface Glyph {
2
+ width: number;
3
+ height: number;
4
+ /** 1 byte per pixel, 1 = ink. */
5
+ pixels: Uint8Array;
6
+ }
7
+ export interface Font {
8
+ /** Line height in pixels. */
9
+ height: number;
10
+ /** Indexed by character code; missing codes are undefined. */
11
+ glyphs: (Glyph | undefined)[];
12
+ }
13
+ /**
14
+ * SCI bitmap font (unchanged since SCI0):
15
+ *
16
+ * +0 u16 first char (unused in practice)
17
+ * +2 u16 char count
18
+ * +4 u16 line height
19
+ * +6 u16 offset per char
20
+ *
21
+ * Each glyph: u8 width, u8 height, then rows of 1bpp pixels, MSB first,
22
+ * each row padded to a whole byte.
23
+ */
24
+ export declare function parseFont(data: Uint8Array): Font;
25
+ /** A font resource (the inverse of parseFont). Missing glyphs are written as 0×0. */
26
+ export declare function writeFont(font: Font): Uint8Array;
27
+ export declare const textWidth: (font: Font, text: string) => number;
28
+ /** Splits text into lines no wider than `maxWidth`, breaking at spaces and honouring CR/LF. */
29
+ export declare function wrapText(font: Font, text: string, maxWidth: number): string[];
30
+ /** A 1-byte-per-pixel ink mask of rendered text. */
31
+ export interface TextBitmap {
32
+ width: number;
33
+ height: number;
34
+ pixels: Uint8Array;
35
+ }
36
+ /** Renders already-wrapped lines, each aligned within the bitmap width. */
37
+ export declare function renderText(font: Font, lines: string[], align?: "left" | "center"): TextBitmap;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * SCI bitmap font (unchanged since SCI0):
3
+ *
4
+ * +0 u16 first char (unused in practice)
5
+ * +2 u16 char count
6
+ * +4 u16 line height
7
+ * +6 u16 offset per char
8
+ *
9
+ * Each glyph: u8 width, u8 height, then rows of 1bpp pixels, MSB first,
10
+ * each row padded to a whole byte.
11
+ */
12
+ export function parseFont(data) {
13
+ const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
14
+ const count = v.getUint16(2, true);
15
+ const height = v.getUint16(4, true);
16
+ const glyphs = [];
17
+ for (let c = 0; c < count; c++) {
18
+ const offset = v.getUint16(6 + c * 2, true);
19
+ const width = data[offset];
20
+ const h = data[offset + 1];
21
+ const stride = Math.ceil(width / 8);
22
+ const pixels = new Uint8Array(width * h);
23
+ for (let y = 0; y < h; y++) {
24
+ for (let x = 0; x < width; x++) {
25
+ const byte = data[offset + 2 + y * stride + (x >> 3)];
26
+ pixels[y * width + x] = (byte >> (7 - (x & 7))) & 1;
27
+ }
28
+ }
29
+ glyphs[c] = { width, height: h, pixels };
30
+ }
31
+ return { height, glyphs };
32
+ }
33
+ /** A font resource (the inverse of parseFont). Missing glyphs are written as 0×0. */
34
+ export function writeFont(font) {
35
+ const count = font.glyphs.length;
36
+ const bodies = Array.from({ length: count }, (_, c) => {
37
+ const g = font.glyphs[c] ?? { width: 0, height: 0, pixels: new Uint8Array(0) };
38
+ if (g.width > 255 || g.height > 255)
39
+ throw new Error(`glyph ${c} is ${g.width}x${g.height}`);
40
+ const stride = Math.ceil(g.width / 8);
41
+ const out = new Uint8Array(2 + stride * g.height);
42
+ out[0] = g.width;
43
+ out[1] = g.height;
44
+ for (let y = 0; y < g.height; y++) {
45
+ for (let x = 0; x < g.width; x++)
46
+ if (g.pixels[y * g.width + x])
47
+ out[2 + y * stride + (x >> 3)] |= 0x80 >> (x & 7);
48
+ }
49
+ return out;
50
+ });
51
+ const out = new Uint8Array(6 + count * 2 + bodies.reduce((n, b) => n + b.length, 0));
52
+ const v = new DataView(out.buffer);
53
+ v.setUint16(2, count, true);
54
+ v.setUint16(4, font.height, true);
55
+ let at = 6 + count * 2;
56
+ bodies.forEach((b, c) => {
57
+ if (at > 0xffff)
58
+ throw new Error("font too large: offsets are 16-bit");
59
+ v.setUint16(6 + c * 2, at, true);
60
+ out.set(b, at);
61
+ at += b.length;
62
+ });
63
+ return out;
64
+ }
65
+ export const textWidth = (font, text) => [...text].reduce((w, ch) => w + (font.glyphs[ch.charCodeAt(0)]?.width ?? 0), 0);
66
+ /** Splits text into lines no wider than `maxWidth`, breaking at spaces and honouring CR/LF. */
67
+ export function wrapText(font, text, maxWidth) {
68
+ const lines = [];
69
+ for (const paragraph of text.split(/\r\n|\r|\n/)) {
70
+ let line = "";
71
+ for (const word of paragraph.split(/(?<= )/)) {
72
+ if (line && textWidth(font, (line + word).trimEnd()) > maxWidth) {
73
+ lines.push(line.trimEnd());
74
+ line = "";
75
+ }
76
+ line += word;
77
+ }
78
+ lines.push(line.trimEnd());
79
+ }
80
+ return lines;
81
+ }
82
+ /** Renders already-wrapped lines, each aligned within the bitmap width. */
83
+ export function renderText(font, lines, align = "left") {
84
+ const width = Math.max(1, ...lines.map((l) => textWidth(font, l)));
85
+ const height = Math.max(1, lines.length * font.height);
86
+ const pixels = new Uint8Array(width * height);
87
+ lines.forEach((line, row) => {
88
+ let x = align === "center" ? Math.floor((width - textWidth(font, line)) / 2) : 0;
89
+ for (const ch of line) {
90
+ const g = font.glyphs[ch.charCodeAt(0)];
91
+ if (!g)
92
+ continue;
93
+ for (let gy = 0; gy < g.height; gy++) {
94
+ for (let gx = 0; gx < g.width; gx++) {
95
+ if (g.pixels[gy * g.width + gx])
96
+ pixels[(row * font.height + gy) * width + x + gx] = 1;
97
+ }
98
+ }
99
+ x += g.width;
100
+ }
101
+ });
102
+ return { width, height, pixels };
103
+ }
@@ -0,0 +1,125 @@
1
+ export interface Glyph {
2
+ width: number;
3
+ height: number;
4
+ /** 1 byte per pixel, 1 = ink. */
5
+ pixels: Uint8Array;
6
+ }
7
+
8
+ export interface Font {
9
+ /** Line height in pixels. */
10
+ height: number;
11
+ /** Indexed by character code; missing codes are undefined. */
12
+ glyphs: (Glyph | undefined)[];
13
+ }
14
+
15
+ /**
16
+ * SCI bitmap font (unchanged since SCI0):
17
+ *
18
+ * +0 u16 first char (unused in practice)
19
+ * +2 u16 char count
20
+ * +4 u16 line height
21
+ * +6 u16 offset per char
22
+ *
23
+ * Each glyph: u8 width, u8 height, then rows of 1bpp pixels, MSB first,
24
+ * each row padded to a whole byte.
25
+ */
26
+ export function parseFont(data: Uint8Array): Font {
27
+ const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
28
+ const count = v.getUint16(2, true);
29
+ const height = v.getUint16(4, true);
30
+
31
+ const glyphs: (Glyph | undefined)[] = [];
32
+ for (let c = 0; c < count; c++) {
33
+ const offset = v.getUint16(6 + c * 2, true);
34
+ const width = data[offset]!;
35
+ const h = data[offset + 1]!;
36
+ const stride = Math.ceil(width / 8);
37
+ const pixels = new Uint8Array(width * h);
38
+ for (let y = 0; y < h; y++) {
39
+ for (let x = 0; x < width; x++) {
40
+ const byte = data[offset + 2 + y * stride + (x >> 3)]!;
41
+ pixels[y * width + x] = (byte >> (7 - (x & 7))) & 1;
42
+ }
43
+ }
44
+ glyphs[c] = { width, height: h, pixels };
45
+ }
46
+ return { height, glyphs };
47
+ }
48
+
49
+ /** A font resource (the inverse of parseFont). Missing glyphs are written as 0×0. */
50
+ export function writeFont(font: Font): Uint8Array {
51
+ const count = font.glyphs.length;
52
+ const bodies = Array.from({ length: count }, (_, c) => {
53
+ const g = font.glyphs[c] ?? { width: 0, height: 0, pixels: new Uint8Array(0) };
54
+ if (g.width > 255 || g.height > 255) throw new Error(`glyph ${c} is ${g.width}x${g.height}`);
55
+ const stride = Math.ceil(g.width / 8);
56
+ const out = new Uint8Array(2 + stride * g.height);
57
+ out[0] = g.width;
58
+ out[1] = g.height;
59
+ for (let y = 0; y < g.height; y++) {
60
+ for (let x = 0; x < g.width; x++) if (g.pixels[y * g.width + x]) out[2 + y * stride + (x >> 3)]! |= 0x80 >> (x & 7);
61
+ }
62
+ return out;
63
+ });
64
+ const out = new Uint8Array(6 + count * 2 + bodies.reduce((n, b) => n + b.length, 0));
65
+ const v = new DataView(out.buffer);
66
+ v.setUint16(2, count, true);
67
+ v.setUint16(4, font.height, true);
68
+ let at = 6 + count * 2;
69
+ bodies.forEach((b, c) => {
70
+ if (at > 0xffff) throw new Error("font too large: offsets are 16-bit");
71
+ v.setUint16(6 + c * 2, at, true);
72
+ out.set(b, at);
73
+ at += b.length;
74
+ });
75
+ return out;
76
+ }
77
+
78
+ export const textWidth = (font: Font, text: string): number =>
79
+ [...text].reduce((w, ch) => w + (font.glyphs[ch.charCodeAt(0)]?.width ?? 0), 0);
80
+
81
+ /** Splits text into lines no wider than `maxWidth`, breaking at spaces and honouring CR/LF. */
82
+ export function wrapText(font: Font, text: string, maxWidth: number): string[] {
83
+ const lines: string[] = [];
84
+ for (const paragraph of text.split(/\r\n|\r|\n/)) {
85
+ let line = "";
86
+ for (const word of paragraph.split(/(?<= )/)) {
87
+ if (line && textWidth(font, (line + word).trimEnd()) > maxWidth) {
88
+ lines.push(line.trimEnd());
89
+ line = "";
90
+ }
91
+ line += word;
92
+ }
93
+ lines.push(line.trimEnd());
94
+ }
95
+ return lines;
96
+ }
97
+
98
+ /** A 1-byte-per-pixel ink mask of rendered text. */
99
+ export interface TextBitmap {
100
+ width: number;
101
+ height: number;
102
+ pixels: Uint8Array;
103
+ }
104
+
105
+ /** Renders already-wrapped lines, each aligned within the bitmap width. */
106
+ export function renderText(font: Font, lines: string[], align: "left" | "center" = "left"): TextBitmap {
107
+ const width = Math.max(1, ...lines.map((l) => textWidth(font, l)));
108
+ const height = Math.max(1, lines.length * font.height);
109
+ const pixels = new Uint8Array(width * height);
110
+
111
+ lines.forEach((line, row) => {
112
+ let x = align === "center" ? Math.floor((width - textWidth(font, line)) / 2) : 0;
113
+ for (const ch of line) {
114
+ const g = font.glyphs[ch.charCodeAt(0)];
115
+ if (!g) continue;
116
+ for (let gy = 0; gy < g.height; gy++) {
117
+ for (let gx = 0; gx < g.width; gx++) {
118
+ if (g.pixels[gy * g.width + gx]) pixels[(row * font.height + gy) * width + x + gx] = 1;
119
+ }
120
+ }
121
+ x += g.width;
122
+ }
123
+ });
124
+ return { width, height, pixels };
125
+ }