sci2-ts 0.15.0 → 0.15.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 +6 -0
- package/apps/viewer/src/files.d.ts +5 -0
- package/apps/viewer/src/files.js +19 -5
- package/apps/viewer/src/files.ts +20 -5
- package/package.json +1 -1
- package/tools/site/build.d.ts +2 -0
- package/tools/site/build.js +10 -1
- package/tools/site/build.ts +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to this project. Versions follow [Semantic Versioning](https
|
|
|
4
4
|
and the entries are generated from commit messages
|
|
5
5
|
([Conventional Commits](https://www.conventionalcommits.org/)) when a release is made.
|
|
6
6
|
|
|
7
|
+
## [0.15.1](https://github.com/rlueder/sci-ts/compare/v0.15.0...v0.15.1) (2026-10-02)
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
* **viewer:** fetch game files with the build's version ([#18](https://github.com/rlueder/sci-ts/issues/18)) ([370e490](https://github.com/rlueder/sci-ts/commit/370e490d9c618d78223fbba1df4850a8c6ebb0a2))
|
|
12
|
+
|
|
7
13
|
## [0.15.0](https://github.com/rlueder/sci-ts/compare/v0.14.0...v0.15.0) (2026-10-02)
|
|
8
14
|
|
|
9
15
|
### Added
|
|
@@ -4,6 +4,11 @@ export declare const BASE: string;
|
|
|
4
4
|
/**
|
|
5
5
|
* FileSource over game/: the dev server's mount (see vite.config.ts), or the game copied
|
|
6
6
|
* next to a built site, with its file names in game/files.json (`pnpm game site`).
|
|
7
|
+
*
|
|
8
|
+
* A built site's files.json also has the build's `version`. It's always fetched fresh, and
|
|
9
|
+
* every game file is fetched with the version on its URL, so after a new build a browser
|
|
10
|
+
* can't mix its cached files with the new ones (an old RESOURCE.000 under a new RESOURCE.MAP
|
|
11
|
+
* doesn't start).
|
|
7
12
|
*/
|
|
8
13
|
export declare const httpFiles: FileSource;
|
|
9
14
|
/**
|
package/apps/viewer/src/files.js
CHANGED
|
@@ -5,14 +5,19 @@ export const BASE = import.meta.env.BASE_URL;
|
|
|
5
5
|
/**
|
|
6
6
|
* FileSource over game/: the dev server's mount (see vite.config.ts), or the game copied
|
|
7
7
|
* next to a built site, with its file names in game/files.json (`pnpm game site`).
|
|
8
|
+
*
|
|
9
|
+
* A built site's files.json also has the build's `version`. It's always fetched fresh, and
|
|
10
|
+
* every game file is fetched with the version on its URL, so after a new build a browser
|
|
11
|
+
* can't mix its cached files with the new ones (an old RESOURCE.000 under a new RESOURCE.MAP
|
|
12
|
+
* doesn't start).
|
|
8
13
|
*/
|
|
9
14
|
export const httpFiles = {
|
|
10
15
|
async read(path) {
|
|
11
|
-
const res = await fetch(
|
|
16
|
+
const res = await fetch(await gameUrl(path));
|
|
12
17
|
return res.ok ? new Uint8Array(await res.arrayBuffer()) : undefined;
|
|
13
18
|
},
|
|
14
19
|
async readRange(path, offset, length) {
|
|
15
|
-
const res = await fetch(
|
|
20
|
+
const res = await fetch(await gameUrl(path), { headers: { Range: `bytes=${offset}-${offset + length - 1}` } });
|
|
16
21
|
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
17
22
|
// A server that ignores Range (Python's http.server) sends the whole file.
|
|
18
23
|
return res.status === 206 ? bytes : bytes.subarray(offset, offset + length);
|
|
@@ -20,11 +25,20 @@ export const httpFiles = {
|
|
|
20
25
|
async list(dir) {
|
|
21
26
|
if (import.meta.env.DEV)
|
|
22
27
|
return (await fetch(`/__list?dir=${encodeURIComponent(dir)}`)).json();
|
|
23
|
-
|
|
24
|
-
return (
|
|
28
|
+
const files = (await staticListing())[dir];
|
|
29
|
+
return Array.isArray(files) ? files : [];
|
|
25
30
|
},
|
|
26
31
|
};
|
|
27
|
-
let
|
|
32
|
+
let listing;
|
|
33
|
+
const staticListing = () => (listing ??= fetch(`${BASE}game/files.json`, { cache: "no-store" }).then((r) => (r.ok ? r.json() : Promise.resolve({}))));
|
|
34
|
+
/** A game file's URL: on a built site, with the build's version (see httpFiles). */
|
|
35
|
+
async function gameUrl(path) {
|
|
36
|
+
const url = `${BASE}game/${path}`;
|
|
37
|
+
if (import.meta.env.DEV)
|
|
38
|
+
return url;
|
|
39
|
+
const version = (await staticListing()).version;
|
|
40
|
+
return typeof version === "string" ? `${url}?v=${encodeURIComponent(version)}` : url;
|
|
41
|
+
}
|
|
28
42
|
// --- Mods -------------------------------------------------------------------------------
|
|
29
43
|
const MODS_KEY = `${APP_ID}.mods`;
|
|
30
44
|
/**
|
package/apps/viewer/src/files.ts
CHANGED
|
@@ -7,25 +7,40 @@ export const BASE = import.meta.env.BASE_URL;
|
|
|
7
7
|
/**
|
|
8
8
|
* FileSource over game/: the dev server's mount (see vite.config.ts), or the game copied
|
|
9
9
|
* next to a built site, with its file names in game/files.json (`pnpm game site`).
|
|
10
|
+
*
|
|
11
|
+
* A built site's files.json also has the build's `version`. It's always fetched fresh, and
|
|
12
|
+
* every game file is fetched with the version on its URL, so after a new build a browser
|
|
13
|
+
* can't mix its cached files with the new ones (an old RESOURCE.000 under a new RESOURCE.MAP
|
|
14
|
+
* doesn't start).
|
|
10
15
|
*/
|
|
11
16
|
export const httpFiles: FileSource = {
|
|
12
17
|
async read(path) {
|
|
13
|
-
const res = await fetch(
|
|
18
|
+
const res = await fetch(await gameUrl(path));
|
|
14
19
|
return res.ok ? new Uint8Array(await res.arrayBuffer()) : undefined;
|
|
15
20
|
},
|
|
16
21
|
async readRange(path, offset, length) {
|
|
17
|
-
const res = await fetch(
|
|
22
|
+
const res = await fetch(await gameUrl(path), { headers: { Range: `bytes=${offset}-${offset + length - 1}` } });
|
|
18
23
|
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
19
24
|
// A server that ignores Range (Python's http.server) sends the whole file.
|
|
20
25
|
return res.status === 206 ? bytes : bytes.subarray(offset, offset + length);
|
|
21
26
|
},
|
|
22
27
|
async list(dir) {
|
|
23
28
|
if (import.meta.env.DEV) return (await fetch(`/__list?dir=${encodeURIComponent(dir)}`)).json();
|
|
24
|
-
|
|
25
|
-
return (
|
|
29
|
+
const files = (await staticListing())[dir];
|
|
30
|
+
return Array.isArray(files) ? files : [];
|
|
26
31
|
},
|
|
27
32
|
};
|
|
28
|
-
|
|
33
|
+
|
|
34
|
+
let listing: Promise<Record<string, unknown>> | undefined;
|
|
35
|
+
const staticListing = () => (listing ??= fetch(`${BASE}game/files.json`, { cache: "no-store" }).then((r): Promise<Record<string, unknown>> => (r.ok ? r.json() : Promise.resolve({}))));
|
|
36
|
+
|
|
37
|
+
/** A game file's URL: on a built site, with the build's version (see httpFiles). */
|
|
38
|
+
async function gameUrl(path: string): Promise<string> {
|
|
39
|
+
const url = `${BASE}game/${path}`;
|
|
40
|
+
if (import.meta.env.DEV) return url;
|
|
41
|
+
const version = (await staticListing()).version;
|
|
42
|
+
return typeof version === "string" ? `${url}?v=${encodeURIComponent(version)}` : url;
|
|
43
|
+
}
|
|
29
44
|
|
|
30
45
|
// --- Mods -------------------------------------------------------------------------------
|
|
31
46
|
|
package/package.json
CHANGED
package/tools/site/build.d.ts
CHANGED
|
@@ -6,3 +6,5 @@ export declare function buildSite(gameDir: string, outDir: string, options?: {
|
|
|
6
6
|
game: string;
|
|
7
7
|
soundFont: boolean;
|
|
8
8
|
}>;
|
|
9
|
+
/** A short hash of the game's files: changes whenever any of them does. */
|
|
10
|
+
export declare function buildVersion(files: Record<string, Uint8Array>): string;
|
package/tools/site/build.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
4
|
import { writeResourceArchive } from "../../packages/sci/src/index.js";
|
|
@@ -40,7 +41,8 @@ export async function buildSite(gameDir, outDir, options = {}) {
|
|
|
40
41
|
for (const [name, data] of Object.entries(files))
|
|
41
42
|
writeFileSync(join(play, "game", name), data);
|
|
42
43
|
// What the player lists in place of the dev server's directory listings.
|
|
43
|
-
|
|
44
|
+
// With the build's version, which the player fetches every file with (apps/viewer/src/files.ts).
|
|
45
|
+
writeFileSync(join(play, "game/files.json"), JSON.stringify({ "": Object.keys(files), version: buildVersion(files) }));
|
|
44
46
|
const sf2 = [join(resolve(gameDir), SOUNDFONT), join(ROOT, SOUNDFONT)].find((f) => existsSync(f));
|
|
45
47
|
const soundFont = sf2 !== undefined;
|
|
46
48
|
if (sf2)
|
|
@@ -110,3 +112,10 @@ ${body}
|
|
|
110
112
|
</html>
|
|
111
113
|
`;
|
|
112
114
|
}
|
|
115
|
+
/** A short hash of the game's files: changes whenever any of them does. */
|
|
116
|
+
export function buildVersion(files) {
|
|
117
|
+
const hash = createHash("sha256");
|
|
118
|
+
for (const name of Object.keys(files).sort())
|
|
119
|
+
hash.update(name).update(files[name]);
|
|
120
|
+
return hash.digest("hex").slice(0, 12);
|
|
121
|
+
}
|
package/tools/site/build.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
4
|
import { writeResourceArchive } from "../../packages/sci/src/index.ts";
|
|
@@ -46,7 +47,8 @@ export async function buildSite(
|
|
|
46
47
|
mkdirSync(join(play, "game"), { recursive: true });
|
|
47
48
|
for (const [name, data] of Object.entries(files)) writeFileSync(join(play, "game", name), data);
|
|
48
49
|
// What the player lists in place of the dev server's directory listings.
|
|
49
|
-
|
|
50
|
+
// With the build's version, which the player fetches every file with (apps/viewer/src/files.ts).
|
|
51
|
+
writeFileSync(join(play, "game/files.json"), JSON.stringify({ "": Object.keys(files), version: buildVersion(files) }));
|
|
50
52
|
const sf2 = [join(resolve(gameDir), SOUNDFONT), join(ROOT, SOUNDFONT)].find((f) => existsSync(f));
|
|
51
53
|
const soundFont = sf2 !== undefined;
|
|
52
54
|
if (sf2) cpSync(sf2, join(play, "soundfonts", basename(sf2)));
|
|
@@ -116,3 +118,10 @@ ${body}
|
|
|
116
118
|
</html>
|
|
117
119
|
`;
|
|
118
120
|
}
|
|
121
|
+
|
|
122
|
+
/** A short hash of the game's files: changes whenever any of them does. */
|
|
123
|
+
export function buildVersion(files: Record<string, Uint8Array>): string {
|
|
124
|
+
const hash = createHash("sha256");
|
|
125
|
+
for (const name of Object.keys(files).sort()) hash.update(name).update(files[name]!);
|
|
126
|
+
return hash.digest("hex").slice(0, 12);
|
|
127
|
+
}
|