@datamoc/mw_games 0.38.0 → 0.40.0
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 +75 -0
- package/api-surface.json +500 -2
- package/data/microgm-opt.PROVENANCE.md +20 -0
- package/data/microgm-opt.sf2 +0 -0
- package/dist/assets/SvgParts.d.ts +31 -0
- package/dist/assets/SvgParts.d.ts.map +1 -0
- package/dist/assets/SvgParts.js +54 -0
- package/dist/assets/SvgParts.js.map +1 -0
- package/dist/assets/SvgSource.d.ts +3 -0
- package/dist/assets/SvgSource.d.ts.map +1 -0
- package/dist/assets/SvgSource.js +22 -0
- package/dist/assets/SvgSource.js.map +1 -0
- package/dist/assets/index.d.ts +2 -0
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +1 -0
- package/dist/assets/index.js.map +1 -1
- package/dist/assets/svg.d.ts.map +1 -1
- package/dist/assets/svg.js +9 -5
- package/dist/assets/svg.js.map +1 -1
- package/dist/mw_games.global.js +110 -110
- package/dist/mwl/presentation.d.ts.map +1 -1
- package/dist/mwl/presentation.js +25 -18
- package/dist/mwl/presentation.js.map +1 -1
- package/dist/physics/PhysicsWorld.d.ts +49 -17
- package/dist/physics/PhysicsWorld.d.ts.map +1 -1
- package/dist/physics/PhysicsWorld.js +297 -59
- package/dist/physics/PhysicsWorld.js.map +1 -1
- package/dist/physics/XpbdWorld.d.ts +1 -1
- package/dist/physics/XpbdWorld.js +1 -1
- package/dist/physics/cloth/index.d.ts +98 -0
- package/dist/physics/cloth/index.d.ts.map +1 -0
- package/dist/physics/cloth/index.js +207 -0
- package/dist/physics/cloth/index.js.map +1 -0
- package/dist/physics/index.d.ts +4 -0
- package/dist/physics/index.d.ts.map +1 -1
- package/dist/physics/index.js +2 -0
- package/dist/physics/index.js.map +1 -1
- package/dist/physics/spring/index.d.ts +90 -0
- package/dist/physics/spring/index.d.ts.map +1 -0
- package/dist/physics/spring/index.js +97 -0
- package/dist/physics/spring/index.js.map +1 -0
- package/dist/skeleton/index.d.ts +94 -13
- package/dist/skeleton/index.d.ts.map +1 -1
- package/dist/skeleton/index.js +192 -73
- package/dist/skeleton/index.js.map +1 -1
- package/dist/two-d/presentation/MwlPresentation.d.ts +12 -0
- package/dist/two-d/presentation/MwlPresentation.d.ts.map +1 -1
- package/dist/two-d/presentation/MwlPresentation.js +204 -153
- package/dist/two-d/presentation/MwlPresentation.js.map +1 -1
- package/dist/two-d/render/ClothSprite.d.ts +83 -0
- package/dist/two-d/render/ClothSprite.d.ts.map +1 -0
- package/dist/two-d/render/ClothSprite.js +105 -0
- package/dist/two-d/render/ClothSprite.js.map +1 -0
- package/dist/two-d/render/HazeLayer.d.ts +112 -0
- package/dist/two-d/render/HazeLayer.d.ts.map +1 -0
- package/dist/two-d/render/HazeLayer.js +185 -0
- package/dist/two-d/render/HazeLayer.js.map +1 -0
- package/dist/two-d/render/NeonGlow.d.ts +100 -0
- package/dist/two-d/render/NeonGlow.d.ts.map +1 -0
- package/dist/two-d/render/NeonGlow.js +183 -0
- package/dist/two-d/render/NeonGlow.js.map +1 -0
- package/dist/two-d/render/SkeletalSprite.d.ts +5 -2
- package/dist/two-d/render/SkeletalSprite.d.ts.map +1 -1
- package/dist/two-d/render/SkeletalSprite.js +27 -3
- package/dist/two-d/render/SkeletalSprite.js.map +1 -1
- package/dist/two-d/render/SpringSprite.d.ts +40 -0
- package/dist/two-d/render/SpringSprite.d.ts.map +1 -0
- package/dist/two-d/render/SpringSprite.js +39 -0
- package/dist/two-d/render/SpringSprite.js.map +1 -0
- package/dist/two-d/render/SpriteDeform.d.ts +156 -0
- package/dist/two-d/render/SpriteDeform.d.ts.map +1 -0
- package/dist/two-d/render/SpriteDeform.js +278 -0
- package/dist/two-d/render/SpriteDeform.js.map +1 -0
- package/dist/two-d/render/SvgSkeleton.d.ts +54 -0
- package/dist/two-d/render/SvgSkeleton.d.ts.map +1 -0
- package/dist/two-d/render/SvgSkeleton.js +112 -0
- package/dist/two-d/render/SvgSkeleton.js.map +1 -0
- package/dist/two-d/render/index.d.ts +12 -0
- package/dist/two-d/render/index.d.ts.map +1 -1
- package/dist/two-d/render/index.js +6 -0
- package/dist/two-d/render/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -7
- package/sbom.cdx.json +6 -58
- package/tools/asset-report.d.mts +37 -0
- package/tools/asset-report.mjs +157 -0
- package/tools/compile-resources.d.mts +24 -0
- package/tools/compile-resources.mjs +138 -5
- package/tools/emit-page.d.mts +9 -1
- package/tools/emit-page.mjs +31 -3
- package/tools/vite.mjs +6 -1
|
@@ -7,9 +7,15 @@ export interface CompileResourcesOptions {
|
|
|
7
7
|
to: string;
|
|
8
8
|
mime?: Record<string, string>;
|
|
9
9
|
groupBy?: (key: string) => string;
|
|
10
|
+
/** Exact paths relative to the asset folder to embed. Omit to include all; [] includes none. Unknown entries fail by name. */
|
|
11
|
+
assetFiles?: readonly string[];
|
|
10
12
|
toWebp?: boolean;
|
|
11
13
|
webpLossless?: boolean;
|
|
12
14
|
webpQuality?: number;
|
|
15
|
+
/** Subset an embedded `.sf2` to the programs the embedded `.mid`/`.midi` files use. Default true; `false` embeds every SoundFont whole. */
|
|
16
|
+
soundFont?: boolean;
|
|
17
|
+
/** Explicit `{ bank, program }` list to subset a SoundFont to, overriding the MIDI-derived list. */
|
|
18
|
+
soundFontUsage?: readonly SoundFontUsage[];
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export interface CompileResourcesGroup {
|
|
@@ -26,9 +32,27 @@ export interface CompileResourcesResult {
|
|
|
26
32
|
skipped: number;
|
|
27
33
|
/** the `CREDITS.txt` text when the asset folder holds a `credits.json`, else null */
|
|
28
34
|
credits: string | null;
|
|
35
|
+
/** one entry per embedded `.sf2`: the programs kept (null when embedded whole) and the byte saving */
|
|
36
|
+
soundFonts: SoundFontEmbed[];
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
/** the plain-text credits for a manifest (an array, or `{ credits: [...] }`, of `{ name, license, author?, url? }`) */
|
|
32
40
|
export function creditsText(manifest: unknown): string;
|
|
33
41
|
|
|
42
|
+
export interface SoundFontUsage {
|
|
43
|
+
bank: number;
|
|
44
|
+
program: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SoundFontEmbed {
|
|
48
|
+
key: string;
|
|
49
|
+
/** programs kept, or null when the font was embedded whole for lack of a usage list */
|
|
50
|
+
programs: number | null;
|
|
51
|
+
rawBytes: number;
|
|
52
|
+
embeddedBytes: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** parses a `--soundfont-usage=bank:program,...` CLI value; undefined when the flag is absent */
|
|
56
|
+
export function soundFontUsageFromFlag(value: string | undefined): SoundFontUsage[] | undefined;
|
|
57
|
+
|
|
34
58
|
export function compileResources(options: CompileResourcesOptions): Promise<CompileResourcesResult>;
|
|
@@ -38,8 +38,56 @@ export const DEFAULT_MIME = {
|
|
|
38
38
|
'.woff2': 'font/woff2',
|
|
39
39
|
'.json': 'application/json',
|
|
40
40
|
'.txt': 'text/plain',
|
|
41
|
+
'.mid': 'audio/midi',
|
|
42
|
+
'.midi': 'audio/midi',
|
|
43
|
+
'.sf2': 'application/octet-stream',
|
|
41
44
|
};
|
|
42
45
|
|
|
46
|
+
const isSoundFontKey = (key) => key.toLowerCase().endsWith('.sf2');
|
|
47
|
+
const isMidiKey = (key) => /\.(mid|midi)$/i.test(key);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The SoundFont/MIDI implementation lives in the library, not in this tool: in this
|
|
51
|
+
* repository it is the TypeScript source, in an installed package it is the built
|
|
52
|
+
* `dist/` the source compiles to. Only called when a build actually embeds a
|
|
53
|
+
* SoundFont it can subset, so a game without one never pays for the import.
|
|
54
|
+
*/
|
|
55
|
+
async function loadSoundFontTools() {
|
|
56
|
+
try {
|
|
57
|
+
const [soundFont, midi] = await Promise.all([
|
|
58
|
+
import('../src/audio/SoundFont.ts'),
|
|
59
|
+
import('../src/audio/Midi.ts'),
|
|
60
|
+
]);
|
|
61
|
+
return { ...soundFont, ...midi };
|
|
62
|
+
} catch {
|
|
63
|
+
return import('../dist/audio/index.js');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function checkedSoundFontUsage(soundFontUsage) {
|
|
68
|
+
if (soundFontUsage === undefined) return undefined;
|
|
69
|
+
if (!Array.isArray(soundFontUsage)) throw new Error('soundFontUsage must be an array of { bank, program }');
|
|
70
|
+
return soundFontUsage.map((entry, index) => {
|
|
71
|
+
const { bank, program } = entry ?? {};
|
|
72
|
+
if (!Number.isInteger(bank) || bank < 0 || !Number.isInteger(program) || program < 0 || program > 127) {
|
|
73
|
+
throw new Error(`soundFontUsage[${index}] needs an integer bank >= 0 and a program between 0 and 127`);
|
|
74
|
+
}
|
|
75
|
+
return { bank, program };
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** parses a `--soundfont-usage=bank:program,...` CLI value; undefined when the flag is absent */
|
|
80
|
+
export function soundFontUsageFromFlag(value) {
|
|
81
|
+
if (value === undefined) return undefined;
|
|
82
|
+
return value
|
|
83
|
+
.split(',')
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.map((pair) => {
|
|
86
|
+
const [bank, program] = pair.split(':').map(Number);
|
|
87
|
+
return { bank, program };
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
43
91
|
export async function* walk(dir) {
|
|
44
92
|
let entries;
|
|
45
93
|
try {
|
|
@@ -58,6 +106,9 @@ export async function* walk(dir) {
|
|
|
58
106
|
* @param options.from folder holding the assets
|
|
59
107
|
* @param options.to folder to write the generated scripts into
|
|
60
108
|
* @param options.mime extension to MIME type, merged over the defaults
|
|
109
|
+
* @param options.assetFiles exact paths relative to `from` to embed; omit for all, [] for none.
|
|
110
|
+
* Unknown paths and explicitly selected unsupported formats fail before replacing output.
|
|
111
|
+
* credits.json is converted to CREDITS.txt only when selected (or when no list is given).
|
|
61
112
|
* @param options.groupBy how to name the script an asset lands in; defaults to its top folder
|
|
62
113
|
* @param options.toWebp convert `.png`/`.jpg`/`.jpeg` assets to WebP before embedding
|
|
63
114
|
* (default false - see `tools/webp-convert.mjs`). The asset's *key* never changes (a game
|
|
@@ -69,6 +120,10 @@ export async function* walk(dir) {
|
|
|
69
120
|
* pixel-art tileset's exact colours survive; set false only with `webpQuality` to opt into
|
|
70
121
|
* lossy re-encoding for photographic source art)
|
|
71
122
|
* @param options.webpQuality 0-100, used only when `webpLossless` is false (default 90)
|
|
123
|
+
* @param options.soundFont subset an embedded `.sf2` to the programs the embedded
|
|
124
|
+
* `.mid`/`.midi` files use (default true); `false` embeds every SoundFont whole
|
|
125
|
+
* @param options.soundFontUsage explicit `{ bank, program }` list to subset to, for a
|
|
126
|
+
* game that voices notes no shipped MIDI file requests; overrides the MIDI-derived list
|
|
72
127
|
* @returns the group names, in the order the page should load them
|
|
73
128
|
*
|
|
74
129
|
* An `assets/credits.json` (an array, or `{ "credits": [...] }`, of `{ name, license, author?, url? }`)
|
|
@@ -81,15 +136,66 @@ export async function compileResources({
|
|
|
81
136
|
to,
|
|
82
137
|
mime = {},
|
|
83
138
|
groupBy,
|
|
139
|
+
assetFiles,
|
|
84
140
|
toWebp: convertToWebp = false,
|
|
85
141
|
webpLossless = true,
|
|
86
142
|
webpQuality = 90,
|
|
143
|
+
soundFont = true,
|
|
144
|
+
soundFontUsage,
|
|
87
145
|
} = {}) {
|
|
88
146
|
if (!from || !to) throw new Error('compileResources needs both `from` and `to`');
|
|
89
147
|
|
|
90
148
|
const types = { ...DEFAULT_MIME, ...mime };
|
|
91
149
|
const group = groupBy ?? ((key) => (key.includes('/') ? key.slice(0, key.indexOf('/')) : 'assets'));
|
|
92
150
|
|
|
151
|
+
const files = [];
|
|
152
|
+
for await (const file of walk(from)) files.push(file);
|
|
153
|
+
const keyOf = (file) => relative(from, file).split(sep).join('/');
|
|
154
|
+
const selected =
|
|
155
|
+
assetFiles === undefined
|
|
156
|
+
? undefined
|
|
157
|
+
: new Set(assetFiles.map((key) => key.replaceAll('\\', '/').replace(/^\.\//, '')));
|
|
158
|
+
if (selected) {
|
|
159
|
+
const known = new Set(files.map(keyOf));
|
|
160
|
+
for (const key of selected) {
|
|
161
|
+
if (!known.has(key)) throw new Error(`unknown asset file ${key}`);
|
|
162
|
+
if (key !== 'credits.json' && !types[extname(key).toLowerCase()])
|
|
163
|
+
throw new Error(`unsupported asset file ${key}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// A shipped SoundFont carries only the programs the game's shipped MIDI files
|
|
168
|
+
// request: the full bank - even a small one - is mostly
|
|
169
|
+
// instruments no track plays. Usage is resolved before any output is replaced,
|
|
170
|
+
// so an unreadable MIDI or SoundFont fails the build with the old output intact.
|
|
171
|
+
// With no shipped MIDI and no explicit `soundFontUsage` the usage is unknowable
|
|
172
|
+
// (a game may voice notes it generates), so the font embeds whole.
|
|
173
|
+
const isSelected = (file) => !selected || selected.has(keyOf(file));
|
|
174
|
+
const explicitUsage = checkedSoundFontUsage(soundFontUsage);
|
|
175
|
+
const embedsSoundFont =
|
|
176
|
+
soundFont !== false && files.some((file) => isSelected(file) && isSoundFontKey(keyOf(file)));
|
|
177
|
+
let soundFontTools = null;
|
|
178
|
+
let usage = explicitUsage;
|
|
179
|
+
if (embedsSoundFont && usage === undefined) {
|
|
180
|
+
const midiFiles = files.filter((file) => isSelected(file) && isMidiKey(keyOf(file)));
|
|
181
|
+
if (midiFiles.length > 0) {
|
|
182
|
+
soundFontTools = await loadSoundFontTools();
|
|
183
|
+
const parsed = [];
|
|
184
|
+
for (const file of midiFiles) {
|
|
185
|
+
try {
|
|
186
|
+
parsed.push(soundFontTools.parseMidi(await readFile(file)));
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`cannot read MIDI asset ${keyOf(file)} to subset a SoundFont: ${error.message ?? error}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
usage = soundFontTools.collectSoundFontUsage(parsed);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (embedsSoundFont && usage !== undefined && !soundFontTools) soundFontTools = await loadSoundFontTools();
|
|
197
|
+
const soundFonts = [];
|
|
198
|
+
|
|
93
199
|
await rm(to, { recursive: true, force: true });
|
|
94
200
|
await mkdir(to, { recursive: true });
|
|
95
201
|
|
|
@@ -106,7 +212,8 @@ export async function compileResources({
|
|
|
106
212
|
const creditsFile = join(from, 'credits.json');
|
|
107
213
|
let credits = null;
|
|
108
214
|
|
|
109
|
-
for
|
|
215
|
+
for (const file of files) {
|
|
216
|
+
if (selected && !selected.has(keyOf(file))) continue;
|
|
110
217
|
if (file === creditsFile) {
|
|
111
218
|
credits = creditsText(JSON.parse(await readFile(file, 'utf8')));
|
|
112
219
|
continue;
|
|
@@ -118,7 +225,7 @@ export async function compileResources({
|
|
|
118
225
|
continue;
|
|
119
226
|
}
|
|
120
227
|
|
|
121
|
-
const key =
|
|
228
|
+
const key = keyOf(file);
|
|
122
229
|
const data = await readFile(file);
|
|
123
230
|
|
|
124
231
|
let mimeType = type;
|
|
@@ -132,6 +239,19 @@ export async function compileResources({
|
|
|
132
239
|
}
|
|
133
240
|
}
|
|
134
241
|
|
|
242
|
+
if (isSoundFontKey(key)) {
|
|
243
|
+
if (usage !== undefined && soundFontTools) {
|
|
244
|
+
try {
|
|
245
|
+
bytes = Buffer.from(soundFontTools.subsetSoundFont(data, usage));
|
|
246
|
+
} catch (error) {
|
|
247
|
+
throw new Error(`cannot subset SoundFont asset ${key}: ${error.message ?? error}`);
|
|
248
|
+
}
|
|
249
|
+
soundFonts.push({ key, programs: usage.length, rawBytes: data.length, embeddedBytes: bytes.length });
|
|
250
|
+
} else {
|
|
251
|
+
soundFonts.push({ key, programs: null, rawBytes: data.length, embeddedBytes: data.length });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
135
255
|
const name = group(key);
|
|
136
256
|
if (!groups.has(name)) groups.set(name, {});
|
|
137
257
|
groups.get(name)[key] = `data:${mimeType};base64,${bytes.toString('base64')}`;
|
|
@@ -152,7 +272,7 @@ export async function compileResources({
|
|
|
152
272
|
|
|
153
273
|
if (credits) await writeFile(join(to, 'CREDITS.txt'), credits, 'utf8');
|
|
154
274
|
|
|
155
|
-
return { groups: written, rawBytes, embeddedBytes, webpConverted, skipped, credits };
|
|
275
|
+
return { groups: written, rawBytes, embeddedBytes, webpConverted, skipped, credits, soundFonts };
|
|
156
276
|
}
|
|
157
277
|
|
|
158
278
|
/** the plain-text credits for a manifest, one block per entry; throws on an entry that cannot be attributed */
|
|
@@ -181,7 +301,7 @@ async function runCli() {
|
|
|
181
301
|
const [from, to] = args.filter((a) => !a.startsWith('--'));
|
|
182
302
|
if (!from || !to) {
|
|
183
303
|
console.error(
|
|
184
|
-
'usage: node compile-resources.mjs <asset folder> <output folder> [--to-webp] [--webp-lossy[=quality]]',
|
|
304
|
+
'usage: node compile-resources.mjs <asset folder> <output folder> [--to-webp] [--webp-lossy[=quality]] [--no-soundfont-subset] [--soundfont-usage=bank:program,...]',
|
|
185
305
|
);
|
|
186
306
|
process.exit(1);
|
|
187
307
|
}
|
|
@@ -190,7 +310,11 @@ async function runCli() {
|
|
|
190
310
|
const webpLossless = !lossyFlag;
|
|
191
311
|
const webpQuality = lossyFlag && lossyFlag.includes('=') ? Number(lossyFlag.split('=')[1]) : 90;
|
|
192
312
|
|
|
193
|
-
const
|
|
313
|
+
const soundFontUsage = soundFontUsageFromFlag(
|
|
314
|
+
args.find((a) => a.startsWith('--soundfont-usage='))?.slice('--soundfont-usage='.length),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const { groups, rawBytes, embeddedBytes, webpConverted, skipped, soundFonts } = await compileResources({
|
|
194
318
|
from,
|
|
195
319
|
to,
|
|
196
320
|
//a flat folder has no subdirectories to group by, so everything lands in one file
|
|
@@ -198,6 +322,8 @@ async function runCli() {
|
|
|
198
322
|
toWebp,
|
|
199
323
|
webpLossless,
|
|
200
324
|
webpQuality,
|
|
325
|
+
soundFont: !args.includes('--no-soundfont-subset'),
|
|
326
|
+
soundFontUsage,
|
|
201
327
|
});
|
|
202
328
|
|
|
203
329
|
const kb = (n) => (n / 1024).toFixed(1).padStart(8) + ' KB';
|
|
@@ -205,6 +331,13 @@ async function runCli() {
|
|
|
205
331
|
console.log(` ${join(relative(process.cwd(), to), name + '.js').padEnd(40)}${kb(size)} (${count} files)`);
|
|
206
332
|
}
|
|
207
333
|
console.log(` ${groups.length} script(s), ${kb(rawBytes)} of assets` + (skipped ? `, ${skipped} skipped` : ''));
|
|
334
|
+
for (const font of soundFonts) {
|
|
335
|
+
console.log(
|
|
336
|
+
font.programs === null
|
|
337
|
+
? ` ${font.key}: SoundFont embedded whole (${kb(font.rawBytes)}), no shipped MIDI to subset against`
|
|
338
|
+
: ` ${font.key}: SoundFont subset to ${font.programs} program(s), ${kb(font.rawBytes)} -> ${kb(font.embeddedBytes)}`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
208
341
|
if (toWebp) {
|
|
209
342
|
console.log(
|
|
210
343
|
` ${webpConverted} image(s) converted to WebP (${webpLossless ? 'lossless' : `lossy, quality ${webpQuality}`}), ` +
|
package/tools/emit-page.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CompileResourcesGroup } from './compile-resources.mjs';
|
|
1
|
+
import type { CompileResourcesGroup, SoundFontEmbed, SoundFontUsage } from './compile-resources.mjs';
|
|
2
2
|
import type { CompressRow } from './compress-dist.mjs';
|
|
3
3
|
import type { SingleFileOptions, SingleFileResult } from './single-file.mjs';
|
|
4
4
|
|
|
@@ -9,6 +9,8 @@ export interface EmitPageOptions {
|
|
|
9
9
|
assets?: string;
|
|
10
10
|
/** maps an asset path to its script's name; defaults to one script per top-level folder */
|
|
11
11
|
groupBy?: (key: string) => string;
|
|
12
|
+
/** Exact paths relative to the asset folder to embed. Omit to include all; [] includes none. Unknown entries fail by name. */
|
|
13
|
+
assetFiles?: readonly string[];
|
|
12
14
|
/** write `.gz`/`.br` siblings for a server to negotiate; default true */
|
|
13
15
|
compress?: boolean;
|
|
14
16
|
/** also write `.xz`, where the system `xz` binary exists */
|
|
@@ -18,6 +20,10 @@ export interface EmitPageOptions {
|
|
|
18
20
|
toWebp?: boolean;
|
|
19
21
|
webpLossless?: boolean;
|
|
20
22
|
webpQuality?: number;
|
|
23
|
+
/** Subset an embedded `.sf2` to the programs the embedded `.mid`/`.midi` files use. Default true; `false` embeds every SoundFont whole. */
|
|
24
|
+
soundFont?: boolean;
|
|
25
|
+
/** Explicit `{ bank, program }` list to subset a SoundFont to, overriding the MIDI-derived list. */
|
|
26
|
+
soundFontUsage?: readonly SoundFontUsage[];
|
|
21
27
|
/** the page's Content-Security-Policy: on by default, `false` to leave it out */
|
|
22
28
|
csp?: boolean | CspOptions;
|
|
23
29
|
}
|
|
@@ -36,6 +42,8 @@ export interface EmitPageResult {
|
|
|
36
42
|
rawBytes: number;
|
|
37
43
|
embeddedBytes: number;
|
|
38
44
|
webpConverted: number;
|
|
45
|
+
/** one entry per embedded `.sf2`: the programs kept (null when embedded whole) and the byte saving */
|
|
46
|
+
soundFonts: SoundFontEmbed[];
|
|
39
47
|
compressed: CompressRow[] | null;
|
|
40
48
|
single: SingleFileResult | null;
|
|
41
49
|
}
|
package/tools/emit-page.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { readFile, writeFile, rm, stat } from 'node:fs/promises';
|
|
|
3
3
|
import { realpathSync } from 'node:fs';
|
|
4
4
|
import { join, resolve as resolvePath } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { compileResources } from './compile-resources.mjs';
|
|
6
|
+
import { compileResources, soundFontUsageFromFlag } from './compile-resources.mjs';
|
|
7
7
|
import { compressDist } from './compress-dist.mjs';
|
|
8
8
|
import { toClassicScript } from './classic-html.mjs';
|
|
9
9
|
import { buildSingleFile } from './single-file.mjs';
|
|
@@ -30,6 +30,7 @@ import { withContentSecurityPolicy } from './csp.mjs';
|
|
|
30
30
|
* [--no-compress] [--xz]
|
|
31
31
|
* [--single-file] [--single-file-compress[=level]]
|
|
32
32
|
* [--single-file-brotli[=quality]] [--to-webp] [--webp-lossy[=quality]]
|
|
33
|
+
* [--no-soundfont-subset] [--soundfont-usage=bank:program,...]
|
|
33
34
|
*
|
|
34
35
|
* `--dist` defaults to `<project folder>/dist` and `--assets` to `<project folder>/assets`; with
|
|
35
36
|
* no assets folder at all, the page is rewritten and nothing is compiled.
|
|
@@ -66,18 +67,32 @@ export async function emitPage({
|
|
|
66
67
|
dist,
|
|
67
68
|
assets,
|
|
68
69
|
groupBy,
|
|
70
|
+
assetFiles,
|
|
69
71
|
compress = true,
|
|
70
72
|
xz = false,
|
|
71
73
|
singleFile = false,
|
|
72
74
|
toWebp = false,
|
|
73
75
|
webpLossless = true,
|
|
74
76
|
webpQuality = 90,
|
|
77
|
+
soundFont = true,
|
|
78
|
+
soundFontUsage,
|
|
75
79
|
csp = true,
|
|
76
80
|
}) {
|
|
77
81
|
if (!dist) throw new Error('emitPage needs the dist folder of a vite build');
|
|
82
|
+
if (!assets && assetFiles?.length) throw new Error(`unknown asset file ${assetFiles[0]}: no assets folder`);
|
|
78
83
|
const compiled = assets
|
|
79
|
-
? await compileResources({
|
|
80
|
-
|
|
84
|
+
? await compileResources({
|
|
85
|
+
from: assets,
|
|
86
|
+
to: join(dist, 'assets'),
|
|
87
|
+
groupBy,
|
|
88
|
+
assetFiles,
|
|
89
|
+
toWebp,
|
|
90
|
+
webpLossless,
|
|
91
|
+
webpQuality,
|
|
92
|
+
soundFont,
|
|
93
|
+
soundFontUsage,
|
|
94
|
+
})
|
|
95
|
+
: { groups: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0, soundFonts: [] };
|
|
81
96
|
|
|
82
97
|
const html = await readFile(join(dist, 'index.html'), 'utf8');
|
|
83
98
|
const classic = toClassicScript(html);
|
|
@@ -132,6 +147,10 @@ async function main(argv) {
|
|
|
132
147
|
level: levelFlag?.includes('=') ? Number(levelFlag.split('=')[1]) : undefined,
|
|
133
148
|
};
|
|
134
149
|
|
|
150
|
+
const soundFontUsage = soundFontUsageFromFlag(
|
|
151
|
+
argv.find((a) => a.startsWith('--soundfont-usage='))?.slice('--soundfont-usage='.length),
|
|
152
|
+
);
|
|
153
|
+
|
|
135
154
|
const result = await emitPage({
|
|
136
155
|
dist,
|
|
137
156
|
assets: hasAssets ? assetsDir : undefined,
|
|
@@ -143,6 +162,8 @@ async function main(argv) {
|
|
|
143
162
|
toWebp: argv.includes('--to-webp'),
|
|
144
163
|
webpLossless: !webpLossyFlag,
|
|
145
164
|
webpQuality: webpLossyFlag?.includes('=') ? Number(webpLossyFlag.split('=')[1]) : 90,
|
|
165
|
+
soundFont: !argv.includes('--no-soundfont-subset'),
|
|
166
|
+
soundFontUsage,
|
|
146
167
|
csp: argv.includes('--no-csp')
|
|
147
168
|
? false
|
|
148
169
|
: { connect: argv.filter((a) => a.startsWith('--connect=')).map((a) => a.slice('--connect='.length)) },
|
|
@@ -158,6 +179,13 @@ async function main(argv) {
|
|
|
158
179
|
`${kb(result.rawBytes)} -> ${kb(result.embeddedBytes)} embedded`,
|
|
159
180
|
);
|
|
160
181
|
}
|
|
182
|
+
for (const font of result.soundFonts ?? []) {
|
|
183
|
+
console.log(
|
|
184
|
+
font.programs === null
|
|
185
|
+
? ` ${font.key}: SoundFont embedded whole (${kb(font.rawBytes)}), no shipped MIDI to subset against`
|
|
186
|
+
: ` ${font.key}: SoundFont subset to ${font.programs} program(s), ${kb(font.rawBytes)} -> ${kb(font.embeddedBytes)}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
161
189
|
console.log(' open index.html directly - no server needed');
|
|
162
190
|
if (result.compressed) {
|
|
163
191
|
for (const row of result.compressed) {
|
package/tools/vite.mjs
CHANGED
|
@@ -11,7 +11,12 @@ import { buildArtifactSbom, serialize, shippedFiles } from './sbom.mjs';
|
|
|
11
11
|
* It sets the configuration that target needs (a classic IIFE bundle named `game.js`, relative
|
|
12
12
|
* urls through `base: './'`, no inlined or copied assets since they are compiled instead) and
|
|
13
13
|
* serves `assets` (default `'assets'`, `false` for none) as the public folder, so an asset path reads the same in `vite` dev as in the
|
|
14
|
-
* build.
|
|
14
|
+
* build. `assetFiles: ['characters/hero.svg', 'music/theme.ogg']` embeds only those paths;
|
|
15
|
+
* omit it for all assets, or pass [] for none. An unknown entry fails by name. Filtering
|
|
16
|
+
* affects the build only, so the dev server still serves the asset folder normally.
|
|
17
|
+
* An embedded `.sf2` is subset at build time to the programs the embedded `.mid` files
|
|
18
|
+
* use (`soundFont: false` embeds it whole, `soundFontUsage` names the programs instead).
|
|
19
|
+
* At the end of the build it runs `emitPage` over the output folder with the same
|
|
15
20
|
* options. A setting the game's own config gives explicitly still wins, since vite merges a
|
|
16
21
|
* plugin's `config` result under the user's.
|
|
17
22
|
*
|