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.
- package/CHANGELOG.md +72 -0
- package/LICENSE +21 -0
- package/README.md +113 -2
- package/apps/viewer/editor.html +43 -0
- package/apps/viewer/explore.html +27 -0
- package/apps/viewer/index.html +26 -0
- package/apps/viewer/package.json +27 -0
- package/apps/viewer/play.html +26 -0
- package/apps/viewer/plugin.d.ts +18 -0
- package/apps/viewer/plugin.js +75 -0
- package/apps/viewer/plugin.ts +84 -0
- package/apps/viewer/src/adlib.d.ts +20 -0
- package/apps/viewer/src/adlib.js +55 -0
- package/apps/viewer/src/adlib.ts +62 -0
- package/apps/viewer/src/app.d.ts +5 -0
- package/apps/viewer/src/app.js +5 -0
- package/apps/viewer/src/app.ts +5 -0
- package/apps/viewer/src/asset-index.d.ts +22 -0
- package/apps/viewer/src/asset-index.js +74 -0
- package/apps/viewer/src/asset-index.ts +90 -0
- package/apps/viewer/src/audio.d.ts +28 -0
- package/apps/viewer/src/audio.js +102 -0
- package/apps/viewer/src/audio.ts +110 -0
- package/apps/viewer/src/editor-handles.d.ts +63 -0
- package/apps/viewer/src/editor-handles.js +332 -0
- package/apps/viewer/src/editor-handles.ts +368 -0
- package/apps/viewer/src/editor-languages.d.ts +23 -0
- package/apps/viewer/src/editor-languages.js +157 -0
- package/apps/viewer/src/editor-languages.ts +135 -0
- package/apps/viewer/src/editor.css +213 -0
- package/apps/viewer/src/editor.d.ts +1 -0
- package/apps/viewer/src/editor.js +393 -0
- package/apps/viewer/src/editor.ts +412 -0
- package/apps/viewer/src/explore/classes.d.ts +7 -0
- package/apps/viewer/src/explore/classes.js +63 -0
- package/apps/viewer/src/explore/classes.ts +61 -0
- package/apps/viewer/src/explore/context.d.ts +33 -0
- package/apps/viewer/src/explore/context.js +96 -0
- package/apps/viewer/src/explore/context.ts +124 -0
- package/apps/viewer/src/explore/explore.css +93 -0
- package/apps/viewer/src/explore/explore.d.ts +1 -0
- package/apps/viewer/src/explore/explore.js +102 -0
- package/apps/viewer/src/explore/explore.ts +104 -0
- package/apps/viewer/src/explore/inspect.d.ts +5 -0
- package/apps/viewer/src/explore/inspect.js +208 -0
- package/apps/viewer/src/explore/inspect.ts +279 -0
- package/apps/viewer/src/explore/live.d.ts +22 -0
- package/apps/viewer/src/explore/live.js +318 -0
- package/apps/viewer/src/explore/live.ts +365 -0
- package/apps/viewer/src/explore/map.d.ts +11 -0
- package/apps/viewer/src/explore/map.js +182 -0
- package/apps/viewer/src/explore/map.ts +193 -0
- package/apps/viewer/src/files.d.ts +21 -0
- package/apps/viewer/src/files.js +69 -0
- package/apps/viewer/src/files.ts +72 -0
- package/apps/viewer/src/game-profile.d.ts +19 -0
- package/apps/viewer/src/game-profile.js +4 -0
- package/apps/viewer/src/game-profile.ts +26 -0
- package/apps/viewer/src/main.d.ts +1 -0
- package/apps/viewer/src/main.js +473 -0
- package/apps/viewer/src/main.ts +527 -0
- package/apps/viewer/src/music.d.ts +14 -0
- package/apps/viewer/src/music.js +32 -0
- package/apps/viewer/src/music.ts +34 -0
- package/apps/viewer/src/opl-worklet.d.ts +1 -0
- package/apps/viewer/src/opl-worklet.js +47 -0
- package/apps/viewer/src/opl-worklet.ts +55 -0
- package/apps/viewer/src/overlay.d.ts +31 -0
- package/apps/viewer/src/overlay.js +103 -0
- package/apps/viewer/src/overlay.ts +123 -0
- package/apps/viewer/src/play.css +14 -0
- package/apps/viewer/src/play.d.ts +1 -0
- package/apps/viewer/src/play.js +179 -0
- package/apps/viewer/src/play.ts +177 -0
- package/apps/viewer/src/saves.d.ts +10 -0
- package/apps/viewer/src/saves.js +50 -0
- package/apps/viewer/src/saves.ts +51 -0
- package/apps/viewer/src/session.d.ts +91 -0
- package/apps/viewer/src/session.js +301 -0
- package/apps/viewer/src/session.ts +321 -0
- package/apps/viewer/src/sound-preview.d.ts +7 -0
- package/apps/viewer/src/sound-preview.js +214 -0
- package/apps/viewer/src/sound-preview.ts +228 -0
- package/apps/viewer/src/style.css +64 -0
- package/apps/viewer/vite.config.ts +25 -0
- package/bin/sci-ts.mjs +9 -0
- package/lib/994.sc +49 -0
- package/lib/995.sc +53 -0
- package/lib/996.sc +299 -0
- package/lib/997.sc +198 -0
- package/lib/998.sc +338 -0
- package/lib/999.sc +189 -0
- package/lib/resources.d.ts +2 -0
- package/lib/resources.js +84 -0
- package/lib/resources.ts +88 -0
- package/lib/system.sh +62 -0
- package/package.json +75 -4
- package/packages/content/room.schema.json +267 -0
- package/packages/content/src/index.d.ts +3 -0
- package/packages/content/src/index.js +3 -0
- package/packages/content/src/index.ts +3 -0
- package/packages/content/src/room.d.ts +111 -0
- package/packages/content/src/room.js +886 -0
- package/packages/content/src/room.ts +1011 -0
- package/packages/content/src/target.d.ts +80 -0
- package/packages/content/src/target.js +1 -0
- package/packages/content/src/target.ts +60 -0
- package/packages/content/src/yarn.d.ts +90 -0
- package/packages/content/src/yarn.js +214 -0
- package/packages/content/src/yarn.ts +261 -0
- package/packages/sci/src/analysis/game.d.ts +90 -0
- package/packages/sci/src/analysis/game.js +221 -0
- package/packages/sci/src/analysis/game.ts +305 -0
- package/packages/sci/src/analysis/runtime.d.ts +58 -0
- package/packages/sci/src/analysis/runtime.js +133 -0
- package/packages/sci/src/analysis/runtime.ts +163 -0
- package/packages/sci/src/audio/adlib.d.ts +73 -0
- package/packages/sci/src/audio/adlib.js +280 -0
- package/packages/sci/src/audio/adlib.ts +329 -0
- package/packages/sci/src/audio/index.d.ts +43 -0
- package/packages/sci/src/audio/index.js +99 -0
- package/packages/sci/src/audio/index.ts +122 -0
- package/packages/sci/src/audio/midi.d.ts +92 -0
- package/packages/sci/src/audio/midi.js +242 -0
- package/packages/sci/src/audio/midi.ts +262 -0
- package/packages/sci/src/audio/opl2.d.ts +74 -0
- package/packages/sci/src/audio/opl2.js +283 -0
- package/packages/sci/src/audio/opl2.ts +283 -0
- package/packages/sci/src/audio/sol.d.ts +32 -0
- package/packages/sci/src/audio/sol.js +81 -0
- package/packages/sci/src/audio/sol.ts +100 -0
- package/packages/sci/src/audio/write.d.ts +35 -0
- package/packages/sci/src/audio/write.js +91 -0
- package/packages/sci/src/audio/write.ts +108 -0
- package/packages/sci/src/gfx/cel.d.ts +36 -0
- package/packages/sci/src/gfx/cel.js +82 -0
- package/packages/sci/src/gfx/cel.ts +110 -0
- package/packages/sci/src/gfx/palette.d.ts +24 -0
- package/packages/sci/src/gfx/palette.js +49 -0
- package/packages/sci/src/gfx/palette.ts +56 -0
- package/packages/sci/src/gfx/pic-file.d.ts +46 -0
- package/packages/sci/src/gfx/pic-file.js +114 -0
- package/packages/sci/src/gfx/pic-file.ts +155 -0
- package/packages/sci/src/gfx/pic.d.ts +27 -0
- package/packages/sci/src/gfx/pic.js +34 -0
- package/packages/sci/src/gfx/pic.ts +53 -0
- package/packages/sci/src/gfx/quantize.d.ts +42 -0
- package/packages/sci/src/gfx/quantize.js +199 -0
- package/packages/sci/src/gfx/quantize.ts +218 -0
- package/packages/sci/src/gfx/view-file.d.ts +62 -0
- package/packages/sci/src/gfx/view-file.js +182 -0
- package/packages/sci/src/gfx/view-file.ts +233 -0
- package/packages/sci/src/gfx/view.d.ts +28 -0
- package/packages/sci/src/gfx/view.js +45 -0
- package/packages/sci/src/gfx/view.ts +62 -0
- package/packages/sci/src/index.d.ts +38 -0
- package/packages/sci/src/index.js +38 -0
- package/packages/sci/src/index.ts +38 -0
- package/packages/sci/src/motion/pathfinding.d.ts +40 -0
- package/packages/sci/src/motion/pathfinding.js +140 -0
- package/packages/sci/src/motion/pathfinding.ts +147 -0
- package/packages/sci/src/resource/archive.d.ts +16 -0
- package/packages/sci/src/resource/archive.js +51 -0
- package/packages/sci/src/resource/archive.ts +56 -0
- package/packages/sci/src/resource/lzs.d.ts +12 -0
- package/packages/sci/src/resource/lzs.js +64 -0
- package/packages/sci/src/resource/lzs.ts +66 -0
- package/packages/sci/src/resource/manager.d.ts +45 -0
- package/packages/sci/src/resource/manager.js +158 -0
- package/packages/sci/src/resource/manager.ts +184 -0
- package/packages/sci/src/resource/map.d.ts +18 -0
- package/packages/sci/src/resource/map.js +34 -0
- package/packages/sci/src/resource/map.ts +44 -0
- package/packages/sci/src/resource/types.d.ts +37 -0
- package/packages/sci/src/resource/types.js +45 -0
- package/packages/sci/src/resource/types.ts +57 -0
- package/packages/sci/src/script/assembly.d.ts +67 -0
- package/packages/sci/src/script/assembly.js +748 -0
- package/packages/sci/src/script/assembly.ts +714 -0
- package/packages/sci/src/script/compiler.d.ts +75 -0
- package/packages/sci/src/script/compiler.js +993 -0
- package/packages/sci/src/script/compiler.ts +1040 -0
- package/packages/sci/src/script/disasm.d.ts +81 -0
- package/packages/sci/src/script/disasm.js +318 -0
- package/packages/sci/src/script/disasm.ts +323 -0
- package/packages/sci/src/script/kernel-names.d.ts +6 -0
- package/packages/sci/src/script/kernel-names.js +165 -0
- package/packages/sci/src/script/kernel-names.ts +165 -0
- package/packages/sci/src/script/opcodes.d.ts +22 -0
- package/packages/sci/src/script/opcodes.js +45 -0
- package/packages/sci/src/script/opcodes.ts +72 -0
- package/packages/sci/src/script/script.d.ts +56 -0
- package/packages/sci/src/script/script.js +63 -0
- package/packages/sci/src/script/script.ts +111 -0
- package/packages/sci/src/script/sexpr.d.ts +45 -0
- package/packages/sci/src/script/sexpr.js +151 -0
- package/packages/sci/src/script/sexpr.ts +139 -0
- package/packages/sci/src/script/vocab.d.ts +15 -0
- package/packages/sci/src/script/vocab.js +44 -0
- package/packages/sci/src/script/vocab.ts +48 -0
- package/packages/sci/src/script/world.d.ts +25 -0
- package/packages/sci/src/script/world.js +65 -0
- package/packages/sci/src/script/world.ts +70 -0
- package/packages/sci/src/text/font.d.ts +37 -0
- package/packages/sci/src/text/font.js +103 -0
- package/packages/sci/src/text/font.ts +125 -0
- package/packages/sci/src/text/message.d.ts +69 -0
- package/packages/sci/src/text/message.js +171 -0
- package/packages/sci/src/text/message.ts +205 -0
- package/packages/sci/src/vm/kernels/arrays.d.ts +19 -0
- package/packages/sci/src/vm/kernels/arrays.js +182 -0
- package/packages/sci/src/vm/kernels/arrays.ts +164 -0
- package/packages/sci/src/vm/kernels/audio.d.ts +70 -0
- package/packages/sci/src/vm/kernels/audio.js +347 -0
- package/packages/sci/src/vm/kernels/audio.ts +349 -0
- package/packages/sci/src/vm/kernels/core.d.ts +5 -0
- package/packages/sci/src/vm/kernels/core.js +87 -0
- package/packages/sci/src/vm/kernels/core.ts +91 -0
- package/packages/sci/src/vm/kernels/graphics.d.ts +141 -0
- package/packages/sci/src/vm/kernels/graphics.js +467 -0
- package/packages/sci/src/vm/kernels/graphics.ts +502 -0
- package/packages/sci/src/vm/kernels/index.d.ts +8 -0
- package/packages/sci/src/vm/kernels/index.js +29 -0
- package/packages/sci/src/vm/kernels/index.ts +32 -0
- package/packages/sci/src/vm/kernels/lists.d.ts +6 -0
- package/packages/sci/src/vm/kernels/lists.js +127 -0
- package/packages/sci/src/vm/kernels/lists.ts +111 -0
- package/packages/sci/src/vm/kernels/motion.d.ts +7 -0
- package/packages/sci/src/vm/kernels/motion.js +204 -0
- package/packages/sci/src/vm/kernels/motion.ts +194 -0
- package/packages/sci/src/vm/kernels/palette.d.ts +82 -0
- package/packages/sci/src/vm/kernels/palette.js +292 -0
- package/packages/sci/src/vm/kernels/palette.ts +303 -0
- package/packages/sci/src/vm/kernels/saves.d.ts +45 -0
- package/packages/sci/src/vm/kernels/saves.js +123 -0
- package/packages/sci/src/vm/kernels/saves.ts +149 -0
- package/packages/sci/src/vm/kernels/system.d.ts +62 -0
- package/packages/sci/src/vm/kernels/system.js +110 -0
- package/packages/sci/src/vm/kernels/system.ts +128 -0
- package/packages/sci/src/vm/kernels/text.d.ts +13 -0
- package/packages/sci/src/vm/kernels/text.js +338 -0
- package/packages/sci/src/vm/kernels/text.ts +328 -0
- package/packages/sci/src/vm/memory.d.ts +138 -0
- package/packages/sci/src/vm/memory.js +168 -0
- package/packages/sci/src/vm/memory.ts +266 -0
- package/packages/sci/src/vm/replace.d.ts +11 -0
- package/packages/sci/src/vm/replace.js +14 -0
- package/packages/sci/src/vm/replace.ts +16 -0
- package/packages/sci/src/vm/savegame.d.ts +89 -0
- package/packages/sci/src/vm/savegame.js +161 -0
- package/packages/sci/src/vm/savegame.ts +188 -0
- package/packages/sci/src/vm/value.d.ts +17 -0
- package/packages/sci/src/vm/value.js +12 -0
- package/packages/sci/src/vm/value.ts +25 -0
- package/packages/sci/src/vm/vm.d.ts +155 -0
- package/packages/sci/src/vm/vm.js +1053 -0
- package/packages/sci/src/vm/vm.ts +914 -0
- package/tools/adlib-render.d.ts +1 -0
- package/tools/adlib-render.js +49 -0
- package/tools/adlib-render.ts +42 -0
- package/tools/analyze.d.ts +1 -0
- package/tools/analyze.js +25 -0
- package/tools/analyze.ts +25 -0
- package/tools/art/build.d.ts +34 -0
- package/tools/art/build.js +175 -0
- package/tools/art/build.ts +153 -0
- package/tools/art.d.ts +1 -0
- package/tools/art.js +31 -0
- package/tools/art.ts +29 -0
- package/tools/asm.d.ts +1 -0
- package/tools/asm.js +61 -0
- package/tools/asm.ts +55 -0
- package/tools/cli.d.ts +1 -0
- package/tools/cli.js +99 -0
- package/tools/cli.ts +100 -0
- package/tools/editor-server.d.ts +29 -0
- package/tools/editor-server.js +162 -0
- package/tools/editor-server.ts +176 -0
- package/tools/explore.d.ts +1 -0
- package/tools/explore.js +305 -0
- package/tools/explore.ts +250 -0
- package/tools/game/audio.d.ts +19 -0
- package/tools/game/audio.js +143 -0
- package/tools/game/audio.ts +130 -0
- package/tools/game/build.d.ts +46 -0
- package/tools/game/build.js +259 -0
- package/tools/game/build.ts +283 -0
- package/tools/game/defaults.d.ts +26 -0
- package/tools/game/defaults.js +82 -0
- package/tools/game/defaults.ts +83 -0
- package/tools/game/font.d.ts +3 -0
- package/tools/game/font.js +124 -0
- package/tools/game/font.ts +126 -0
- package/tools/game/kit.d.ts +7 -0
- package/tools/game/kit.js +7 -0
- package/tools/game/kit.ts +10 -0
- package/tools/game/new.d.ts +8 -0
- package/tools/game/new.js +39 -0
- package/tools/game/new.ts +38 -0
- package/tools/game/target.d.ts +7 -0
- package/tools/game/target.js +41 -0
- package/tools/game/target.ts +42 -0
- package/tools/game/template/resources.ts.tmpl +43 -0
- package/tools/game/template/rooms/1.room.yaml +9 -0
- package/tools/game/template/rooms/1.yarn +17 -0
- package/tools/game/template/scripts/0.sc +24 -0
- package/tools/game.d.ts +1 -0
- package/tools/game.js +77 -0
- package/tools/game.ts +70 -0
- package/tools/mod-build.d.ts +28 -0
- package/tools/mod-build.js +173 -0
- package/tools/mod-build.ts +179 -0
- package/tools/mod-target.d.ts +16 -0
- package/tools/mod-target.js +26 -0
- package/tools/mod-target.ts +32 -0
- package/tools/mod.d.ts +1 -0
- package/tools/mod.js +47 -0
- package/tools/mod.ts +45 -0
- package/tools/msg.d.ts +1 -0
- package/tools/msg.js +40 -0
- package/tools/msg.ts +36 -0
- package/tools/node-files.d.ts +3 -0
- package/tools/node-files.js +34 -0
- package/tools/node-files.ts +33 -0
- package/tools/patches.d.ts +3 -0
- package/tools/patches.js +13 -0
- package/tools/patches.ts +13 -0
- package/tools/pic-build.d.ts +9 -0
- package/tools/pic-build.js +26 -0
- package/tools/pic-build.ts +27 -0
- package/tools/pic.d.ts +1 -0
- package/tools/pic.js +64 -0
- package/tools/pic.ts +61 -0
- package/tools/play.d.ts +1 -0
- package/tools/play.js +54 -0
- package/tools/play.ts +54 -0
- package/tools/png.d.ts +16 -0
- package/tools/png.js +124 -0
- package/tools/png.ts +123 -0
- package/tools/res.d.ts +1 -0
- package/tools/res.js +48 -0
- package/tools/res.ts +50 -0
- package/tools/scenario.d.ts +1 -0
- package/tools/scenario.js +74 -0
- package/tools/scenario.ts +80 -0
- package/tools/site/build.d.ts +8 -0
- package/tools/site/build.js +110 -0
- package/tools/site/build.ts +116 -0
- package/tools/site/markdown.d.ts +11 -0
- package/tools/site/markdown.js +88 -0
- package/tools/site/markdown.ts +84 -0
- package/tools/view-build.d.ts +34 -0
- package/tools/view-build.js +47 -0
- package/tools/view-build.ts +71 -0
- package/tools/view.d.ts +1 -0
- package/tools/view.js +54 -0
- package/tools/view.ts +52 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pathfinding over SCI polygons.
|
|
3
|
+
*
|
|
4
|
+
* Rooms describe walkable space with polygons of four access types:
|
|
5
|
+
* Total (0): can't enter; if you start inside you may walk out.
|
|
6
|
+
* Nearest (1): can't enter; clicking inside walks to its nearest edge.
|
|
7
|
+
* Barred (2): can't enter at all.
|
|
8
|
+
* Contained (3): the walkable area itself: you can't leave it.
|
|
9
|
+
*
|
|
10
|
+
* `findPath` snaps unreachable start/end points to the nearest reachable point, builds a
|
|
11
|
+
* visibility graph over the polygon vertices (two points are connected if the straight
|
|
12
|
+
* segment between them stays walkable) and runs Dijkstra. Boundaries count as walkable,
|
|
13
|
+
* so paths can hug walls and corners, like the original.
|
|
14
|
+
*/
|
|
15
|
+
export const PolygonType = { Total: 0, Nearest: 1, Barred: 2, Contained: 3 } as const;
|
|
16
|
+
|
|
17
|
+
export interface Point {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Polygon {
|
|
23
|
+
type: number;
|
|
24
|
+
points: Point[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const EPS = 1e-9;
|
|
28
|
+
|
|
29
|
+
const cross = (o: Point, a: Point, b: Point) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
30
|
+
|
|
31
|
+
function onSegment(p: Point, a: Point, b: Point): boolean {
|
|
32
|
+
if (Math.abs(cross(a, b, p)) > EPS) return false;
|
|
33
|
+
return p.x >= Math.min(a.x, b.x) - EPS && p.x <= Math.max(a.x, b.x) + EPS && p.y >= Math.min(a.y, b.y) - EPS && p.y <= Math.max(a.y, b.y) + EPS;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function edges(poly: Polygon): [Point, Point][] {
|
|
37
|
+
return poly.points.map((p, i) => [p, poly.points[(i + 1) % poly.points.length]!]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type Containment = "inside" | "outside" | "edge";
|
|
41
|
+
|
|
42
|
+
export function contains(poly: Polygon, p: Point): Containment {
|
|
43
|
+
let inside = false;
|
|
44
|
+
for (const [a, b] of edges(poly)) {
|
|
45
|
+
if (onSegment(p, a, b)) return "edge";
|
|
46
|
+
if (a.y > p.y !== b.y > p.y && p.x < ((b.x - a.x) * (p.y - a.y)) / (b.y - a.y) + a.x) inside = !inside;
|
|
47
|
+
}
|
|
48
|
+
return inside ? "inside" : "outside";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Walkable = inside (or on) every contained polygon and not strictly inside any obstacle. */
|
|
52
|
+
export function walkable(polys: Polygon[], p: Point): boolean {
|
|
53
|
+
for (const poly of polys) {
|
|
54
|
+
const c = contains(poly, p);
|
|
55
|
+
if (poly.type === PolygonType.Contained ? c === "outside" : c === "inside") return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True if segments ab and cd cross at a point interior to both. */
|
|
61
|
+
function properlyIntersect(a: Point, b: Point, c: Point, d: Point): boolean {
|
|
62
|
+
const d1 = cross(c, d, a), d2 = cross(c, d, b), d3 = cross(a, b, c), d4 = cross(a, b, d);
|
|
63
|
+
return ((d1 > EPS && d2 < -EPS) || (d1 < -EPS && d2 > EPS)) && ((d3 > EPS && d4 < -EPS) || (d3 < -EPS && d4 > EPS));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A segment is clear if it crosses no polygon edge and its interior stays walkable. */
|
|
67
|
+
function clear(polys: Polygon[], a: Point, b: Point): boolean {
|
|
68
|
+
for (const poly of polys) for (const [c, d] of edges(poly)) if (properlyIntersect(a, b, c, d)) return false;
|
|
69
|
+
// Crossing no edges can still mean running through a polygon's interior (e.g. a diagonal
|
|
70
|
+
// between two of its vertices), so sample along the segment.
|
|
71
|
+
const steps = Math.max(2, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / 4));
|
|
72
|
+
for (let i = 1; i < steps; i++) {
|
|
73
|
+
const t = i / steps;
|
|
74
|
+
if (!walkable(polys, { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t })) return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function nearestOnSegment(p: Point, a: Point, b: Point): Point {
|
|
80
|
+
const dx = b.x - a.x, dy = b.y - a.y;
|
|
81
|
+
const len2 = dx * dx + dy * dy;
|
|
82
|
+
const t = len2 ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2)) : 0;
|
|
83
|
+
return { x: a.x + t * dx, y: a.y + t * dy };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Nearest walkable point to `p`, found on polygon edges (rounded to whole pixels). */
|
|
87
|
+
export function nearestWalkable(polys: Polygon[], p: Point): Point {
|
|
88
|
+
if (walkable(polys, p)) return p;
|
|
89
|
+
let best: Point | undefined;
|
|
90
|
+
let bestDist = Infinity;
|
|
91
|
+
for (const poly of polys) {
|
|
92
|
+
for (const [a, b] of edges(poly)) {
|
|
93
|
+
const q = nearestOnSegment(p, a, b);
|
|
94
|
+
// Try the point and its integer neighbours; SCI coordinates are whole pixels.
|
|
95
|
+
for (const cand of [{ x: Math.round(q.x), y: Math.round(q.y) }, { x: Math.floor(q.x), y: Math.floor(q.y) }, { x: Math.ceil(q.x), y: Math.ceil(q.y) }, q]) {
|
|
96
|
+
const d = Math.hypot(cand.x - p.x, cand.y - p.y);
|
|
97
|
+
if (d < bestDist && walkable(polys, cand)) {
|
|
98
|
+
best = cand;
|
|
99
|
+
bestDist = d;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return best ?? p;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Shortest walkable path from `start` to `end`, including both endpoints. If the end is
|
|
109
|
+
* unreachable it's moved to the nearest reachable point; if the start is inside an
|
|
110
|
+
* obstacle (e.g. pushed into one), the path first steps out of it.
|
|
111
|
+
*/
|
|
112
|
+
export function findPath(start: Point, end: Point, polys: Polygon[]): Point[] {
|
|
113
|
+
// Obstacles that contain the start are ignored, so an actor stuck in one can walk out.
|
|
114
|
+
const active = polys.filter((p) => p.type === PolygonType.Contained || contains(p, start) !== "inside");
|
|
115
|
+
const from = nearestWalkable(active, start);
|
|
116
|
+
const to = nearestWalkable(active, end);
|
|
117
|
+
const prefix = from.x !== start.x || from.y !== start.y ? [start] : [];
|
|
118
|
+
|
|
119
|
+
if (clear(active, from, to)) return [...prefix, from, to];
|
|
120
|
+
|
|
121
|
+
const nodes: Point[] = [from, to];
|
|
122
|
+
for (const poly of active) for (const p of poly.points) if (walkable(active, p)) nodes.push(p);
|
|
123
|
+
|
|
124
|
+
// Dijkstra over the visibility graph (edges evaluated lazily; rooms have few vertices).
|
|
125
|
+
const dist = new Array<number>(nodes.length).fill(Infinity);
|
|
126
|
+
const prev = new Array<number>(nodes.length).fill(-1);
|
|
127
|
+
const done = new Array<boolean>(nodes.length).fill(false);
|
|
128
|
+
dist[0] = 0;
|
|
129
|
+
for (;;) {
|
|
130
|
+
let u = -1;
|
|
131
|
+
for (let i = 0; i < nodes.length; i++) if (!done[i] && dist[i]! < Infinity && (u === -1 || dist[i]! < dist[u]!)) u = i;
|
|
132
|
+
if (u === -1 || u === 1) break;
|
|
133
|
+
done[u] = true;
|
|
134
|
+
for (let v = 0; v < nodes.length; v++) {
|
|
135
|
+
if (done[v]) continue;
|
|
136
|
+
const w = Math.hypot(nodes[v]!.x - nodes[u]!.x, nodes[v]!.y - nodes[u]!.y);
|
|
137
|
+
if (dist[u]! + w < dist[v]! && clear(active, nodes[u]!, nodes[v]!)) {
|
|
138
|
+
dist[v] = dist[u]! + w;
|
|
139
|
+
prev[v] = u;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (prev[1] === -1) return [...prefix, from]; // unreachable: stay put
|
|
144
|
+
const path: Point[] = [];
|
|
145
|
+
for (let i = 1; i !== -1; i = prev[i]!) path.unshift(nodes[i]!);
|
|
146
|
+
return [...prefix, ...path];
|
|
147
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ResourceId } from "./types.ts";
|
|
2
|
+
export interface ResourceData extends ResourceId {
|
|
3
|
+
data: Uint8Array;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* RESOURCE.MAP and RESOURCE.000 for a set of resources: the files an interpreter opens a game
|
|
7
|
+
* by. Resources are stored uncompressed (method 0), in type then number order.
|
|
8
|
+
*
|
|
9
|
+
* volume entry: u8 type, u16 number, u32 packed size, u32 unpacked size, u16 method, data
|
|
10
|
+
* map: { u8 type, u16 table offset } per type, then 0xFF with the end offset;
|
|
11
|
+
* per type, { u16 number, u32 volume offset } per resource (see parseResourceMap)
|
|
12
|
+
*/
|
|
13
|
+
export declare function writeResourceArchive(resources: readonly ResourceData[]): {
|
|
14
|
+
map: Uint8Array;
|
|
15
|
+
volume: Uint8Array;
|
|
16
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { resourceKey } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* RESOURCE.MAP and RESOURCE.000 for a set of resources: the files an interpreter opens a game
|
|
4
|
+
* by. Resources are stored uncompressed (method 0), in type then number order.
|
|
5
|
+
*
|
|
6
|
+
* volume entry: u8 type, u16 number, u32 packed size, u32 unpacked size, u16 method, data
|
|
7
|
+
* map: { u8 type, u16 table offset } per type, then 0xFF with the end offset;
|
|
8
|
+
* per type, { u16 number, u32 volume offset } per resource (see parseResourceMap)
|
|
9
|
+
*/
|
|
10
|
+
export function writeResourceArchive(resources) {
|
|
11
|
+
const sorted = [...resources].sort((a, b) => a.type - b.type || a.number - b.number);
|
|
12
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
13
|
+
if (sorted[i].type === sorted[i - 1].type && sorted[i].number === sorted[i - 1].number) {
|
|
14
|
+
throw new Error(`resource ${resourceKey(sorted[i])} is there twice`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const volume = new Uint8Array(sorted.reduce((n, r) => n + 13 + r.data.length, 0));
|
|
18
|
+
const vv = new DataView(volume.buffer);
|
|
19
|
+
const offsets = [];
|
|
20
|
+
let at = 0;
|
|
21
|
+
for (const r of sorted) {
|
|
22
|
+
offsets.push(at);
|
|
23
|
+
volume[at] = r.type;
|
|
24
|
+
vv.setUint16(at + 1, r.number, true);
|
|
25
|
+
vv.setUint32(at + 3, r.data.length, true);
|
|
26
|
+
vv.setUint32(at + 7, r.data.length, true);
|
|
27
|
+
vv.setUint16(at + 11, 0, true);
|
|
28
|
+
volume.set(r.data, at + 13);
|
|
29
|
+
at += 13 + r.data.length;
|
|
30
|
+
}
|
|
31
|
+
const types = [...new Set(sorted.map((r) => r.type))];
|
|
32
|
+
const map = new Uint8Array((types.length + 1) * 3 + sorted.length * 6);
|
|
33
|
+
if (map.length > 0xffff)
|
|
34
|
+
throw new Error(`too many resources for one map (${sorted.length})`);
|
|
35
|
+
const mv = new DataView(map.buffer);
|
|
36
|
+
let table = (types.length + 1) * 3;
|
|
37
|
+
types.forEach((type, i) => {
|
|
38
|
+
map[i * 3] = type;
|
|
39
|
+
mv.setUint16(i * 3 + 1, table, true);
|
|
40
|
+
sorted.forEach((r, k) => {
|
|
41
|
+
if (r.type !== type)
|
|
42
|
+
return;
|
|
43
|
+
mv.setUint16(table, r.number, true);
|
|
44
|
+
mv.setUint32(table + 2, offsets[k], true);
|
|
45
|
+
table += 6;
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
map[types.length * 3] = 0xff;
|
|
49
|
+
mv.setUint16(types.length * 3 + 1, table, true);
|
|
50
|
+
return { map, volume };
|
|
51
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { resourceKey, type ResourceId } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export interface ResourceData extends ResourceId {
|
|
4
|
+
data: Uint8Array;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* RESOURCE.MAP and RESOURCE.000 for a set of resources: the files an interpreter opens a game
|
|
9
|
+
* by. Resources are stored uncompressed (method 0), in type then number order.
|
|
10
|
+
*
|
|
11
|
+
* volume entry: u8 type, u16 number, u32 packed size, u32 unpacked size, u16 method, data
|
|
12
|
+
* map: { u8 type, u16 table offset } per type, then 0xFF with the end offset;
|
|
13
|
+
* per type, { u16 number, u32 volume offset } per resource (see parseResourceMap)
|
|
14
|
+
*/
|
|
15
|
+
export function writeResourceArchive(resources: readonly ResourceData[]): { map: Uint8Array; volume: Uint8Array } {
|
|
16
|
+
const sorted = [...resources].sort((a, b) => a.type - b.type || a.number - b.number);
|
|
17
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
18
|
+
if (sorted[i]!.type === sorted[i - 1]!.type && sorted[i]!.number === sorted[i - 1]!.number) {
|
|
19
|
+
throw new Error(`resource ${resourceKey(sorted[i]!)} is there twice`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const volume = new Uint8Array(sorted.reduce((n, r) => n + 13 + r.data.length, 0));
|
|
24
|
+
const vv = new DataView(volume.buffer);
|
|
25
|
+
const offsets: number[] = [];
|
|
26
|
+
let at = 0;
|
|
27
|
+
for (const r of sorted) {
|
|
28
|
+
offsets.push(at);
|
|
29
|
+
volume[at] = r.type;
|
|
30
|
+
vv.setUint16(at + 1, r.number, true);
|
|
31
|
+
vv.setUint32(at + 3, r.data.length, true);
|
|
32
|
+
vv.setUint32(at + 7, r.data.length, true);
|
|
33
|
+
vv.setUint16(at + 11, 0, true);
|
|
34
|
+
volume.set(r.data, at + 13);
|
|
35
|
+
at += 13 + r.data.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const types = [...new Set(sorted.map((r) => r.type))];
|
|
39
|
+
const map = new Uint8Array((types.length + 1) * 3 + sorted.length * 6);
|
|
40
|
+
if (map.length > 0xffff) throw new Error(`too many resources for one map (${sorted.length})`);
|
|
41
|
+
const mv = new DataView(map.buffer);
|
|
42
|
+
let table = (types.length + 1) * 3;
|
|
43
|
+
types.forEach((type, i) => {
|
|
44
|
+
map[i * 3] = type;
|
|
45
|
+
mv.setUint16(i * 3 + 1, table, true);
|
|
46
|
+
sorted.forEach((r, k) => {
|
|
47
|
+
if (r.type !== type) return;
|
|
48
|
+
mv.setUint16(table, r.number, true);
|
|
49
|
+
mv.setUint32(table + 2, offsets[k]!, true);
|
|
50
|
+
table += 6;
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
map[types.length * 3] = 0xff;
|
|
54
|
+
mv.setUint16(types.length * 3 + 1, table, true);
|
|
55
|
+
return { map, volume };
|
|
56
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STACpack / LZS decompression (compression method 32 in SCI2 volumes).
|
|
3
|
+
*
|
|
4
|
+
* Bitstream is read MSB-first. Tokens:
|
|
5
|
+
* 0 bbbbbbbb literal byte
|
|
6
|
+
* 1 1 ooooooo len back-reference, 7-bit offset (offset 0 = end marker)
|
|
7
|
+
* 1 0 ooooooooooo len back-reference, 11-bit offset
|
|
8
|
+
*
|
|
9
|
+
* Length codes: 00=2 01=3 10=4 | 1100=5 1101=6 1110=7 | 1111 then nibbles
|
|
10
|
+
* added to 8 until a nibble other than 0xF.
|
|
11
|
+
*/
|
|
12
|
+
export declare function decompressLzs(src: Uint8Array, unpackedSize: number): Uint8Array;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STACpack / LZS decompression (compression method 32 in SCI2 volumes).
|
|
3
|
+
*
|
|
4
|
+
* Bitstream is read MSB-first. Tokens:
|
|
5
|
+
* 0 bbbbbbbb literal byte
|
|
6
|
+
* 1 1 ooooooo len back-reference, 7-bit offset (offset 0 = end marker)
|
|
7
|
+
* 1 0 ooooooooooo len back-reference, 11-bit offset
|
|
8
|
+
*
|
|
9
|
+
* Length codes: 00=2 01=3 10=4 | 1100=5 1101=6 1110=7 | 1111 then nibbles
|
|
10
|
+
* added to 8 until a nibble other than 0xF.
|
|
11
|
+
*/
|
|
12
|
+
export function decompressLzs(src, unpackedSize) {
|
|
13
|
+
const out = new Uint8Array(unpackedSize);
|
|
14
|
+
let written = 0;
|
|
15
|
+
let bytePos = 0;
|
|
16
|
+
let bitBuf = 0;
|
|
17
|
+
let bitCount = 0;
|
|
18
|
+
const bits = (n) => {
|
|
19
|
+
while (bitCount < n) {
|
|
20
|
+
bitBuf = (bitBuf << 8) | (src[bytePos++] ?? 0);
|
|
21
|
+
bitCount += 8;
|
|
22
|
+
}
|
|
23
|
+
bitCount -= n;
|
|
24
|
+
return (bitBuf >>> bitCount) & ((1 << n) - 1);
|
|
25
|
+
};
|
|
26
|
+
const length = () => {
|
|
27
|
+
switch (bits(2)) {
|
|
28
|
+
case 0: return 2;
|
|
29
|
+
case 1: return 3;
|
|
30
|
+
case 2: return 4;
|
|
31
|
+
}
|
|
32
|
+
switch (bits(2)) {
|
|
33
|
+
case 0: return 5;
|
|
34
|
+
case 1: return 6;
|
|
35
|
+
case 2: return 7;
|
|
36
|
+
}
|
|
37
|
+
let len = 8;
|
|
38
|
+
let nibble;
|
|
39
|
+
do {
|
|
40
|
+
nibble = bits(4);
|
|
41
|
+
len += nibble;
|
|
42
|
+
} while (nibble === 0xf);
|
|
43
|
+
return len;
|
|
44
|
+
};
|
|
45
|
+
while (written < unpackedSize) {
|
|
46
|
+
if (bits(1) === 0) {
|
|
47
|
+
out[written++] = bits(8);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const offset = bits(1) ? bits(7) : bits(11);
|
|
51
|
+
if (offset === 0)
|
|
52
|
+
break;
|
|
53
|
+
let from = written - offset;
|
|
54
|
+
if (from < 0)
|
|
55
|
+
throw new Error(`LZS: back-reference before start (offset ${offset} at ${written})`);
|
|
56
|
+
for (let n = length(); n > 0 && written < unpackedSize; n--) {
|
|
57
|
+
out[written++] = out[from++];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (written !== unpackedSize) {
|
|
61
|
+
throw new Error(`LZS: produced ${written} bytes, expected ${unpackedSize}`);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STACpack / LZS decompression (compression method 32 in SCI2 volumes).
|
|
3
|
+
*
|
|
4
|
+
* Bitstream is read MSB-first. Tokens:
|
|
5
|
+
* 0 bbbbbbbb literal byte
|
|
6
|
+
* 1 1 ooooooo len back-reference, 7-bit offset (offset 0 = end marker)
|
|
7
|
+
* 1 0 ooooooooooo len back-reference, 11-bit offset
|
|
8
|
+
*
|
|
9
|
+
* Length codes: 00=2 01=3 10=4 | 1100=5 1101=6 1110=7 | 1111 then nibbles
|
|
10
|
+
* added to 8 until a nibble other than 0xF.
|
|
11
|
+
*/
|
|
12
|
+
export function decompressLzs(src: Uint8Array, unpackedSize: number): Uint8Array {
|
|
13
|
+
const out = new Uint8Array(unpackedSize);
|
|
14
|
+
let written = 0;
|
|
15
|
+
let bytePos = 0;
|
|
16
|
+
let bitBuf = 0;
|
|
17
|
+
let bitCount = 0;
|
|
18
|
+
|
|
19
|
+
const bits = (n: number): number => {
|
|
20
|
+
while (bitCount < n) {
|
|
21
|
+
bitBuf = (bitBuf << 8) | (src[bytePos++] ?? 0);
|
|
22
|
+
bitCount += 8;
|
|
23
|
+
}
|
|
24
|
+
bitCount -= n;
|
|
25
|
+
return (bitBuf >>> bitCount) & ((1 << n) - 1);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const length = (): number => {
|
|
29
|
+
switch (bits(2)) {
|
|
30
|
+
case 0: return 2;
|
|
31
|
+
case 1: return 3;
|
|
32
|
+
case 2: return 4;
|
|
33
|
+
}
|
|
34
|
+
switch (bits(2)) {
|
|
35
|
+
case 0: return 5;
|
|
36
|
+
case 1: return 6;
|
|
37
|
+
case 2: return 7;
|
|
38
|
+
}
|
|
39
|
+
let len = 8;
|
|
40
|
+
let nibble: number;
|
|
41
|
+
do {
|
|
42
|
+
nibble = bits(4);
|
|
43
|
+
len += nibble;
|
|
44
|
+
} while (nibble === 0xf);
|
|
45
|
+
return len;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
while (written < unpackedSize) {
|
|
49
|
+
if (bits(1) === 0) {
|
|
50
|
+
out[written++] = bits(8);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const offset = bits(1) ? bits(7) : bits(11);
|
|
54
|
+
if (offset === 0) break;
|
|
55
|
+
let from = written - offset;
|
|
56
|
+
if (from < 0) throw new Error(`LZS: back-reference before start (offset ${offset} at ${written})`);
|
|
57
|
+
for (let n = length(); n > 0 && written < unpackedSize; n--) {
|
|
58
|
+
out[written++] = out[from++]!;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (written !== unpackedSize) {
|
|
63
|
+
throw new Error(`LZS: produced ${written} bytes, expected ${unpackedSize}`);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type ResourceId, type ResourceType } from "./types.ts";
|
|
2
|
+
/** Where game files come from: Node fs in tools, fetch/File API in the browser. */
|
|
3
|
+
export interface FileSource {
|
|
4
|
+
read(path: string): Promise<Uint8Array | undefined>;
|
|
5
|
+
/** Byte range read, so large volumes needn't be loaded whole. */
|
|
6
|
+
readRange(path: string, offset: number, length: number): Promise<Uint8Array>;
|
|
7
|
+
list(dir: string): Promise<string[]>;
|
|
8
|
+
}
|
|
9
|
+
export interface ResourceInfo extends ResourceId {
|
|
10
|
+
source: "volume" | "patch" | "memory";
|
|
11
|
+
/** Volume: header offset. Patch: file path. */
|
|
12
|
+
location: number | string;
|
|
13
|
+
}
|
|
14
|
+
export interface Resource extends ResourceId {
|
|
15
|
+
data: Uint8Array;
|
|
16
|
+
}
|
|
17
|
+
export declare class ResourceManager {
|
|
18
|
+
private readonly files;
|
|
19
|
+
private readonly index;
|
|
20
|
+
private volume;
|
|
21
|
+
private readonly patches;
|
|
22
|
+
private readonly extras;
|
|
23
|
+
/** Resources replaced at run time (an editor's latest build), by key. */
|
|
24
|
+
private readonly memory;
|
|
25
|
+
private constructor();
|
|
26
|
+
/** Indexes RESOURCE.MAP, then lets loose patch files (game dir, then PATCHES/) override it. */
|
|
27
|
+
static open(files: FileSource, patchDirs?: string[]): Promise<ResourceManager>;
|
|
28
|
+
list(type?: ResourceType): ResourceInfo[];
|
|
29
|
+
has(id: ResourceId): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Replaces (or adds) a resource with already decoded data, until the page goes away: a
|
|
32
|
+
* live editor's rebuilt room. Whatever parsed the old one must forget it (replaceResources).
|
|
33
|
+
*/
|
|
34
|
+
override(id: ResourceId, data: Uint8Array): void;
|
|
35
|
+
load(id: ResourceId): Promise<Resource>;
|
|
36
|
+
/**
|
|
37
|
+
* Reads the whole volume and every patch into memory so `loadSync` works. The VM needs
|
|
38
|
+
* this: bytecode can load a script or look up a view in the middle of an instruction.
|
|
39
|
+
*/
|
|
40
|
+
preload(extraFiles?: string[]): Promise<void>;
|
|
41
|
+
loadSync(id: ResourceId): Resource;
|
|
42
|
+
/** A whole game file read by `preload` (e.g. RESOURCE.SFX), if present. */
|
|
43
|
+
file(name: string): Uint8Array | undefined;
|
|
44
|
+
private info;
|
|
45
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { decompressLzs } from "./lzs.js";
|
|
2
|
+
import { parseResourceMap } from "./map.js";
|
|
3
|
+
import { patchExtensions, resourceKey } from "./types.js";
|
|
4
|
+
const VOLUME = "RESOURCE.000";
|
|
5
|
+
const VOLUME_HEADER_SIZE = 13; // type u8, number u16, packed u32, unpacked u32, method u16
|
|
6
|
+
const Compression = { None: 0, Lzs: 32 };
|
|
7
|
+
/** Extra-header sizes for patch size bytes with bit 7 set (per ScummVM). */
|
|
8
|
+
const patchHeaderCodes = { 0: 24, 1: 2, 4: 8 };
|
|
9
|
+
export class ResourceManager {
|
|
10
|
+
files;
|
|
11
|
+
index = new Map();
|
|
12
|
+
volume;
|
|
13
|
+
patches = new Map();
|
|
14
|
+
extras = new Map();
|
|
15
|
+
/** Resources replaced at run time (an editor's latest build), by key. */
|
|
16
|
+
memory = new Map();
|
|
17
|
+
constructor(files) {
|
|
18
|
+
this.files = files;
|
|
19
|
+
}
|
|
20
|
+
/** Indexes RESOURCE.MAP, then lets loose patch files (game dir, then PATCHES/) override it. */
|
|
21
|
+
static async open(files, patchDirs = ["", "PATCHES"]) {
|
|
22
|
+
const rm = new ResourceManager(files);
|
|
23
|
+
const map = await files.read("RESOURCE.MAP");
|
|
24
|
+
if (!map)
|
|
25
|
+
throw new Error("RESOURCE.MAP not found");
|
|
26
|
+
for (const e of parseResourceMap(map)) {
|
|
27
|
+
rm.index.set(resourceKey(e), { type: e.type, number: e.number, source: "volume", location: e.offset });
|
|
28
|
+
}
|
|
29
|
+
for (const dir of patchDirs) {
|
|
30
|
+
for (const name of await files.list(dir)) {
|
|
31
|
+
const m = /^(\d+)\.([a-z0-9]+)$/i.exec(name);
|
|
32
|
+
const type = m && patchExtensions[m[2].toLowerCase()];
|
|
33
|
+
if (type === undefined || type === null)
|
|
34
|
+
continue;
|
|
35
|
+
const id = { type, number: Number(m[1]) };
|
|
36
|
+
rm.index.set(resourceKey(id), { ...id, source: "patch", location: dir ? `${dir}/${name}` : name });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return rm;
|
|
40
|
+
}
|
|
41
|
+
list(type) {
|
|
42
|
+
const all = [...this.index.values()];
|
|
43
|
+
return (type === undefined ? all : all.filter((r) => r.type === type)).sort((a, b) => a.type - b.type || a.number - b.number);
|
|
44
|
+
}
|
|
45
|
+
has(id) {
|
|
46
|
+
return this.index.has(resourceKey(id));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Replaces (or adds) a resource with already decoded data, until the page goes away: a
|
|
50
|
+
* live editor's rebuilt room. Whatever parsed the old one must forget it (replaceResources).
|
|
51
|
+
*/
|
|
52
|
+
override(id, data) {
|
|
53
|
+
const key = resourceKey(id);
|
|
54
|
+
this.memory.set(key, data);
|
|
55
|
+
this.index.set(key, { type: id.type, number: id.number, source: "memory", location: key });
|
|
56
|
+
}
|
|
57
|
+
async load(id) {
|
|
58
|
+
const info = this.info(id);
|
|
59
|
+
let data;
|
|
60
|
+
if (info.source === "memory") {
|
|
61
|
+
data = this.memory.get(info.location);
|
|
62
|
+
}
|
|
63
|
+
else if (info.source === "patch") {
|
|
64
|
+
const file = await this.files.read(info.location);
|
|
65
|
+
if (!file)
|
|
66
|
+
throw new Error(`Patch file ${info.location} disappeared`);
|
|
67
|
+
data = decodePatch(info, file);
|
|
68
|
+
}
|
|
69
|
+
else if (this.volume) {
|
|
70
|
+
data = decodeVolumeEntry(info, this.volume, info.location);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const offset = info.location;
|
|
74
|
+
const header = await this.files.readRange(VOLUME, offset, VOLUME_HEADER_SIZE);
|
|
75
|
+
const packed = new DataView(header.buffer, header.byteOffset).getUint32(3, true);
|
|
76
|
+
const entry = new Uint8Array(VOLUME_HEADER_SIZE + packed);
|
|
77
|
+
entry.set(header);
|
|
78
|
+
entry.set(await this.files.readRange(VOLUME, offset + VOLUME_HEADER_SIZE, packed), VOLUME_HEADER_SIZE);
|
|
79
|
+
data = decodeVolumeEntry(info, entry, 0);
|
|
80
|
+
}
|
|
81
|
+
return { type: info.type, number: info.number, data };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Reads the whole volume and every patch into memory so `loadSync` works. The VM needs
|
|
85
|
+
* this: bytecode can load a script or look up a view in the middle of an instruction.
|
|
86
|
+
*/
|
|
87
|
+
async preload(extraFiles = ["RESOURCE.SFX"]) {
|
|
88
|
+
this.volume ??= await this.files.read(VOLUME);
|
|
89
|
+
for (const name of extraFiles) {
|
|
90
|
+
const data = this.extras.has(name) ? undefined : await this.files.read(name);
|
|
91
|
+
if (data)
|
|
92
|
+
this.extras.set(name, data);
|
|
93
|
+
}
|
|
94
|
+
for (const info of this.index.values()) {
|
|
95
|
+
if (info.source === "patch" && !this.patches.has(info.location)) {
|
|
96
|
+
const file = await this.files.read(info.location);
|
|
97
|
+
if (file)
|
|
98
|
+
this.patches.set(info.location, file);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
loadSync(id) {
|
|
103
|
+
const info = this.info(id);
|
|
104
|
+
if (!this.volume)
|
|
105
|
+
throw new Error("loadSync before preload()");
|
|
106
|
+
const data = info.source === "memory"
|
|
107
|
+
? this.memory.get(info.location)
|
|
108
|
+
: info.source === "patch"
|
|
109
|
+
? decodePatch(info, this.patches.get(info.location))
|
|
110
|
+
: decodeVolumeEntry(info, this.volume, info.location);
|
|
111
|
+
return { type: info.type, number: info.number, data };
|
|
112
|
+
}
|
|
113
|
+
/** A whole game file read by `preload` (e.g. RESOURCE.SFX), if present. */
|
|
114
|
+
file(name) {
|
|
115
|
+
return this.extras.get(name);
|
|
116
|
+
}
|
|
117
|
+
info(id) {
|
|
118
|
+
const info = this.index.get(resourceKey(id));
|
|
119
|
+
if (!info)
|
|
120
|
+
throw new Error(`Resource ${resourceKey(id)} not found`);
|
|
121
|
+
return info;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Decodes the volume entry at `offset` in `bytes`. */
|
|
125
|
+
function decodeVolumeEntry(info, bytes, offset) {
|
|
126
|
+
const h = new DataView(bytes.buffer, bytes.byteOffset + offset, VOLUME_HEADER_SIZE);
|
|
127
|
+
const type = h.getUint8(0) & 0x7f;
|
|
128
|
+
const number = h.getUint16(1, true);
|
|
129
|
+
const packed = h.getUint32(3, true);
|
|
130
|
+
const unpacked = h.getUint32(7, true);
|
|
131
|
+
const method = h.getUint16(11, true);
|
|
132
|
+
if (type !== info.type || number !== info.number) {
|
|
133
|
+
throw new Error(`Volume header mismatch at ${info.location}: got ${type}:${number}, expected ${resourceKey(info)}`);
|
|
134
|
+
}
|
|
135
|
+
const body = bytes.subarray(offset + VOLUME_HEADER_SIZE, offset + VOLUME_HEADER_SIZE + packed);
|
|
136
|
+
switch (method) {
|
|
137
|
+
case Compression.None: return body;
|
|
138
|
+
case Compression.Lzs: return decompressLzs(body, unpacked);
|
|
139
|
+
default: throw new Error(`Unsupported compression method ${method} for ${resourceKey(info)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Patch files: u8 type, u8 extra-header size, [extra header], data.
|
|
144
|
+
* SCI32 view/pic patches set bit 7 of the size byte and store a code instead.
|
|
145
|
+
*/
|
|
146
|
+
function decodePatch(info, file) {
|
|
147
|
+
const type = file[0] & 0x7f;
|
|
148
|
+
if (type !== info.type)
|
|
149
|
+
throw new Error(`Patch ${info.location} has type ${type}, expected ${info.type}`);
|
|
150
|
+
let extra = file[1];
|
|
151
|
+
if (extra & 0x80) {
|
|
152
|
+
const size = patchHeaderCodes[extra & 0x7f];
|
|
153
|
+
if (size === undefined)
|
|
154
|
+
throw new Error(`Patch ${info.location}: unknown header code ${extra & 0x7f}`);
|
|
155
|
+
extra = size;
|
|
156
|
+
}
|
|
157
|
+
return file.subarray(2 + extra);
|
|
158
|
+
}
|