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,338 @@
1
+ import { ResourceType } from "../../resource/types.js";
2
+ import { parseFont, textWidth, wrapText } from "../../text/font.js";
3
+ import { hasRef, parseMessages } from "../../text/message.js";
4
+ import { NULL, fromInt, segmentOf, toSigned } from "../value.js";
5
+ import { stringHelpers } from "./arrays.js";
6
+ import { graphics, graphicsKernels } from "./graphics.js";
7
+ import { EventType, SciKey, input } from "./system.js";
8
+ const TextAlign = { Left: 0, Center: 1, Right: 2 };
9
+ /** Per-VM text state: font and message caches plus the message cursor. */
10
+ class TextState {
11
+ vm;
12
+ fonts = new Map();
13
+ messages = new Map();
14
+ /** Message cursor stack: the current tuple, plus one entry per reference being followed. */
15
+ cursor = [];
16
+ cursorModule = 0;
17
+ saved = [];
18
+ last;
19
+ constructor(vm) {
20
+ this.vm = vm;
21
+ }
22
+ /** Drops parsed message files (their resources were replaced). */
23
+ forgetMessages() {
24
+ this.messages.clear();
25
+ }
26
+ font(n) {
27
+ let f = this.fonts.get(n);
28
+ if (!f) {
29
+ const id = { type: ResourceType.Font, number: n };
30
+ f = parseFont(this.vm.resources.loadSync(this.vm.resources.has(id) ? id : { type: ResourceType.Font, number: 0 }).data);
31
+ this.fonts.set(n, f);
32
+ }
33
+ return f;
34
+ }
35
+ messageFile(module) {
36
+ if (!this.messages.has(module)) {
37
+ const id = { type: ResourceType.Message, number: module };
38
+ this.messages.set(module, this.vm.resources.has(id) ? parseMessages(this.vm.resources.loadSync(id).data) : null);
39
+ }
40
+ return this.messages.get(module) ?? undefined;
41
+ }
42
+ /**
43
+ * Returns the next message at the cursor, following references (a record with a ref
44
+ * pushes the referenced tuple; running off the end of a referenced sequence pops back).
45
+ */
46
+ next() {
47
+ const file = this.messageFile(this.cursorModule);
48
+ for (let guard = 0; guard < 32 && file; guard++) {
49
+ const t = this.cursor[this.cursor.length - 1];
50
+ if (!t)
51
+ return undefined;
52
+ const rec = file.records.find((r) => r.noun === t.noun && r.verb === t.verb && r.cond === t.cond && r.seq === t.seq);
53
+ if (!rec) {
54
+ if (this.cursor.length > 1) {
55
+ this.cursor.pop();
56
+ continue;
57
+ }
58
+ return undefined;
59
+ }
60
+ if (hasRef(rec)) {
61
+ t.seq++;
62
+ this.cursor.push({ ...rec.ref });
63
+ continue;
64
+ }
65
+ this.last = { module: this.cursorModule, tuple: { ...t } };
66
+ t.seq++;
67
+ return { talker: rec.talker, text: rec.text };
68
+ }
69
+ return undefined;
70
+ }
71
+ push() {
72
+ this.saved.push({ module: this.cursorModule, stack: this.cursor.map((t) => ({ ...t })) });
73
+ }
74
+ pop() {
75
+ const s = this.saved.pop();
76
+ if (s)
77
+ (this.cursorModule = s.module), (this.cursor = s.stack);
78
+ }
79
+ }
80
+ const stateOf = new WeakMap();
81
+ const text_ = (vm) => text(vm);
82
+ const text = (vm) => {
83
+ let t = stateOf.get(vm);
84
+ if (!t)
85
+ stateOf.set(vm, (t = new TextState(vm)));
86
+ return t;
87
+ };
88
+ /** For replaceResources: forget parsed message files. */
89
+ export const forgetMessages = (vm) => text(vm).forgetMessages();
90
+ const prop = (vm, obj, name) => toSigned(vm.getProp(obj, name) ?? 0);
91
+ /** Width used when a script asks for "default" wrapping (SCI32: 3/5 of the screen). */
92
+ const DEFAULT_MAX_WIDTH = (320 * 3) / 5;
93
+ /**
94
+ * Sierra text escapes: `\xx` (two hex digits) is a character code, e.g. `\1d`/`\1e` are the
95
+ * arrow glyphs on dialog scroll buttons.
96
+ */
97
+ export const unescapeText = (s) => s.replace(/\\([0-9a-fA-F]{2})/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16)));
98
+ /** Draws wrapped text into a bitmap, inside `rect`, with alignment. */
99
+ function drawText(bmp, font, str, rect, color, align) {
100
+ const width = rect.right - rect.left;
101
+ const lines = wrapText(font, str, width);
102
+ lines.forEach((line, row) => {
103
+ const w = textWidth(font, line);
104
+ let x = rect.left + (align === TextAlign.Center ? Math.floor((width - w) / 2) : align === TextAlign.Right ? width - w : 0);
105
+ const y0 = rect.top + row * font.height;
106
+ for (const ch of line) {
107
+ const g = font.glyphs[ch.charCodeAt(0)];
108
+ if (!g)
109
+ continue;
110
+ for (let gy = 0; gy < g.height; gy++) {
111
+ const y = y0 + gy;
112
+ if (y < rect.top || y >= rect.bottom || y >= bmp.height)
113
+ continue;
114
+ for (let gx = 0; gx < g.width; gx++) {
115
+ const px = x + gx;
116
+ if (g.pixels[gy * g.width + gx] && px >= 0 && px < bmp.width)
117
+ bmp.pixels[y * bmp.width + px] = color;
118
+ }
119
+ }
120
+ x += g.width;
121
+ }
122
+ });
123
+ }
124
+ function drawBorder(bmp, color) {
125
+ for (let x = 0; x < bmp.width; x++)
126
+ (bmp.pixels[x] = color), (bmp.pixels[(bmp.height - 1) * bmp.width + x] = color);
127
+ for (let y = 0; y < bmp.height; y++)
128
+ (bmp.pixels[y * bmp.width] = color), (bmp.pixels[y * bmp.width + bmp.width - 1] = color);
129
+ }
130
+ /**
131
+ * SCI32 text: scripts measure text (TextSize), render it into an off-screen bitmap
132
+ * (CreateTextBitmap) and show that bitmap as a screen item via the object's `bitmap`.
133
+ */
134
+ export const textKernels = {
135
+ Message: (vm, [func = 0, ...args]) => {
136
+ const t = text(vm);
137
+ switch (func) {
138
+ case 0: // get(module, noun, verb, cond, seq, buffer)
139
+ case 2: { // size(module, noun, verb, cond, seq)
140
+ const [module = 0, noun = 0, verb = 0, cond = 0, seq = 0, buffer = 0] = args;
141
+ if (func === 2) {
142
+ const saved = { cursor: t.cursor, module: t.cursorModule };
143
+ t.cursorModule = module;
144
+ t.cursor = [{ noun, verb, cond, seq }];
145
+ const m = t.next();
146
+ (t.cursor = saved.cursor), (t.cursorModule = saved.module);
147
+ return m ? m.text.length + 1 : 0;
148
+ }
149
+ t.cursorModule = module;
150
+ t.cursor = [{ noun, verb, cond, seq }];
151
+ const m = t.next();
152
+ if (buffer)
153
+ stringHelpers.writeString(vm, buffer, m?.text ?? "");
154
+ return m?.talker ?? 0;
155
+ }
156
+ case 1: { // next(buffer)
157
+ const m = t.next();
158
+ if (args[0])
159
+ stringHelpers.writeString(vm, args[0], m?.text ?? "");
160
+ return m?.talker ?? 0;
161
+ }
162
+ // SCI32 dropped subop 3 and shifted the rest up by one.
163
+ case 4:
164
+ case 5:
165
+ case 6: { // ref cond / verb / noun of (module, noun, verb, cond, seq)
166
+ const [module = 0, noun = 0, verb = 0, cond = 0, seq = 0] = args;
167
+ const rec = t.messageFile(module)?.records.find((r) => r.noun === noun && r.verb === verb && r.cond === cond && r.seq === seq);
168
+ if (!rec || !hasRef(rec))
169
+ return 0xffff;
170
+ return func === 4 ? rec.ref.cond : func === 5 ? rec.ref.verb : rec.ref.noun;
171
+ }
172
+ case 7:
173
+ t.push();
174
+ return 0;
175
+ case 8:
176
+ t.pop();
177
+ return 0;
178
+ case 9: { // last message → buffer: module, noun, verb, cond, seq (used to pick the voice clip)
179
+ const buf = vm.memory.array(args[0] ?? 0);
180
+ if (buf && t.last)
181
+ buf.data.splice(0, 5, t.last.module, t.last.tuple.noun, t.last.tuple.verb, t.last.tuple.cond, t.last.tuple.seq);
182
+ return NULL;
183
+ }
184
+ }
185
+ return 0;
186
+ },
187
+ // TextSize(rectArray, text, font, maxWidth): writes [left, top, right, bottom] (inclusive).
188
+ TextSize: (vm, [rect = 0, str = 0, fontNo = 0, maxWidth = 0]) => {
189
+ const font = text(vm).font(fontNo);
190
+ const s = stringHelpers.str(vm, str);
191
+ let w, h;
192
+ const max = toSigned(maxWidth);
193
+ if (max >= 0) {
194
+ const limit = max || DEFAULT_MAX_WIDTH;
195
+ if (!/[\r\n]/.test(s) && textWidth(font, s) <= limit) {
196
+ // Fits on one line: measured as-is, spaces included (a lone " " has a width).
197
+ w = textWidth(font, s);
198
+ h = font.height;
199
+ }
200
+ else {
201
+ const lines = wrapText(font, s, limit);
202
+ w = Math.min(limit, Math.max(0, ...lines.map((l) => textWidth(font, l))));
203
+ h = lines.length * font.height;
204
+ }
205
+ }
206
+ else {
207
+ w = textWidth(font, s);
208
+ h = font.height;
209
+ }
210
+ const a = stringHelpers.resolve(vm, rect);
211
+ if (a)
212
+ a.data.splice(0, 4, 0, 0, fromInt(w - 1), fromInt(h - 1));
213
+ return 0;
214
+ },
215
+ TextWidth: (vm, [str = 0, fontNo = 0]) => textWidth(text(vm).font(fontNo), stringHelpers.str(vm, str)),
216
+ PointSize: (vm, [fontNo = 0]) => text(vm).font(fontNo).height,
217
+ TextFonts: () => 0,
218
+ TextColors: () => 0,
219
+ // CreateTextBitmap(0, width, height, textObject) → bitmap of that size;
220
+ // CreateTextBitmap(1, textObject) → the text drawn over the object's own view cel.
221
+ CreateTextBitmap: (vm, args) => {
222
+ const [subop = 0] = args;
223
+ const [width = 0, height = 0, obj = 0] = subop === 1 ? [0, 0, args[1] ?? 0] : args.slice(1);
224
+ if (!vm.memory.object(obj))
225
+ return NULL;
226
+ const w = Math.max(1, toSigned(width)), h = Math.max(1, toSigned(height));
227
+ const str = unescapeText(stringHelpers.str(vm, vm.getProp(obj, "text") ?? 0));
228
+ const font = text(vm).font(prop(vm, obj, "font"));
229
+ const fore = prop(vm, obj, "fore") & 0xff;
230
+ const align = prop(vm, obj, "mode");
231
+ const rect = {
232
+ left: prop(vm, obj, "textLeft"),
233
+ top: prop(vm, obj, "textTop"),
234
+ right: prop(vm, obj, "textRight") + 1,
235
+ bottom: prop(vm, obj, "textBottom") + 1,
236
+ };
237
+ if (rect.right <= rect.left)
238
+ (rect.left = 0), (rect.right = w);
239
+ if (rect.bottom <= rect.top)
240
+ (rect.top = 0), (rect.bottom = h);
241
+ let ref;
242
+ if (subop === 0) {
243
+ const back = prop(vm, obj, "back") & 0xff;
244
+ const skip = prop(vm, obj, "skip") & 0xff;
245
+ ref = vm.memory.newBitmap(w, h, back, skip);
246
+ const border = prop(vm, obj, "borderColor");
247
+ if (border >= 0)
248
+ drawBorder(vm.memory.bitmap(ref), border & 0xff);
249
+ }
250
+ else {
251
+ // Text over the object's view cel (e.g. a button face).
252
+ const found = graphics(vm).cel(prop(vm, obj, "view"), prop(vm, obj, "loop"), prop(vm, obj, "cel"));
253
+ const cw = found?.cel.width ?? w, ch = found?.cel.height ?? h;
254
+ ref = vm.memory.newBitmap(cw, ch, found?.cel.skipColor ?? 0, found?.cel.skipColor ?? 0);
255
+ if (found)
256
+ vm.memory.bitmap(ref).pixels.set(found.cel.pixels);
257
+ }
258
+ drawText(vm.memory.bitmap(ref), font, str, rect, fore, align);
259
+ return ref;
260
+ },
261
+ /**
262
+ * EditText(control): the kernel's modal line editor (save descriptions, names, numbers). It
263
+ * shows the control as its own screen item, takes keys until something ends the editing,
264
+ * writes the text back, and returns 1 if it changed. Runs as a native generator that yields
265
+ * a frame per iteration so the browser keeps drawing and delivering input.
266
+ *
267
+ * Like SSCI (ScummVM's kernelEditText: "the last event needs to be allowed to dispatch a
268
+ * second time"), it only peeks at the event that ends editing (Enter, Escape, Tab,
269
+ * Shift-Tab, Up, Down, or a click outside the field) and leaves it queued, so the dialog
270
+ * handles it too: one Enter both finishes the text and confirms.
271
+ */
272
+ *EditText(vm, [obj = 0]) {
273
+ if (!vm.memory.object(obj))
274
+ return 0;
275
+ const g = graphics(vm);
276
+ const inp = input(vm);
277
+ const textRef = vm.getProp(obj, "text") ?? 0;
278
+ const original = stringHelpers.str(vm, textRef);
279
+ let text = original;
280
+ const maxLength = Math.max(1, prop(vm, obj, "width") || 255);
281
+ const font = text_(vm).font(prop(vm, obj, "font"));
282
+ const fore = prop(vm, obj, "fore") & 0xff;
283
+ const back = prop(vm, obj, "back") & 0xff;
284
+ const skip = prop(vm, obj, "skip") & 0xff;
285
+ const border = prop(vm, obj, "borderColor");
286
+ const width = Math.max(8, prop(vm, obj, "nsRight") - prop(vm, obj, "nsLeft") + 1);
287
+ const height = Math.max(font.height + 2, prop(vm, obj, "nsBottom") - prop(vm, obj, "nsTop") + 1);
288
+ const rect = { left: prop(vm, obj, "textLeft"), top: prop(vm, obj, "textTop"), right: prop(vm, obj, "textRight") + 1, bottom: prop(vm, obj, "textBottom") + 1 };
289
+ const savedBitmap = vm.getProp(obj, "bitmap") ?? 0;
290
+ // The field on screen, for telling clicks inside it from clicks elsewhere.
291
+ const plane = vm.getProp(obj, "plane") ?? 0;
292
+ const originX = vm.memory.object(plane) ? prop(vm, plane, "inLeft") : 0, originY = vm.memory.object(plane) ? prop(vm, plane, "inTop") : 0;
293
+ const field = { left: originX + prop(vm, obj, "x"), top: originY + prop(vm, obj, "y") };
294
+ const inField = (x, y) => x >= field.left && x < field.left + width && y >= field.top && y < field.top + height;
295
+ const ENDS = new Set([13, 27, 9, SciKey.ShiftTab, SciKey.Up, SciKey.Down]);
296
+ let bitmap = 0;
297
+ g.items.add(obj);
298
+ let done = false;
299
+ for (let frame = 0; !done; frame++) {
300
+ if (bitmap)
301
+ vm.memory.free(segmentOf(bitmap));
302
+ bitmap = vm.memory.newBitmap(width, height, back, skip);
303
+ const bmp = vm.memory.bitmap(bitmap);
304
+ if (border >= 0)
305
+ drawBorder(bmp, border & 0xff);
306
+ const cursor = (frame >> 4) & 1 ? "" : "_"; // blink
307
+ drawText(bmp, font, text + cursor, rect, fore, TextAlign.Left);
308
+ vm.setProp(obj, "bitmap", bitmap);
309
+ graphicsKernels.FrameOut(vm, []);
310
+ yield { frame: true };
311
+ while (inp.queue.length && !done) {
312
+ const e = inp.queue[0];
313
+ // Peek: an event that ends editing stays queued for the dialog.
314
+ if ((e.type === EventType.KeyDown && ENDS.has(e.message)) || (e.type === EventType.MouseDown && !inField(e.x, e.y))) {
315
+ done = true;
316
+ break;
317
+ }
318
+ inp.queue.shift();
319
+ if (e.type !== EventType.KeyDown)
320
+ continue;
321
+ if (e.message === 8)
322
+ text = text.slice(0, -1);
323
+ else if (e.message >= 32 && e.message < 256 && text.length < maxLength)
324
+ text += String.fromCharCode(e.message);
325
+ }
326
+ }
327
+ stringHelpers.writeString(vm, textRef, text);
328
+ vm.memory.free(segmentOf(bitmap));
329
+ vm.setProp(obj, "bitmap", savedBitmap);
330
+ g.items.delete(obj);
331
+ return text !== original ? 1 : 0;
332
+ },
333
+ Bitmap: (vm, [subop = 0, ref = 0]) => {
334
+ if (subop === 1 && vm.memory.bitmap(ref))
335
+ vm.memory.free(segmentOf(ref));
336
+ return 0;
337
+ },
338
+ };
@@ -0,0 +1,328 @@
1
+ import { ResourceType } from "../../resource/types.ts";
2
+ import { parseFont, textWidth, wrapText, type Font } from "../../text/font.ts";
3
+ import { hasRef, parseMessages, type MessageFile, type MessageTuple } from "../../text/message.ts";
4
+ import type { SciBitmap } from "../memory.ts";
5
+ import type { FrameRequest, KernelFn, SendRequest, Vm } from "../vm.ts";
6
+ import { NULL, fromInt, segmentOf, toSigned, type Value } from "../value.ts";
7
+ import { stringHelpers } from "./arrays.ts";
8
+ import { graphics, graphicsKernels } from "./graphics.ts";
9
+ import { EventType, SciKey, input } from "./system.ts";
10
+
11
+ const TextAlign = { Left: 0, Center: 1, Right: 2 } as const;
12
+
13
+ /** Per-VM text state: font and message caches plus the message cursor. */
14
+ class TextState {
15
+ private readonly fonts = new Map<number, Font>();
16
+ private readonly messages = new Map<number, MessageFile | null>();
17
+ /** Message cursor stack: the current tuple, plus one entry per reference being followed. */
18
+ cursor: MessageTuple[] = [];
19
+ cursorModule = 0;
20
+ private readonly saved: { module: number; stack: MessageTuple[] }[] = [];
21
+ last: { module: number; tuple: MessageTuple } | undefined;
22
+
23
+ constructor(private readonly vm: Vm) {}
24
+
25
+ /** Drops parsed message files (their resources were replaced). */
26
+ forgetMessages(): void {
27
+ this.messages.clear();
28
+ }
29
+
30
+ font(n: number): Font {
31
+ let f = this.fonts.get(n);
32
+ if (!f) {
33
+ const id = { type: ResourceType.Font, number: n };
34
+ f = parseFont(this.vm.resources.loadSync(this.vm.resources.has(id) ? id : { type: ResourceType.Font, number: 0 }).data);
35
+ this.fonts.set(n, f);
36
+ }
37
+ return f;
38
+ }
39
+
40
+ messageFile(module: number): MessageFile | undefined {
41
+ if (!this.messages.has(module)) {
42
+ const id = { type: ResourceType.Message, number: module };
43
+ this.messages.set(module, this.vm.resources.has(id) ? parseMessages(this.vm.resources.loadSync(id).data) : null);
44
+ }
45
+ return this.messages.get(module) ?? undefined;
46
+ }
47
+
48
+ /**
49
+ * Returns the next message at the cursor, following references (a record with a ref
50
+ * pushes the referenced tuple; running off the end of a referenced sequence pops back).
51
+ */
52
+ next(): { talker: number; text: string } | undefined {
53
+ const file = this.messageFile(this.cursorModule);
54
+ for (let guard = 0; guard < 32 && file; guard++) {
55
+ const t = this.cursor[this.cursor.length - 1];
56
+ if (!t) return undefined;
57
+ const rec = file.records.find((r) => r.noun === t.noun && r.verb === t.verb && r.cond === t.cond && r.seq === t.seq);
58
+ if (!rec) {
59
+ if (this.cursor.length > 1) {
60
+ this.cursor.pop();
61
+ continue;
62
+ }
63
+ return undefined;
64
+ }
65
+ if (hasRef(rec)) {
66
+ t.seq++;
67
+ this.cursor.push({ ...rec.ref });
68
+ continue;
69
+ }
70
+ this.last = { module: this.cursorModule, tuple: { ...t } };
71
+ t.seq++;
72
+ return { talker: rec.talker, text: rec.text };
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ push(): void {
78
+ this.saved.push({ module: this.cursorModule, stack: this.cursor.map((t) => ({ ...t })) });
79
+ }
80
+
81
+ pop(): void {
82
+ const s = this.saved.pop();
83
+ if (s) (this.cursorModule = s.module), (this.cursor = s.stack);
84
+ }
85
+ }
86
+
87
+ const stateOf = new WeakMap<Vm, TextState>();
88
+ const text_ = (vm: Vm): TextState => text(vm);
89
+ const text = (vm: Vm): TextState => {
90
+ let t = stateOf.get(vm);
91
+ if (!t) stateOf.set(vm, (t = new TextState(vm)));
92
+ return t;
93
+ };
94
+
95
+ /** For replaceResources: forget parsed message files. */
96
+ export const forgetMessages = (vm: Vm): void => text(vm).forgetMessages();
97
+
98
+ const prop = (vm: Vm, obj: Value, name: string) => toSigned(vm.getProp(obj, name) ?? 0);
99
+
100
+ /** Width used when a script asks for "default" wrapping (SCI32: 3/5 of the screen). */
101
+ const DEFAULT_MAX_WIDTH = (320 * 3) / 5;
102
+
103
+ /**
104
+ * Sierra text escapes: `\xx` (two hex digits) is a character code, e.g. `\1d`/`\1e` are the
105
+ * arrow glyphs on dialog scroll buttons.
106
+ */
107
+ export const unescapeText = (s: string) => s.replace(/\\([0-9a-fA-F]{2})/g, (_m, hex: string) => String.fromCharCode(parseInt(hex, 16)));
108
+
109
+ /** Draws wrapped text into a bitmap, inside `rect`, with alignment. */
110
+ function drawText(bmp: SciBitmap, font: Font, str: string, rect: { left: number; top: number; right: number; bottom: number }, color: number, align: number) {
111
+ const width = rect.right - rect.left;
112
+ const lines = wrapText(font, str, width);
113
+ lines.forEach((line, row) => {
114
+ const w = textWidth(font, line);
115
+ let x = rect.left + (align === TextAlign.Center ? Math.floor((width - w) / 2) : align === TextAlign.Right ? width - w : 0);
116
+ const y0 = rect.top + row * font.height;
117
+ for (const ch of line) {
118
+ const g = font.glyphs[ch.charCodeAt(0)];
119
+ if (!g) continue;
120
+ for (let gy = 0; gy < g.height; gy++) {
121
+ const y = y0 + gy;
122
+ if (y < rect.top || y >= rect.bottom || y >= bmp.height) continue;
123
+ for (let gx = 0; gx < g.width; gx++) {
124
+ const px = x + gx;
125
+ if (g.pixels[gy * g.width + gx] && px >= 0 && px < bmp.width) bmp.pixels[y * bmp.width + px] = color;
126
+ }
127
+ }
128
+ x += g.width;
129
+ }
130
+ });
131
+ }
132
+
133
+ function drawBorder(bmp: SciBitmap, color: number) {
134
+ for (let x = 0; x < bmp.width; x++) (bmp.pixels[x] = color), (bmp.pixels[(bmp.height - 1) * bmp.width + x] = color);
135
+ for (let y = 0; y < bmp.height; y++) (bmp.pixels[y * bmp.width] = color), (bmp.pixels[y * bmp.width + bmp.width - 1] = color);
136
+ }
137
+
138
+ /**
139
+ * SCI32 text: scripts measure text (TextSize), render it into an off-screen bitmap
140
+ * (CreateTextBitmap) and show that bitmap as a screen item via the object's `bitmap`.
141
+ */
142
+ export const textKernels: Record<string, KernelFn> = {
143
+ Message: (vm, [func = 0, ...args]) => {
144
+ const t = text(vm);
145
+ switch (func) {
146
+ case 0: // get(module, noun, verb, cond, seq, buffer)
147
+ case 2: { // size(module, noun, verb, cond, seq)
148
+ const [module = 0, noun = 0, verb = 0, cond = 0, seq = 0, buffer = 0] = args;
149
+ if (func === 2) {
150
+ const saved = { cursor: t.cursor, module: t.cursorModule };
151
+ t.cursorModule = module;
152
+ t.cursor = [{ noun, verb, cond, seq }];
153
+ const m = t.next();
154
+ (t.cursor = saved.cursor), (t.cursorModule = saved.module);
155
+ return m ? m.text.length + 1 : 0;
156
+ }
157
+ t.cursorModule = module;
158
+ t.cursor = [{ noun, verb, cond, seq }];
159
+ const m = t.next();
160
+ if (buffer) stringHelpers.writeString(vm, buffer, m?.text ?? "");
161
+ return m?.talker ?? 0;
162
+ }
163
+ case 1: { // next(buffer)
164
+ const m = t.next();
165
+ if (args[0]) stringHelpers.writeString(vm, args[0], m?.text ?? "");
166
+ return m?.talker ?? 0;
167
+ }
168
+ // SCI32 dropped subop 3 and shifted the rest up by one.
169
+ case 4: case 5: case 6: { // ref cond / verb / noun of (module, noun, verb, cond, seq)
170
+ const [module = 0, noun = 0, verb = 0, cond = 0, seq = 0] = args;
171
+ const rec = t.messageFile(module)?.records.find((r) => r.noun === noun && r.verb === verb && r.cond === cond && r.seq === seq);
172
+ if (!rec || !hasRef(rec)) return 0xffff;
173
+ return func === 4 ? rec.ref.cond : func === 5 ? rec.ref.verb : rec.ref.noun;
174
+ }
175
+ case 7: t.push(); return 0;
176
+ case 8: t.pop(); return 0;
177
+ case 9: { // last message → buffer: module, noun, verb, cond, seq (used to pick the voice clip)
178
+ const buf = vm.memory.array(args[0] ?? 0);
179
+ if (buf && t.last) buf.data.splice(0, 5, t.last.module, t.last.tuple.noun, t.last.tuple.verb, t.last.tuple.cond, t.last.tuple.seq);
180
+ return NULL;
181
+ }
182
+ }
183
+ return 0;
184
+ },
185
+
186
+ // TextSize(rectArray, text, font, maxWidth): writes [left, top, right, bottom] (inclusive).
187
+ TextSize: (vm, [rect = 0, str = 0, fontNo = 0, maxWidth = 0]) => {
188
+ const font = text(vm).font(fontNo);
189
+ const s = stringHelpers.str(vm, str);
190
+ let w: number, h: number;
191
+ const max = toSigned(maxWidth);
192
+ if (max >= 0) {
193
+ const limit = max || DEFAULT_MAX_WIDTH;
194
+ if (!/[\r\n]/.test(s) && textWidth(font, s) <= limit) {
195
+ // Fits on one line: measured as-is, spaces included (a lone " " has a width).
196
+ w = textWidth(font, s);
197
+ h = font.height;
198
+ } else {
199
+ const lines = wrapText(font, s, limit);
200
+ w = Math.min(limit, Math.max(0, ...lines.map((l) => textWidth(font, l))));
201
+ h = lines.length * font.height;
202
+ }
203
+ } else {
204
+ w = textWidth(font, s);
205
+ h = font.height;
206
+ }
207
+ const a = stringHelpers.resolve(vm, rect);
208
+ if (a) a.data.splice(0, 4, 0, 0, fromInt(w - 1), fromInt(h - 1));
209
+ return 0;
210
+ },
211
+ TextWidth: (vm, [str = 0, fontNo = 0]) => textWidth(text(vm).font(fontNo), stringHelpers.str(vm, str)),
212
+ PointSize: (vm, [fontNo = 0]) => text(vm).font(fontNo).height,
213
+ TextFonts: () => 0,
214
+ TextColors: () => 0,
215
+
216
+ // CreateTextBitmap(0, width, height, textObject) → bitmap of that size;
217
+ // CreateTextBitmap(1, textObject) → the text drawn over the object's own view cel.
218
+ CreateTextBitmap: (vm, args) => {
219
+ const [subop = 0] = args;
220
+ const [width = 0, height = 0, obj = 0] = subop === 1 ? [0, 0, args[1] ?? 0] : args.slice(1);
221
+ if (!vm.memory.object(obj)) return NULL;
222
+ const w = Math.max(1, toSigned(width)), h = Math.max(1, toSigned(height));
223
+ const str = unescapeText(stringHelpers.str(vm, vm.getProp(obj, "text") ?? 0));
224
+ const font = text(vm).font(prop(vm, obj, "font"));
225
+ const fore = prop(vm, obj, "fore") & 0xff;
226
+ const align = prop(vm, obj, "mode");
227
+ const rect = {
228
+ left: prop(vm, obj, "textLeft"),
229
+ top: prop(vm, obj, "textTop"),
230
+ right: prop(vm, obj, "textRight") + 1,
231
+ bottom: prop(vm, obj, "textBottom") + 1,
232
+ };
233
+ if (rect.right <= rect.left) (rect.left = 0), (rect.right = w);
234
+ if (rect.bottom <= rect.top) (rect.top = 0), (rect.bottom = h);
235
+
236
+ let ref: Value;
237
+ if (subop === 0) {
238
+ const back = prop(vm, obj, "back") & 0xff;
239
+ const skip = prop(vm, obj, "skip") & 0xff;
240
+ ref = vm.memory.newBitmap(w, h, back, skip);
241
+ const border = prop(vm, obj, "borderColor");
242
+ if (border >= 0) drawBorder(vm.memory.bitmap(ref)!, border & 0xff);
243
+ } else {
244
+ // Text over the object's view cel (e.g. a button face).
245
+ const found = graphics(vm).cel(prop(vm, obj, "view"), prop(vm, obj, "loop"), prop(vm, obj, "cel"));
246
+ const cw = found?.cel.width ?? w, ch = found?.cel.height ?? h;
247
+ ref = vm.memory.newBitmap(cw, ch, found?.cel.skipColor ?? 0, found?.cel.skipColor ?? 0);
248
+ if (found) vm.memory.bitmap(ref)!.pixels.set(found.cel.pixels);
249
+ }
250
+ drawText(vm.memory.bitmap(ref)!, font, str, rect, fore, align);
251
+ return ref;
252
+ },
253
+
254
+ /**
255
+ * EditText(control): the kernel's modal line editor (save descriptions, names, numbers). It
256
+ * shows the control as its own screen item, takes keys until something ends the editing,
257
+ * writes the text back, and returns 1 if it changed. Runs as a native generator that yields
258
+ * a frame per iteration so the browser keeps drawing and delivering input.
259
+ *
260
+ * Like SSCI (ScummVM's kernelEditText: "the last event needs to be allowed to dispatch a
261
+ * second time"), it only peeks at the event that ends editing (Enter, Escape, Tab,
262
+ * Shift-Tab, Up, Down, or a click outside the field) and leaves it queued, so the dialog
263
+ * handles it too: one Enter both finishes the text and confirms.
264
+ */
265
+ *EditText(vm: Vm, [obj = 0]: Value[]): Generator<SendRequest | FrameRequest, Value, Value> {
266
+ if (!vm.memory.object(obj)) return 0;
267
+ const g = graphics(vm);
268
+ const inp = input(vm);
269
+ const textRef = vm.getProp(obj, "text") ?? 0;
270
+ const original = stringHelpers.str(vm, textRef);
271
+ let text = original;
272
+ const maxLength = Math.max(1, prop(vm, obj, "width") || 255);
273
+ const font = text_(vm).font(prop(vm, obj, "font"));
274
+ const fore = prop(vm, obj, "fore") & 0xff;
275
+ const back = prop(vm, obj, "back") & 0xff;
276
+ const skip = prop(vm, obj, "skip") & 0xff;
277
+ const border = prop(vm, obj, "borderColor");
278
+ const width = Math.max(8, prop(vm, obj, "nsRight") - prop(vm, obj, "nsLeft") + 1);
279
+ const height = Math.max(font.height + 2, prop(vm, obj, "nsBottom") - prop(vm, obj, "nsTop") + 1);
280
+ const rect = { left: prop(vm, obj, "textLeft"), top: prop(vm, obj, "textTop"), right: prop(vm, obj, "textRight") + 1, bottom: prop(vm, obj, "textBottom") + 1 };
281
+ const savedBitmap = vm.getProp(obj, "bitmap") ?? 0;
282
+ // The field on screen, for telling clicks inside it from clicks elsewhere.
283
+ const plane = vm.getProp(obj, "plane") ?? 0;
284
+ const originX = vm.memory.object(plane) ? prop(vm, plane, "inLeft") : 0, originY = vm.memory.object(plane) ? prop(vm, plane, "inTop") : 0;
285
+ const field = { left: originX + prop(vm, obj, "x"), top: originY + prop(vm, obj, "y") };
286
+ const inField = (x: number, y: number) => x >= field.left && x < field.left + width && y >= field.top && y < field.top + height;
287
+ const ENDS = new Set<number>([13, 27, 9, SciKey.ShiftTab, SciKey.Up, SciKey.Down]);
288
+ let bitmap: Value = 0;
289
+ g.items.add(obj);
290
+
291
+ let done = false;
292
+ for (let frame = 0; !done; frame++) {
293
+ if (bitmap) vm.memory.free(segmentOf(bitmap));
294
+ bitmap = vm.memory.newBitmap(width, height, back, skip);
295
+ const bmp = vm.memory.bitmap(bitmap)!;
296
+ if (border >= 0) drawBorder(bmp, border & 0xff);
297
+ const cursor = (frame >> 4) & 1 ? "" : "_"; // blink
298
+ drawText(bmp, font, text + cursor, rect, fore, TextAlign.Left);
299
+ vm.setProp(obj, "bitmap", bitmap);
300
+ graphicsKernels.FrameOut!(vm, []);
301
+ yield { frame: true };
302
+
303
+ while (inp.queue.length && !done) {
304
+ const e = inp.queue[0]!;
305
+ // Peek: an event that ends editing stays queued for the dialog.
306
+ if ((e.type === EventType.KeyDown && ENDS.has(e.message)) || (e.type === EventType.MouseDown && !inField(e.x, e.y))) {
307
+ done = true;
308
+ break;
309
+ }
310
+ inp.queue.shift();
311
+ if (e.type !== EventType.KeyDown) continue;
312
+ if (e.message === 8) text = text.slice(0, -1);
313
+ else if (e.message >= 32 && e.message < 256 && text.length < maxLength) text += String.fromCharCode(e.message);
314
+ }
315
+ }
316
+
317
+ stringHelpers.writeString(vm, textRef, text);
318
+ vm.memory.free(segmentOf(bitmap));
319
+ vm.setProp(obj, "bitmap", savedBitmap);
320
+ g.items.delete(obj);
321
+ return text !== original ? 1 : 0;
322
+ },
323
+
324
+ Bitmap: (vm, [subop = 0, ref = 0]) => {
325
+ if (subop === 1 && vm.memory.bitmap(ref)) vm.memory.free(segmentOf(ref));
326
+ return 0;
327
+ },
328
+ };