@datamoc/mw_games 0.16.0 → 0.17.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 +162 -0
- package/README.md +21 -1
- package/dist/ai/lua.d.ts.map +1 -1
- package/dist/ai/lua.js +1 -0
- package/dist/ai/lua.js.map +1 -1
- package/dist/core/ActionJournal.d.ts +5 -0
- package/dist/core/ActionJournal.d.ts.map +1 -1
- package/dist/core/ActionJournal.js +12 -1
- package/dist/core/ActionJournal.js.map +1 -1
- package/dist/core/Clone.d.ts +31 -0
- package/dist/core/Clone.d.ts.map +1 -0
- package/dist/core/Clone.js +84 -0
- package/dist/core/Clone.js.map +1 -0
- package/dist/core/Collection.d.ts +1 -0
- package/dist/core/Collection.d.ts.map +1 -1
- package/dist/core/Collection.js +6 -2
- package/dist/core/Collection.js.map +1 -1
- package/dist/core/HttpTransport.d.ts +17 -0
- package/dist/core/HttpTransport.d.ts.map +1 -1
- package/dist/core/HttpTransport.js +61 -0
- package/dist/core/HttpTransport.js.map +1 -1
- package/dist/core/Multiplayer.d.ts +9 -0
- package/dist/core/Multiplayer.d.ts.map +1 -1
- package/dist/core/Multiplayer.js +48 -4
- package/dist/core/Multiplayer.js.map +1 -1
- package/dist/core/News.d.ts.map +1 -1
- package/dist/core/News.js +1 -2
- package/dist/core/News.js.map +1 -1
- package/dist/core/Sanitize.d.ts +19 -0
- package/dist/core/Sanitize.d.ts.map +1 -1
- package/dist/core/Sanitize.js +37 -0
- package/dist/core/Sanitize.js.map +1 -1
- package/dist/core/Save.d.ts +18 -9
- package/dist/core/Save.d.ts.map +1 -1
- package/dist/core/Save.js +37 -21
- package/dist/core/Save.js.map +1 -1
- package/dist/core/SaveSync.js +4 -4
- package/dist/core/SaveSync.js.map +1 -1
- package/dist/core/Settings.d.ts.map +1 -1
- package/dist/core/Settings.js +2 -1
- package/dist/core/Settings.js.map +1 -1
- package/dist/core/State.d.ts.map +1 -1
- package/dist/core/State.js +8 -7
- package/dist/core/State.js.map +1 -1
- package/dist/core/StoredValue.d.ts +1 -1
- package/dist/core/StoredValue.d.ts.map +1 -1
- package/dist/core/StoredValue.js +3 -2
- package/dist/core/StoredValue.js.map +1 -1
- package/dist/core/Telemetry.d.ts +19 -1
- package/dist/core/Telemetry.d.ts.map +1 -1
- package/dist/core/Telemetry.js +26 -1
- package/dist/core/Telemetry.js.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/mw_games.global.js +107 -107
- package/dist/mwl/MapFile.js +11 -1
- package/dist/mwl/MapFile.js.map +1 -1
- package/dist/mwl/expression.js +38 -5
- package/dist/mwl/expression.js.map +1 -1
- package/dist/mwl/fengari.d.ts +8 -0
- package/dist/mwl/fengari.d.ts.map +1 -1
- package/dist/mwl/fengari.js +72 -2
- package/dist/mwl/fengari.js.map +1 -1
- package/dist/mwl/persistence.d.ts.map +1 -1
- package/dist/mwl/persistence.js +2 -1
- package/dist/mwl/persistence.js.map +1 -1
- package/dist/mwl/runtime.d.ts.map +1 -1
- package/dist/mwl/runtime.js +6 -1
- package/dist/mwl/runtime.js.map +1 -1
- package/dist/rpg/Marshal.d.ts.map +1 -1
- package/dist/rpg/Marshal.js +13 -0
- package/dist/rpg/Marshal.js.map +1 -1
- package/dist/simulation/Campaign.d.ts.map +1 -1
- package/dist/simulation/Campaign.js +9 -5
- package/dist/simulation/Campaign.js.map +1 -1
- package/dist/simulation/Harness.d.ts.map +1 -1
- package/dist/simulation/Harness.js +2 -1
- package/dist/simulation/Harness.js.map +1 -1
- package/dist/simulation/Runtime.d.ts +10 -0
- package/dist/simulation/Runtime.d.ts.map +1 -1
- package/dist/simulation/Runtime.js +25 -4
- package/dist/simulation/Runtime.js.map +1 -1
- package/dist/testing/index.d.ts +105 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +134 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +90 -25
- package/sbom.cdx.json +5589 -0
- package/tools/benchmark-browser.d.mts +33 -0
- package/tools/benchmark-browser.mjs +163 -0
- package/tools/benchmark-history.d.mts +8 -0
- package/tools/benchmark-history.mjs +31 -0
- package/tools/browser-smoke.d.mts +30 -0
- package/tools/browser-smoke.mjs +167 -0
- package/tools/bundle-size.d.mts +11 -0
- package/tools/bundle-size.mjs +138 -0
- package/tools/csp.d.mts +7 -0
- package/tools/csp.mjs +65 -0
- package/tools/emit-page.d.mts +44 -0
- package/tools/emit-page.mjs +188 -0
- package/tools/extract-rgssad.d.mts +5 -0
- package/tools/extract-rgssad.mjs +64 -0
- package/tools/find-chrome.d.mts +7 -0
- package/tools/find-chrome.mjs +104 -0
- package/tools/i18n-edit.d.mts +29 -0
- package/tools/i18n-edit.mjs +748 -0
- package/tools/make-example-assets.d.mts +2 -0
- package/tools/make-example-assets.mjs +727 -0
- package/tools/multiplayer-server.d.mts +27 -0
- package/tools/multiplayer-server.mjs +195 -0
- package/tools/mwl.mjs +1 -0
- package/tools/sbom.d.mts +83 -0
- package/tools/sbom.mjs +332 -0
- package/tools/vendored/rgssad-wasm/LICENSE-MIT +21 -0
- package/tools/vendored/rgssad-wasm/README.md +15 -0
- package/tools/vendored/rgssad-wasm/package.json +17 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm.d.ts +41 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm.js +6 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm_bg.js +399 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm_bg.wasm +0 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm_bg.wasm.d.ts +17 -0
- package/tools/vite.d.mts +17 -0
- package/tools/vite.mjs +83 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { CompileResourcesGroup } from './compile-resources.mjs';
|
|
2
|
+
import type { CompressRow } from './compress-dist.mjs';
|
|
3
|
+
import type { SingleFileOptions, SingleFileResult } from './single-file.mjs';
|
|
4
|
+
|
|
5
|
+
export interface EmitPageOptions {
|
|
6
|
+
/** the vite output folder holding `index.html` */
|
|
7
|
+
dist: string;
|
|
8
|
+
/** the asset source folder compiled into `dist/assets/*.js`; omit for a game with no assets */
|
|
9
|
+
assets?: string;
|
|
10
|
+
/** maps an asset path to its script's name; defaults to one script per top-level folder */
|
|
11
|
+
groupBy?: (key: string) => string;
|
|
12
|
+
/** write `.gz`/`.br` siblings for a server to negotiate; default true */
|
|
13
|
+
compress?: boolean;
|
|
14
|
+
/** also write `.xz`, where the system `xz` binary exists */
|
|
15
|
+
xz?: boolean;
|
|
16
|
+
/** also write `standalone.html` with every script inlined */
|
|
17
|
+
singleFile?: boolean | Omit<SingleFileOptions, 'dist'>;
|
|
18
|
+
toWebp?: boolean;
|
|
19
|
+
webpLossless?: boolean;
|
|
20
|
+
webpQuality?: number;
|
|
21
|
+
/** the page's Content-Security-Policy: on by default, `false` to leave it out */
|
|
22
|
+
csp?: boolean | CspOptions;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CspOptions {
|
|
26
|
+
/** origins the game connects to (`NewsClient`, `TelemetryClient`, `LockstepClient`...) */
|
|
27
|
+
connect?: string[];
|
|
28
|
+
/** keep `'unsafe-eval'`, which Pixi 8 needs unless the game imports `pixi.js/unsafe-eval`; default true */
|
|
29
|
+
eval?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface EmitPageResult {
|
|
33
|
+
dist: string;
|
|
34
|
+
entry: string;
|
|
35
|
+
groups: CompileResourcesGroup[];
|
|
36
|
+
rawBytes: number;
|
|
37
|
+
embeddedBytes: number;
|
|
38
|
+
webpConverted: number;
|
|
39
|
+
compressed: CompressRow[] | null;
|
|
40
|
+
single: SingleFileResult | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** finishes a vite build as a page that opens from `file://` */
|
|
44
|
+
export function emitPage(options: EmitPageOptions): Promise<EmitPageResult>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, writeFile, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { join, resolve as resolvePath } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { compileResources } from './compile-resources.mjs';
|
|
7
|
+
import { compressDist } from './compress-dist.mjs';
|
|
8
|
+
import { toClassicScript } from './classic-html.mjs';
|
|
9
|
+
import { buildSingleFile } from './single-file.mjs';
|
|
10
|
+
import { withContentSecurityPolicy } from './csp.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Turns a vite build into a folder that opens by double-clicking.
|
|
14
|
+
*
|
|
15
|
+
* Two things have to change from what vite emits:
|
|
16
|
+
*
|
|
17
|
+
* - its entry tag is `<script type="module" crossorigin>`, and both attributes stop a
|
|
18
|
+
* `file://` page from running the script. The bundle is a plain IIFE, so the tag is
|
|
19
|
+
* rewritten as a classic script - with `defer`, because a module tag was implicitly
|
|
20
|
+
* deferred and a classic one is not: without it the scripts run from `<head>` before
|
|
21
|
+
* the body exists, and the game finds no canvas;
|
|
22
|
+
* - the assets have to be compiled into scripts and listed in the page, because a
|
|
23
|
+
* `file://` page cannot read a directory to discover them.
|
|
24
|
+
*
|
|
25
|
+
* Shipped as `@datamoc/mw_games/tools/emit-page` (`emitPage(options)`, the function every flag
|
|
26
|
+
* below maps onto) and as the `mwg-emit` command; `@datamoc/mw_games/tools/vite`'s `mwgPage()`
|
|
27
|
+
* plugin calls `emitPage` at the end of `vite build`, so a game needs neither by hand.
|
|
28
|
+
*
|
|
29
|
+
* usage: mwg-emit <project folder> [--dist=<dir>] [--assets=<dir>] [--no-csp] [--connect=<origin>]...
|
|
30
|
+
* [--no-compress] [--xz]
|
|
31
|
+
* [--single-file] [--single-file-compress[=level]]
|
|
32
|
+
* [--single-file-brotli[=quality]] [--to-webp] [--webp-lossy[=quality]]
|
|
33
|
+
*
|
|
34
|
+
* `--dist` defaults to `<project folder>/dist` and `--assets` to `<project folder>/assets`; with
|
|
35
|
+
* no assets folder at all, the page is rewritten and nothing is compiled.
|
|
36
|
+
*
|
|
37
|
+
* After the page is rewritten, precompressed `.gz`/`.br` siblings are written next to
|
|
38
|
+
* the text files (see `tools/compress-dist.mjs`): the `file://` page keeps loading the
|
|
39
|
+
* originals, while a server in front of the same folder can serve the smaller files.
|
|
40
|
+
* `--no-compress` (or `MWG_NO_COMPRESS=1`) skips that pass; `--xz` also writes `.xz`
|
|
41
|
+
* archives where the system `xz` binary exists.
|
|
42
|
+
*
|
|
43
|
+
* `--single-file` additionally writes `dist/standalone.html` (see `tools/single-file.mjs`):
|
|
44
|
+
* every script the page loads, inlined into one file with no sibling `.js` at all.
|
|
45
|
+
* `--single-file-compress[=level]` gzips each inlined script; `--single-file-brotli[=quality]`
|
|
46
|
+
* compresses with brotli instead (smaller, needs a newer browser - see `single-file.mjs`'s own
|
|
47
|
+
* doc comment), unpacked either way at load with the browser's own `DecompressionStream`. Both
|
|
48
|
+
* are additive - the regular multi-file `index.html` this function already writes is untouched
|
|
49
|
+
* - so the build-options matrix (multi-file/single-file, compressed/not, gzip/brotli) is a
|
|
50
|
+
* choice per build, not a fork of the pipeline.
|
|
51
|
+
*
|
|
52
|
+
* `--to-webp` converts `.png`/`.jpg`/`.jpeg` example assets to WebP before embedding (see
|
|
53
|
+
* `tools/webp-convert.mjs`), lossless by default; `--webp-lossy[=quality]` opts into lossy
|
|
54
|
+
* re-encoding instead. Needs the optional `sharp` devDependency installed.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Finishes a vite build in `dist` as a page that opens from `file://`: compiles `assets` into
|
|
59
|
+
* scripts under `dist/assets`, rewrites the module entry tag to a classic deferred one with the
|
|
60
|
+
* asset scripts before it, writes a Content-Security-Policy into the page (`csp`, see
|
|
61
|
+
* `tools/csp.mjs`; `false` leaves it out, `{ connect: [...] }` declares the endpoints a game
|
|
62
|
+
* talks to), removes vite's `.vite` metadata, then optionally precompresses and writes a
|
|
63
|
+
* single-file variant. Returns what it did, for a caller to report.
|
|
64
|
+
*/
|
|
65
|
+
export async function emitPage({
|
|
66
|
+
dist,
|
|
67
|
+
assets,
|
|
68
|
+
groupBy,
|
|
69
|
+
compress = true,
|
|
70
|
+
xz = false,
|
|
71
|
+
singleFile = false,
|
|
72
|
+
toWebp = false,
|
|
73
|
+
webpLossless = true,
|
|
74
|
+
webpQuality = 90,
|
|
75
|
+
csp = true,
|
|
76
|
+
}) {
|
|
77
|
+
if (!dist) throw new Error('emitPage needs the dist folder of a vite build');
|
|
78
|
+
const compiled = assets
|
|
79
|
+
? await compileResources({ from: assets, to: join(dist, 'assets'), groupBy, toWebp, webpLossless, webpQuality })
|
|
80
|
+
: { groups: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0 };
|
|
81
|
+
|
|
82
|
+
const html = await readFile(join(dist, 'index.html'), 'utf8');
|
|
83
|
+
const classic = toClassicScript(html);
|
|
84
|
+
if (!classic) {
|
|
85
|
+
throw new Error('no <script type="module"> entry tag found in the vite output - did the build succeed?');
|
|
86
|
+
}
|
|
87
|
+
const tag = `<script defer src="${classic.src}"></script>`;
|
|
88
|
+
const assetTags = compiled.groups.map((g) => `\t<script defer src="./assets/${g.name}.js"></script>`).join('\n');
|
|
89
|
+
//defer preserves document order, so the asset scripts still run before the game does
|
|
90
|
+
const page = assetTags ? classic.html.replace(tag, `${assetTags}\n\t${tag}`) : classic.html;
|
|
91
|
+
const cspOptions = csp === true ? {} : csp;
|
|
92
|
+
await writeFile(join(dist, 'index.html'), cspOptions ? withContentSecurityPolicy(page, cspOptions) : page, 'utf8');
|
|
93
|
+
|
|
94
|
+
//vite leaves a .vite folder of build metadata that the shipped folder does not need
|
|
95
|
+
await rm(join(dist, '.vite'), { recursive: true, force: true });
|
|
96
|
+
|
|
97
|
+
const compressed = compress ? await compressDist(dist, { xz }) : null;
|
|
98
|
+
const single = singleFile ? await buildSingleFile({ dist, ...(singleFile === true ? {} : singleFile) }) : null;
|
|
99
|
+
if (single && cspOptions) {
|
|
100
|
+
//a compressed standalone page unpacks each script and inserts it inline, so the scripts
|
|
101
|
+
//the multi-file page loads by src are allowed by hash as well
|
|
102
|
+
const sources = [...page.matchAll(/<script[^>]*\bsrc="\.\/([^"]+)"[^>]*><\/script>/g)].map((m) => m[1]);
|
|
103
|
+
const scripts = await Promise.all(sources.map((source) => readFile(join(dist, source), 'utf8')));
|
|
104
|
+
const standalone = await readFile(single.path, 'utf8');
|
|
105
|
+
await writeFile(single.path, withContentSecurityPolicy(standalone, { ...cspOptions, scripts }), 'utf8');
|
|
106
|
+
}
|
|
107
|
+
return { dist, entry: classic.src, ...compiled, compressed, single };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const kb = (n) => (n / 1024).toFixed(1) + ' KB';
|
|
111
|
+
|
|
112
|
+
async function main(argv) {
|
|
113
|
+
const folder = argv.find((arg) => !arg.startsWith('--'));
|
|
114
|
+
if (!folder) {
|
|
115
|
+
console.error('usage: mwg-emit <project folder> [--dist=<dir>] [--assets=<dir>] [options]');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
const option = (name) => argv.find((a) => a === `--${name}` || a.startsWith(`--${name}=`));
|
|
119
|
+
const value = (name) => option(name)?.split('=').slice(1).join('=') || undefined;
|
|
120
|
+
const root = resolvePath(folder);
|
|
121
|
+
const dist = resolvePath(value('dist') ?? join(root, 'dist'));
|
|
122
|
+
const assetsDir = resolvePath(value('assets') ?? join(root, 'assets'));
|
|
123
|
+
const hasAssets = (await stat(assetsDir).catch(() => null))?.isDirectory() ?? false;
|
|
124
|
+
|
|
125
|
+
const webpLossyFlag = option('webp-lossy');
|
|
126
|
+
const brotliFlag = option('single-file-brotli');
|
|
127
|
+
const compressFlag = option('single-file-compress');
|
|
128
|
+
const levelFlag = brotliFlag ?? compressFlag;
|
|
129
|
+
const singleFile = argv.includes('--single-file') && {
|
|
130
|
+
compress: Boolean(brotliFlag || compressFlag),
|
|
131
|
+
algorithm: brotliFlag ? 'brotli' : 'gzip',
|
|
132
|
+
level: levelFlag?.includes('=') ? Number(levelFlag.split('=')[1]) : undefined,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const result = await emitPage({
|
|
136
|
+
dist,
|
|
137
|
+
assets: hasAssets ? assetsDir : undefined,
|
|
138
|
+
//a flat assets folder becomes one script, whatever its subfolders
|
|
139
|
+
groupBy: () => 'assets',
|
|
140
|
+
compress: !(argv.includes('--no-compress') || process.env.MWG_NO_COMPRESS === '1'),
|
|
141
|
+
xz: argv.includes('--xz'),
|
|
142
|
+
singleFile,
|
|
143
|
+
toWebp: argv.includes('--to-webp'),
|
|
144
|
+
webpLossless: !webpLossyFlag,
|
|
145
|
+
webpQuality: webpLossyFlag?.includes('=') ? Number(webpLossyFlag.split('=')[1]) : 90,
|
|
146
|
+
csp: argv.includes('--no-csp')
|
|
147
|
+
? false
|
|
148
|
+
: { connect: argv.filter((a) => a.startsWith('--connect=')).map((a) => a.slice('--connect='.length)) },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
console.log(`\n ${dist}`);
|
|
152
|
+
console.log(
|
|
153
|
+
` index.html + ${result.entry} + ${result.groups.length} asset script(s), ${kb(result.rawBytes)} of assets`,
|
|
154
|
+
);
|
|
155
|
+
if (argv.includes('--to-webp')) {
|
|
156
|
+
console.log(
|
|
157
|
+
` ${result.webpConverted} image(s) converted to WebP (${webpLossyFlag ? 'lossy' : 'lossless'}), ` +
|
|
158
|
+
`${kb(result.rawBytes)} -> ${kb(result.embeddedBytes)} embedded`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
console.log(' open index.html directly - no server needed');
|
|
162
|
+
if (result.compressed) {
|
|
163
|
+
for (const row of result.compressed) {
|
|
164
|
+
const parts = [`${row.file}: ${kb(row.raw)}`];
|
|
165
|
+
if (row.gzip) parts.push(`gz ${kb(row.gzip)}`);
|
|
166
|
+
if (row.brotli) parts.push(`br ${kb(row.brotli)}`);
|
|
167
|
+
if (row.xz) parts.push(`xz ${kb(row.xz)}`);
|
|
168
|
+
console.log(' ' + parts.join(', '));
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
console.log(' (compression skipped)');
|
|
172
|
+
}
|
|
173
|
+
if (result.single) {
|
|
174
|
+
const { algorithm, level } = singleFile;
|
|
175
|
+
console.log(
|
|
176
|
+
`\n ${result.single.path}`,
|
|
177
|
+
`\n ${result.single.scripts} script(s) inlined, ${kb(result.single.rawBytes)} raw -> ${kb(result.single.embeddedBytes)} embedded` +
|
|
178
|
+
(singleFile.compress
|
|
179
|
+
? ` (${algorithm} level ${level ?? (algorithm === 'brotli' ? 11 : 9)})`
|
|
180
|
+
: ' (uncompressed)'),
|
|
181
|
+
);
|
|
182
|
+
console.log(' one file, no server, no sibling script - open it directly');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
//realpath, since an npm `bin` shim reaches this file through a symlink
|
|
187
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url))
|
|
188
|
+
await main(process.argv.slice(2));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** where an archive entry lands under `outDir`; throws when its name would escape `outDir` */
|
|
2
|
+
export function entryPath(outDir: string, fileName: string): string;
|
|
3
|
+
|
|
4
|
+
/** extracts every entry of an RPG Maker XP/VX `.rgssad` archive under `outDir`; resolves to the file count */
|
|
5
|
+
export function extractRgssad(archivePath: string, outDir: string): Promise<number>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import * as glue from './vendored/rgssad-wasm/rgssad_wasm_bg.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extracts an RPG Maker XP/VX `.rgssad` archive to plain files, through the vendored
|
|
9
|
+
* `rgssad-wasm` decoder (MIT, see `vendored/rgssad-wasm/README.md`). A format tool, for someone
|
|
10
|
+
* porting a game whose archive they own; nothing a player receives. Shipped as
|
|
11
|
+
* `@datamoc/mw_games/tools/extract-rgssad` and `mwg-extract-rgssad <archive> <outDir>` (item 389).
|
|
12
|
+
*
|
|
13
|
+
* Entry names come from the archive, so they are untrusted: each is normalised (RPG Maker writes
|
|
14
|
+
* `\\` separators) and refused unless it resolves inside `outDir`, the check that stops an entry
|
|
15
|
+
* named `..\\..\\anything` from writing outside the folder asked for.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Resolved relative to this script's own location, not the caller's cwd.
|
|
19
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
let ready = null;
|
|
21
|
+
|
|
22
|
+
function decoder() {
|
|
23
|
+
ready ??= WebAssembly.instantiate(fs.readFileSync(path.join(here, 'vendored/rgssad-wasm/rgssad_wasm_bg.wasm')), {
|
|
24
|
+
'./rgssad_wasm_bg.js': glue,
|
|
25
|
+
}).then(({ instance }) => {
|
|
26
|
+
glue.__wbg_set_wasm(instance.exports);
|
|
27
|
+
if (instance.exports.__wbindgen_start) instance.exports.__wbindgen_start();
|
|
28
|
+
});
|
|
29
|
+
return ready;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** where an archive entry lands under `outDir`, or a thrown error when its name escapes it */
|
|
33
|
+
export function entryPath(outDir, fileName) {
|
|
34
|
+
const root = path.resolve(outDir);
|
|
35
|
+
const target = path.resolve(root, fileName.replace(/\\/g, '/'));
|
|
36
|
+
if (target !== root && !target.startsWith(root + path.sep))
|
|
37
|
+
throw new Error(`rgssad entry "${fileName}" would be written outside ${root}`);
|
|
38
|
+
return target;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** extracts every entry of `archivePath` under `outDir`; returns how many files were written */
|
|
42
|
+
export async function extractRgssad(archivePath, outDir) {
|
|
43
|
+
await decoder();
|
|
44
|
+
const data = fs.readFileSync(archivePath);
|
|
45
|
+
const reader = new glue.Reader(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
|
|
46
|
+
let count = 0;
|
|
47
|
+
for (let entry = reader.readEntry(); entry != null; entry = reader.readEntry()) {
|
|
48
|
+
const outPath = entryPath(outDir, entry.fileName);
|
|
49
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
50
|
+
fs.writeFileSync(outPath, Buffer.from(entry.data));
|
|
51
|
+
count++;
|
|
52
|
+
}
|
|
53
|
+
return count;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//realpath, since an npm `bin` shim reaches this file through a symlink
|
|
57
|
+
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
58
|
+
const [, , archivePath, outDir] = process.argv;
|
|
59
|
+
if (!archivePath || !outDir) {
|
|
60
|
+
console.error('usage: mwg-extract-rgssad <archive.rgssad> <outDir>');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
console.log(`Extracted ${await extractRgssad(archivePath, outDir)} files to ${outDir}`);
|
|
64
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Browser } from 'playwright-core';
|
|
2
|
+
|
|
3
|
+
/** the Chrome or Chromium to drive: the platform's usual install, then Playwright's own, then PATH */
|
|
4
|
+
export function findChrome(): Promise<string>;
|
|
5
|
+
|
|
6
|
+
/** headless Chrome set up for `file://` pages; `CHROME_PATH` names the executable, `argsEnv` names the variable holding extra flags */
|
|
7
|
+
export function launchChrome(options?: { argsEnv?: string }): Promise<Browser>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { access, readdir } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `playwright-core`, loaded on first use: it is an optional peer of the published package, so a
|
|
7
|
+
* game that never runs a browser check does not need it installed.
|
|
8
|
+
*/
|
|
9
|
+
async function playwright() {
|
|
10
|
+
try {
|
|
11
|
+
return await import('playwright-core');
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error('the browser checks need playwright-core: npm install --save-dev playwright-core');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The browser to drive: the platform's usual Chrome (or, on Windows, Edge), then a Chromium that
|
|
19
|
+
* Playwright itself installed (`PLAYWRIGHT_BROWSERS_PATH`, common in CI images and containers),
|
|
20
|
+
* then whatever is on PATH. `CHROME_PATH` overrides all of them in `launchChrome`.
|
|
21
|
+
*/
|
|
22
|
+
export async function findChrome() {
|
|
23
|
+
const candidates =
|
|
24
|
+
process.platform === 'win32'
|
|
25
|
+
? [
|
|
26
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
27
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
28
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
29
|
+
]
|
|
30
|
+
: process.platform === 'darwin'
|
|
31
|
+
? [
|
|
32
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
33
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
34
|
+
]
|
|
35
|
+
: ['/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser'];
|
|
36
|
+
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
try {
|
|
39
|
+
await access(candidate);
|
|
40
|
+
return candidate;
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
const bundled = await playwrightChromium();
|
|
44
|
+
if (bundled) return bundled;
|
|
45
|
+
return process.platform === 'win32' ? 'chrome.exe' : 'google-chrome';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The newest Chromium under Playwright's browsers folder, whatever revision it is: the one
|
|
50
|
+
* `playwright-core` would pick is often not the one a CI image or container installed.
|
|
51
|
+
*/
|
|
52
|
+
async function playwrightChromium() {
|
|
53
|
+
const roots = [
|
|
54
|
+
process.env.PLAYWRIGHT_BROWSERS_PATH,
|
|
55
|
+
process.platform === 'win32'
|
|
56
|
+
? join(process.env.LOCALAPPDATA ?? join(homedir(), 'AppData', 'Local'), 'ms-playwright')
|
|
57
|
+
: process.platform === 'darwin'
|
|
58
|
+
? join(homedir(), 'Library', 'Caches', 'ms-playwright')
|
|
59
|
+
: join(homedir(), '.cache', 'ms-playwright'),
|
|
60
|
+
].filter(Boolean);
|
|
61
|
+
const inside = [
|
|
62
|
+
'chrome-linux64/chrome',
|
|
63
|
+
'chrome-linux/chrome',
|
|
64
|
+
'chrome-win64/chrome.exe',
|
|
65
|
+
'chrome-win/chrome.exe',
|
|
66
|
+
'chrome-mac-arm64/Chromium.app/Contents/MacOS/Chromium',
|
|
67
|
+
'chrome-mac/Chromium.app/Contents/MacOS/Chromium',
|
|
68
|
+
];
|
|
69
|
+
for (const root of roots) {
|
|
70
|
+
const revisions = (await readdir(root).catch(() => []))
|
|
71
|
+
.filter((name) => /^chromium-\d+$/.test(name))
|
|
72
|
+
.sort((a, b) => Number(b.slice(9)) - Number(a.slice(9)));
|
|
73
|
+
for (const revision of revisions)
|
|
74
|
+
for (const path of inside) {
|
|
75
|
+
const candidate = join(root, revision, path);
|
|
76
|
+
if (
|
|
77
|
+
await access(candidate).then(
|
|
78
|
+
() => true,
|
|
79
|
+
() => false,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
return candidate;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Launches headless Chrome for a page loaded from `file://`, the way every tool here wants it:
|
|
90
|
+
* the same discovery, `--allow-file-access-from-files`, and machine-specific flags read from
|
|
91
|
+
* `argsEnv` (a runner with no GPU passes `--use-gl=angle --use-angle=swiftshader` that way,
|
|
92
|
+
* since that is a property of the machine rather than of the tool). Each caller keeps its own
|
|
93
|
+
* page setup and assertions; this is the launch they all had in common.
|
|
94
|
+
*/
|
|
95
|
+
export async function launchChrome({ argsEnv = 'MWG_BENCHMARK_CHROME_ARGS' } = {}) {
|
|
96
|
+
const executablePath = process.env.CHROME_PATH ?? (await findChrome());
|
|
97
|
+
const extraArgs = (process.env[argsEnv] ?? '').split(' ').filter(Boolean);
|
|
98
|
+
const { chromium } = await playwright();
|
|
99
|
+
return chromium.launch({
|
|
100
|
+
executablePath,
|
|
101
|
+
headless: true,
|
|
102
|
+
args: ['--allow-file-access-from-files', ...extraArgs],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Catalog } from '@datamoc/mw_games/i18n';
|
|
2
|
+
import type { MarkdownSpan } from '@datamoc/mw_games/two-d/ui';
|
|
3
|
+
|
|
4
|
+
export interface LoadedCatalogFile {
|
|
5
|
+
catalog: Catalog;
|
|
6
|
+
format: 'json' | 'ftl';
|
|
7
|
+
typography: boolean | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SerializedTarget {
|
|
11
|
+
text: string;
|
|
12
|
+
flattened: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadCatalogFile(path: string): Promise<LoadedCatalogFile>;
|
|
16
|
+
export function serializeTarget(targetPath: string, catalog: Catalog): SerializedTarget;
|
|
17
|
+
export function spansToAnsi(spans: readonly MarkdownSpan[]): string;
|
|
18
|
+
export function truncateAnsi(ansi: string, width: number): string;
|
|
19
|
+
export function placeholderSummary(baseText: string, targetText: string): string;
|
|
20
|
+
export function expectedPlaceholders(baseText: string): string;
|
|
21
|
+
|
|
22
|
+
export interface PlayerCandidate {
|
|
23
|
+
cmd: string;
|
|
24
|
+
args: string[];
|
|
25
|
+
shell?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function playerCandidates(cuePath: string, platform?: NodeJS.Platform): PlayerCandidate[];
|
|
29
|
+
export function main(argv?: string[]): Promise<void>;
|