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,321 @@
1
+ import {
2
+ EventType, ResourceManager, ResourceType, SciKey, SoundDevice, Vm, allKernels, audio, graphics, hostFiles, input, parseAdLibBank, readingTime, restoreGame, saves, snapshot,
3
+ type Frame, type VmSnapshot,
4
+ } from "../../../packages/sci/src/index.ts";
5
+ import { AdLibOutput } from "./adlib.ts";
6
+ import { WebAudioOutput } from "./audio.ts";
7
+ import { BASE, httpFiles } from "./files.ts";
8
+ import { SoundFontMidi } from "./music.ts";
9
+ import { APP_ID } from "./app.ts";
10
+ import { browserSaveStore } from "./saves.ts";
11
+
12
+ export type MusicMode = "gm" | "adlib";
13
+
14
+ export interface SessionOptions {
15
+ canvas: HTMLCanvasElement;
16
+ /** Reuse an open resource manager (it gets preloaded). */
17
+ rm?: ResourceManager;
18
+ /** Active mods: saves are kept apart per set of mods. */
19
+ mods?: string[];
20
+ /** Keep saves under this name instead (the editor's, apart from playing). */
21
+ saveNamespace?: string;
22
+ /**
23
+ * Sierra's debug room, which the game loads when a file "18.scr" exists (their machines had one).
24
+ * Alt-T teleports, Alt-G sets flags, Alt-I gives items, Alt-H sets the hour... (Alt-? lists).
25
+ */
26
+ debug?: boolean;
27
+ /** Start with audio suspended; `setMuted(false)` turns it on. */
28
+ muted?: boolean;
29
+ /**
30
+ * Keep running when the page isn't painted (a hidden tab or pane), where
31
+ * requestAnimationFrame stops: a timer takes over. For automated testing.
32
+ */
33
+ runHidden?: boolean;
34
+ /**
35
+ * Called for canvas mouse-downs before the game sees them; return true to swallow the
36
+ * click (the explorer's inspect mode). Coordinates are game screen pixels.
37
+ */
38
+ interceptClick?: (x: number, y: number, e: MouseEvent) => boolean;
39
+ }
40
+
41
+ /**
42
+ * One running game in a canvas: VM, audio, input and the frame loop. The player page and
43
+ * the explorer's live view both run the game through this.
44
+ */
45
+ export class GameSession {
46
+ paused = false;
47
+ /** Game frames run so far. */
48
+ frames = 0;
49
+ fps = 0;
50
+ error: { message: string; backtrace: string[] } | undefined;
51
+ /** After each drawn browser frame. */
52
+ readonly onDraw: (() => void)[] = [];
53
+ private latest: Frame | undefined;
54
+ private readonly ctx: CanvasRenderingContext2D;
55
+ private readonly image: ImageData;
56
+ private musicModeValue: MusicMode = "gm";
57
+ private stepsOwed = 0;
58
+
59
+ private constructor(
60
+ readonly rm: ResourceManager,
61
+ readonly vm: Vm,
62
+ readonly canvas: HTMLCanvasElement,
63
+ readonly sound: WebAudioOutput,
64
+ readonly music: SoundFontMidi,
65
+ /** Only for games with an AdLib instrument bank (patch 3). */
66
+ private readonly adlib: AdLibOutput | undefined,
67
+ private readonly options: SessionOptions,
68
+ ) {
69
+ this.ctx = canvas.getContext("2d")!;
70
+ this.image = this.ctx.createImageData(320, 200);
71
+ }
72
+
73
+ static async create(options: SessionOptions): Promise<GameSession> {
74
+ const rm = options.rm ?? (await ResourceManager.open(httpFiles));
75
+ await rm.preload();
76
+ const vm = new Vm(rm);
77
+ vm.registerKernels(allKernels);
78
+ // Saved games persist in this browser (IndexedDB) and appear in the game's Restore screen.
79
+ saves(vm).store = await browserSaveStore(options.saveNamespace ?? [...(options.mods ?? [])].sort().join("+"));
80
+ if (options.debug) hostFiles(vm).add("18.scr");
81
+
82
+ const sound = new WebAudioOutput(httpFiles, (name) => rm.file(name));
83
+ audio(vm).output = sound;
84
+ // Mods add dialogue without recordings: give those lines time to be read.
85
+ if (options.mods?.length) audio(vm).missingSpeechTicks = readingTime(vm);
86
+ // Music: General MIDI through a SoundFont (loads in the background), or the AdLib track
87
+ // through Sierra's driver and an OPL2 emulator: what most players heard in 1994.
88
+ const music = new SoundFontMidi(sound.ctx);
89
+ music.load(`${BASE}soundfonts/GeneralUser-GS.sf2`).catch((e) => console.warn("SoundFont unavailable:", e));
90
+ const bank = { type: ResourceType.Patch, number: 3 };
91
+ const adlib = rm.has(bank) ? await AdLibOutput.create(sound.ctx, parseAdLibBank(rm.loadSync(bank).data)) : undefined;
92
+
93
+ const session = new GameSession(rm, vm, options.canvas, sound, music, adlib, options);
94
+ graphics(vm).onFrame = (f) => (session.latest = f);
95
+ session.setMusicMode(readMusicMode());
96
+ session.setMuted(!!options.muted);
97
+ session.attachInput();
98
+ for (const type of ["pointerdown", "keydown"]) window.addEventListener(type, () => session.muted || sound.unlock());
99
+ return session;
100
+ }
101
+
102
+ get musicMode(): MusicMode {
103
+ return this.musicModeValue;
104
+ }
105
+
106
+ /** Whether the game can play its AdLib tracks (it has an instrument bank). */
107
+ get hasAdLib(): boolean {
108
+ return !!this.adlib;
109
+ }
110
+
111
+ setMusicMode(mode: MusicMode): void {
112
+ if (!this.adlib) mode = "gm";
113
+ this.musicModeValue = mode;
114
+ this.adlib?.reset();
115
+ if (mode === "adlib" && this.adlib) audio(this.vm).setMusicDevice(SoundDevice.AdLib, this.adlib);
116
+ else audio(this.vm).setMusicDevice(SoundDevice.GeneralMidi, this.music);
117
+ try {
118
+ localStorage.setItem(`${APP_ID}.music`, mode);
119
+ } catch {
120
+ /* private mode: just don't remember */
121
+ }
122
+ }
123
+
124
+ muted = false;
125
+ /** Browsers keep audio suspended until a user gesture: unmuted, the next gesture unlocks it. */
126
+ setMuted(muted: boolean): void {
127
+ this.muted = muted;
128
+ if (muted) void this.sound.ctx.suspend();
129
+ else this.sound.unlock();
130
+ }
131
+
132
+ /** True while `start({ fastForward })` is still running ahead. */
133
+ fastForwarding = false;
134
+
135
+ /**
136
+ * Starts the game and the frame loop. With `fastForward`, the game first runs as fast as
137
+ * it can on a frame clock (60 frames = 1 second of game time), from the very first frame,
138
+ * with `script[n]` called before frame n (scripted input) until frame `until`; audio is
139
+ * silent meanwhile. Then real time takes over, continuing the clock where it was.
140
+ */
141
+ start(options: { fastForward?: { script: Record<number, () => void>; until: number; done?: () => void } } = {}): void {
142
+ this.vm.start(this.vm.exportAddress(0, 0), "play");
143
+ const ff = options.fastForward;
144
+ const g = graphics(this.vm);
145
+ if (ff) {
146
+ this.fastForwarding = true;
147
+ this.vm.clock = () => (g.frames * 1000) / 60;
148
+ void this.sound.ctx.suspend();
149
+ }
150
+ const finishFastForward = () => {
151
+ this.fastForwarding = false;
152
+ const base = (g.frames * 1000) / 60, t0 = performance.now();
153
+ this.vm.clock = () => base + (performance.now() - t0);
154
+ if (!this.muted) this.sound.unlock();
155
+ ff?.done?.();
156
+ };
157
+ let prev = performance.now();
158
+ let lastFps = prev;
159
+ let counted = 0;
160
+ // SCI32 games expect ~60 FrameOuts per second. If the browser ticks slower (a hidden tab,
161
+ // a slow machine), run several game frames per tick to catch up, up to a cap.
162
+ const FRAME_MS = 1000 / 60;
163
+ let owed = 0;
164
+ const tick = (now: number) => {
165
+ try {
166
+ if (this.fastForwarding && ff) {
167
+ // As many frames as fit in ~30 ms, then draw so the page stays responsive.
168
+ const stop = performance.now() + 30;
169
+ while (g.frames < ff.until && this.vm.running && performance.now() < stop) {
170
+ ff.script[g.frames]?.();
171
+ this.vm.run();
172
+ this.frames++;
173
+ }
174
+ if (g.frames >= ff.until) finishFastForward();
175
+ prev = now;
176
+ }
177
+ owed = this.paused || this.fastForwarding ? 0 : Math.min(owed + (now - prev) / FRAME_MS, 30);
178
+ prev = now;
179
+ let steps = this.paused ? this.stepsOwed : 0;
180
+ this.stepsOwed = 0;
181
+ while ((owed >= 1 || steps > 0) && this.vm.running) {
182
+ this.vm.run();
183
+ this.frames++;
184
+ counted++;
185
+ if (steps > 0) steps--;
186
+ else owed--;
187
+ }
188
+ if (this.latest) this.draw(this.latest);
189
+ for (const hook of this.onDraw) hook();
190
+ if (now - lastFps > 1000) {
191
+ this.fps = counted;
192
+ counted = 0;
193
+ lastFps = now;
194
+ }
195
+ if (this.vm.running) schedule();
196
+ } catch (e) {
197
+ this.error = { message: (e as Error).message, backtrace: this.vm.backtrace() };
198
+ console.error(e);
199
+ for (const hook of this.onDraw) hook();
200
+ }
201
+ };
202
+ // One tick per animation frame; with runHidden, a timer fires it if frames stop coming.
203
+ let pending = false;
204
+ const fire = (now: number) => {
205
+ if (!pending) return;
206
+ pending = false;
207
+ tick(now);
208
+ };
209
+ const schedule = () => {
210
+ pending = true;
211
+ requestAnimationFrame(fire);
212
+ if (this.options.runHidden) setTimeout(() => fire(performance.now()), 50);
213
+ };
214
+ schedule();
215
+ }
216
+
217
+ /** While paused: run `n` game frames on the next tick. */
218
+ step(n = 1): void {
219
+ this.stepsOwed += n;
220
+ }
221
+
222
+ snapshot(): VmSnapshot {
223
+ return snapshot(this.vm);
224
+ }
225
+
226
+ /** Like the game's own restore: replace memory, then restart execution with `replay`. */
227
+ /** Loads a snapshot as the game's own Restore does (see `restoreGame`). */
228
+ restore(snap: VmSnapshot): void {
229
+ restoreGame(this.vm, snap);
230
+ }
231
+
232
+ private draw(f: Frame): void {
233
+ const { pixels, palette, rgb } = f;
234
+ const data = this.image.data;
235
+ for (let i = 0; i < pixels.length; i++) {
236
+ // Mid-transition frames carry RGB directly; otherwise look colours up in the palette.
237
+ const src = rgb ?? palette.rgb;
238
+ const c = rgb ? i * 3 : pixels[i]! * 3;
239
+ data[i * 4] = src[c]!;
240
+ data[i * 4 + 1] = src[c + 1]!;
241
+ data[i * 4 + 2] = src[c + 2]!;
242
+ data[i * 4 + 3] = 255;
243
+ }
244
+ this.ctx.putImageData(this.image, 0, 0);
245
+ }
246
+
247
+ // --- Input: map page coordinates to the 320×200 game screen ---------------------
248
+ private attachInput(): void {
249
+ const { canvas } = this;
250
+ const inp = input(this.vm);
251
+ const toGame = (e: MouseEvent) => {
252
+ const r = canvas.getBoundingClientRect();
253
+ return {
254
+ x: Math.max(0, Math.min(319, Math.floor(((e.clientX - r.left) / r.width) * 320))),
255
+ y: Math.max(0, Math.min(199, Math.floor(((e.clientY - r.top) / r.height) * 200))),
256
+ };
257
+ };
258
+ // While a fast-forward plays its script, the player's mouse and keys stay out of it: a
259
+ // queued click reads the mouse position when the game gets to it, so a real mouse move in
260
+ // between would send the scripted click wherever the pointer is.
261
+ const track = (e: MouseEvent) => {
262
+ if (!this.fastForwarding) ({ x: inp.x, y: inp.y } = toGame(e));
263
+ };
264
+ let swallowed = false;
265
+ canvas.addEventListener("mousemove", track);
266
+ canvas.addEventListener("contextmenu", (e) => e.preventDefault());
267
+ canvas.addEventListener("mousedown", (e) => {
268
+ if (this.fastForwarding) return;
269
+ const { x, y } = toGame(e);
270
+ swallowed = !!this.options.interceptClick?.(x, y, e);
271
+ if (swallowed) return;
272
+ track(e);
273
+ // SCI reports the right button as a click with shift held.
274
+ inp.push({ type: EventType.MouseDown, message: 0, modifiers: e.button === 2 ? 3 : 0 });
275
+ });
276
+ canvas.addEventListener("mouseup", (e) => {
277
+ if (swallowed || this.fastForwarding) return;
278
+ track(e);
279
+ inp.push({ type: EventType.MouseUp, message: 0 });
280
+ });
281
+ window.addEventListener("keydown", (e) => {
282
+ if (e.metaKey || !canvas.isConnected || this.fastForwarding) return; // leave browser shortcuts alone
283
+ // Typing into the page's own inputs (search boxes, the room editor) isn't for the game.
284
+ const target = e.target as HTMLElement | null;
285
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable)) return;
286
+ // DOS reports Alt+letter as the key's scan code in the high byte (Alt-T = 0x1400); use
287
+ // the physical key, since on a Mac Alt changes the character (Alt-T types "†").
288
+ const scan = e.altKey ? ALT_SCAN_CODES[e.code] : undefined;
289
+ const code = scan ? scan << 8 : e.key === "Tab" && e.shiftKey ? SciKey.ShiftTab : KEY_CODES[e.key] ?? (e.key.length === 1 ? e.key.charCodeAt(0) : 0);
290
+ if (!code) return;
291
+ e.preventDefault();
292
+ inp.push({ type: EventType.KeyDown, message: code, modifiers: modifiers(e) });
293
+ });
294
+ }
295
+ }
296
+
297
+ const KEY_CODES: Record<string, number> = {
298
+ Enter: 13, Escape: 27, Backspace: 8, Tab: 9, " ": 32,
299
+ ArrowUp: SciKey.Up, ArrowDown: SciKey.Down, ArrowLeft: SciKey.Left, ArrowRight: SciKey.Right,
300
+ Home: SciKey.Home, End: SciKey.End, PageUp: SciKey.PageUp, PageDown: SciKey.PageDown,
301
+ Insert: SciKey.Insert, Delete: SciKey.Delete, Clear: SciKey.Center,
302
+ F1: SciKey.F1, F2: SciKey.F2, F3: SciKey.F3, F4: SciKey.F4, F5: SciKey.F5,
303
+ F6: SciKey.F6, F7: SciKey.F7, F8: SciKey.F8, F9: SciKey.F9, F10: SciKey.F10,
304
+ };
305
+ /** PC scan codes for the letter keys (Alt+letter). */
306
+ const ALT_SCAN_CODES: Record<string, number> = Object.fromEntries(
307
+ [..."QWERTYUIOP"].map((k, i) => [`Key${k}`, 0x10 + i]).concat(
308
+ [..."ASDFGHJKL"].map((k, i) => [`Key${k}`, 0x1e + i]),
309
+ [..."ZXCVBNM"].map((k, i) => [`Key${k}`, 0x2c + i]),
310
+ ),
311
+ );
312
+ /** SCI modifier bits: 3 = shift (both), 4 = ctrl, 8 = alt. */
313
+ const modifiers = (e: KeyboardEvent | MouseEvent) => (e.shiftKey ? 3 : 0) | (e.ctrlKey ? 4 : 0) | (e.altKey ? 8 : 0);
314
+
315
+ function readMusicMode(): MusicMode {
316
+ try {
317
+ return localStorage.getItem(`${APP_ID}.music`) === "adlib" ? "adlib" : "gm";
318
+ } catch {
319
+ return "gm";
320
+ }
321
+ }
@@ -0,0 +1,7 @@
1
+ import { type ResourceManager } from "../../../packages/sci/src/index.ts";
2
+ export declare function stopSoundPreview(): void;
3
+ /** Space in the viewer: play or pause the sound on show, if any. */
4
+ export declare function toggleSoundPreview(): boolean;
5
+ /** The sound's length and tracks, for its row in the list. */
6
+ export declare function soundSummary(data: Uint8Array): string;
7
+ export declare function renderSound(rm: ResourceManager, number: number, data: Uint8Array, stillWanted?: () => boolean): Promise<HTMLElement>;
@@ -0,0 +1,214 @@
1
+ import { AudioIndex, ResourceType, SongPlayer, SoundDevice, parseAdLibBank, parseSound } from "../../../packages/sci/src/index.js";
2
+ import { AdLibOutput } from "./adlib.js";
3
+ import { WebAudioOutput } from "./audio.js";
4
+ import { BASE, httpFiles } from "./files.js";
5
+ import { SoundFontMidi } from "./music.js";
6
+ let audio;
7
+ const openAudio = (rm) => (audio ??= (async () => {
8
+ await rm.preload(); // the audio maps and the AdLib bank are read synchronously
9
+ const out = new WebAudioOutput(httpFiles, (name) => rm.file(name));
10
+ const gm = new SoundFontMidi(out.ctx);
11
+ const gmReady = gm.load(`${BASE}soundfonts/GeneralUser-GS.sf2`);
12
+ gmReady.catch((e) => console.warn("SoundFont unavailable:", e));
13
+ const bank = { type: ResourceType.Patch, number: 3 };
14
+ const adlib = rm.has(bank) ? await AdLibOutput.create(out.ctx, parseAdLibBank(rm.loadSync(bank).data)) : undefined;
15
+ return { out, gm, gmReady, adlib, index: AudioIndex.build(rm) };
16
+ })());
17
+ const SOURCE_NAMES = { gm: "General MIDI", adlib: "AdLib", digital: "Digital (SFX)" };
18
+ const nowTicks = () => (performance.now() * 60) / 1000;
19
+ const clock = (ticks) => {
20
+ const s = Math.max(0, Math.round(ticks / 60));
21
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
22
+ };
23
+ /** What's playing, so switching resources (or sources) can stop it. */
24
+ let stopCurrent;
25
+ let toggleCurrent;
26
+ export function stopSoundPreview() {
27
+ stopCurrent?.();
28
+ stopCurrent = undefined;
29
+ toggleCurrent = undefined;
30
+ }
31
+ /** Space in the viewer: play or pause the sound on show, if any. */
32
+ export function toggleSoundPreview() {
33
+ if (!toggleCurrent)
34
+ return false;
35
+ toggleCurrent();
36
+ return true;
37
+ }
38
+ /** The sound's length and tracks, for its row in the list. */
39
+ export function soundSummary(data) {
40
+ const gm = parseSound(data, SoundDevice.GeneralMidi);
41
+ const adlib = parseSound(data, SoundDevice.AdLib);
42
+ const tracks = [gm && "MIDI", adlib && "AdLib"].filter(Boolean).join(" · ");
43
+ const length = gm?.length ?? adlib?.length;
44
+ return [length !== undefined ? clock(length) : undefined, tracks].filter(Boolean).join(" · ");
45
+ }
46
+ const make = (tag, props = {}) => Object.assign(document.createElement(tag), props);
47
+ export async function renderSound(rm, number, data, stillWanted = () => true) {
48
+ stopSoundPreview();
49
+ const songs = {};
50
+ const gmSong = parseSound(data, SoundDevice.GeneralMidi);
51
+ const adlibSong = parseSound(data, SoundDevice.AdLib);
52
+ if (gmSong)
53
+ songs.gm = gmSong;
54
+ // The AdLib track needs the game's instrument bank.
55
+ if (adlibSong && rm.has({ type: ResourceType.Patch, number: 3 }))
56
+ songs.adlib = adlibSong;
57
+ const wrap = make("div", { className: "sound" });
58
+ const info = make("p", { className: "meta" });
59
+ const controls = make("div", { className: "player" });
60
+ const play = make("button", { type: "button", className: "play", textContent: "▶", title: "Play / pause (Space)" });
61
+ const stop = make("button", { type: "button", textContent: "■", title: "Stop" });
62
+ const source = make("select", { title: "Which version to play" });
63
+ const loopBox = make("input", { type: "checkbox" });
64
+ const loop = make("label", { className: "loop-opt" });
65
+ loop.append(loopBox, " loop");
66
+ const seek = make("input", { type: "range", min: "0", max: "1000", value: "0", className: "seek" });
67
+ const time = make("span", { className: "time", textContent: "0:00 / 0:00" });
68
+ controls.append(play, stop, source, loop, seek, time);
69
+ wrap.append(info, controls);
70
+ const a = await openAudio(rm);
71
+ const clip = a.index.effects.get(number);
72
+ const sources = [...(clip ? ["digital"] : []), ...(songs.gm ? ["gm"] : []), ...(songs.adlib ? ["adlib"] : [])];
73
+ for (const s of sources)
74
+ source.append(make("option", { value: s, textContent: SOURCE_NAMES[s] }));
75
+ if (!sources.length) {
76
+ info.textContent = "No playable track for these devices.";
77
+ play.disabled = stop.disabled = true;
78
+ return wrap;
79
+ }
80
+ const describe = (s) => `${s.channels.length} channels · ${s.events.length} events · ${clock(s.length)}`;
81
+ info.textContent = [
82
+ songs.gm && `General MIDI: ${describe(songs.gm)}`,
83
+ songs.adlib && `AdLib: ${describe(songs.adlib)}`,
84
+ clip && `digital effect: ${clock(clip.ticks ?? 0)}`,
85
+ ].filter(Boolean).join(" · ");
86
+ // --- Playback ---
87
+ let player;
88
+ let midi;
89
+ let started = 0; // tick at position 0
90
+ let pausedAt;
91
+ let digitalPlaying = false;
92
+ // A timer, not animation frames: music keeps playing in a background tab.
93
+ let timer;
94
+ const length = () => (source.value === "digital" ? clip?.ticks ?? 0 : songs[source.value]?.length ?? 0);
95
+ const position = () => {
96
+ const at = (pausedAt ?? nowTicks()) - started;
97
+ const len = Math.max(length(), 1);
98
+ return loopBox.checked ? at % len : Math.min(at, len);
99
+ };
100
+ const playing = () => !!player || digitalPlaying;
101
+ const frame = () => {
102
+ if (player && pausedAt === undefined) {
103
+ player.update(nowTicks());
104
+ if (player.finished)
105
+ return halt();
106
+ }
107
+ if (digitalPlaying && pausedAt === undefined && !loopBox.checked && nowTicks() - started >= length())
108
+ return halt();
109
+ seek.value = String(Math.round((position() / Math.max(length(), 1)) * 1000));
110
+ time.textContent = `${clock(position())} / ${clock(length())}`;
111
+ };
112
+ const silence = () => {
113
+ if (!midi)
114
+ return;
115
+ for (let c = 0; c < 16; c++)
116
+ midi.send([0xb0 | c, 123, 0]); // all notes off
117
+ };
118
+ function halt() {
119
+ clearInterval(timer);
120
+ timer = undefined;
121
+ player?.stop();
122
+ silence();
123
+ player = undefined;
124
+ if (digitalPlaying)
125
+ a.out.stop("preview");
126
+ digitalPlaying = false;
127
+ pausedAt = undefined;
128
+ play.textContent = "▶";
129
+ seek.value = "0";
130
+ time.textContent = `0:00 / ${clock(length())}`;
131
+ }
132
+ const start = async (from = 0) => {
133
+ halt();
134
+ a.out.unlock();
135
+ const s = source.value;
136
+ if (s === "digital") {
137
+ a.out.play("preview", clip, { loop: loopBox.checked, volume: 1 });
138
+ digitalPlaying = true;
139
+ started = nowTicks();
140
+ }
141
+ else {
142
+ if (s === "gm") {
143
+ time.textContent = "loading SoundFont…";
144
+ await a.gmReady.catch(() => undefined); // the SoundFont is 32 MB: the first time takes a moment
145
+ midi = a.gm;
146
+ }
147
+ else {
148
+ a.adlib.reset();
149
+ midi = a.adlib;
150
+ }
151
+ player = new SongPlayer(songs[s], midi, loopBox.checked, 127, nowTicks());
152
+ if (from)
153
+ player.seek(from, nowTicks());
154
+ started = nowTicks() - from;
155
+ }
156
+ play.textContent = "❚❚";
157
+ timer = setInterval(frame, 15);
158
+ };
159
+ const toggle = () => {
160
+ if (!playing())
161
+ return void start();
162
+ const now = nowTicks();
163
+ if (pausedAt === undefined) {
164
+ pausedAt = now;
165
+ player?.pause(now);
166
+ if (digitalPlaying)
167
+ a.out.pause("preview");
168
+ play.textContent = "▶";
169
+ }
170
+ else {
171
+ started += now - pausedAt;
172
+ player?.resume(now);
173
+ if (digitalPlaying)
174
+ a.out.resume("preview");
175
+ pausedAt = undefined;
176
+ play.textContent = "❚❚";
177
+ }
178
+ };
179
+ play.onclick = toggle;
180
+ stop.onclick = halt;
181
+ source.onchange = () => {
182
+ const was = playing();
183
+ halt();
184
+ seek.disabled = source.value === "digital";
185
+ if (was)
186
+ void start();
187
+ };
188
+ loopBox.onchange = () => {
189
+ if (player)
190
+ player.loop = loopBox.checked;
191
+ };
192
+ // Seeking: music only (digital clips play from the start).
193
+ seek.oninput = () => {
194
+ if (source.value === "digital")
195
+ return;
196
+ const to = (Number(seek.value) / 1000) * length();
197
+ if (player) {
198
+ player.seek(to, nowTicks());
199
+ started = nowTicks() - to;
200
+ if (pausedAt !== undefined)
201
+ pausedAt = nowTicks();
202
+ }
203
+ else
204
+ void start(to);
205
+ };
206
+ seek.disabled = source.value === "digital";
207
+ time.textContent = `0:00 / ${clock(length())}`;
208
+ // Only the sound on show owns the controls (a slow load may finish after moving on).
209
+ if (stillWanted()) {
210
+ stopCurrent = halt;
211
+ toggleCurrent = toggle;
212
+ }
213
+ return wrap;
214
+ }