@datamoc/mw_games 0.39.1 → 0.41.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/REFERENCE.md +2115 -0
  3. package/api-surface.json +48 -0
  4. package/data/microgm-opt.PROVENANCE.md +23 -0
  5. package/data/microgm-opt.sf2 +0 -0
  6. package/data/soundfont-catalog.json +183 -0
  7. package/dist/ai/index.d.ts +10 -0
  8. package/dist/ai/index.d.ts.map +1 -1
  9. package/dist/ai/index.js.map +1 -1
  10. package/dist/ai/lua.d.ts +6 -0
  11. package/dist/ai/lua.d.ts.map +1 -1
  12. package/dist/ai/lua.js.map +1 -1
  13. package/dist/audio/SampleDecoder.d.ts +41 -0
  14. package/dist/audio/SampleDecoder.d.ts.map +1 -0
  15. package/dist/audio/SampleDecoder.js +70 -0
  16. package/dist/audio/SampleDecoder.js.map +1 -0
  17. package/dist/audio/SoundFont.d.ts +59 -0
  18. package/dist/audio/SoundFont.d.ts.map +1 -1
  19. package/dist/audio/SoundFont.js +248 -2
  20. package/dist/audio/SoundFont.js.map +1 -1
  21. package/dist/audio/index.d.ts +3 -1
  22. package/dist/audio/index.d.ts.map +1 -1
  23. package/dist/audio/index.js +2 -1
  24. package/dist/audio/index.js.map +1 -1
  25. package/dist/mw_games.global.js +11 -11
  26. package/dist/mwl/presentation.d.ts +16 -0
  27. package/dist/mwl/presentation.d.ts.map +1 -1
  28. package/dist/mwl/presentation.js.map +1 -1
  29. package/dist/mwl/pyroeis-runtime.d.ts +7 -0
  30. package/dist/mwl/pyroeis-runtime.d.ts.map +1 -1
  31. package/dist/mwl/pyroeis-runtime.js.map +1 -1
  32. package/dist/mwl/pyroeis.d.ts +3 -0
  33. package/dist/mwl/pyroeis.d.ts.map +1 -1
  34. package/dist/mwl/pyroeis.js.map +1 -1
  35. package/dist/physics/PhysicsWorld.d.ts +3 -0
  36. package/dist/physics/PhysicsWorld.d.ts.map +1 -1
  37. package/dist/physics/PhysicsWorld.js.map +1 -1
  38. package/dist/physics/XpbdWorld.d.ts +11 -0
  39. package/dist/physics/XpbdWorld.d.ts.map +1 -1
  40. package/dist/physics/XpbdWorld.js.map +1 -1
  41. package/dist/physics/cloth/index.d.ts +20 -19
  42. package/dist/physics/cloth/index.d.ts.map +1 -1
  43. package/dist/physics/cloth/index.js +19 -0
  44. package/dist/physics/cloth/index.js.map +1 -1
  45. package/dist/physics/spring/index.d.ts +19 -18
  46. package/dist/physics/spring/index.d.ts.map +1 -1
  47. package/dist/physics/spring/index.js +23 -0
  48. package/dist/physics/spring/index.js.map +1 -1
  49. package/dist/three-d/Character3D.d.ts +8 -7
  50. package/dist/three-d/Character3D.d.ts.map +1 -1
  51. package/dist/three-d/Character3D.js +7 -0
  52. package/dist/three-d/Character3D.js.map +1 -1
  53. package/dist/three-d/Collision3D.d.ts +4 -0
  54. package/dist/three-d/Collision3D.d.ts.map +1 -1
  55. package/dist/three-d/Collision3D.js.map +1 -1
  56. package/dist/three-d/Engine3D.d.ts +2 -0
  57. package/dist/three-d/Engine3D.d.ts.map +1 -1
  58. package/dist/three-d/Engine3D.js.map +1 -1
  59. package/dist/three-d/Grid.d.ts +5 -0
  60. package/dist/three-d/Grid.d.ts.map +1 -1
  61. package/dist/three-d/Grid.js.map +1 -1
  62. package/dist/three-d/Heightmap.d.ts +3 -0
  63. package/dist/three-d/Heightmap.d.ts.map +1 -1
  64. package/dist/three-d/Heightmap.js.map +1 -1
  65. package/dist/three-d/TileGrid3D.d.ts +6 -0
  66. package/dist/three-d/TileGrid3D.d.ts.map +1 -1
  67. package/dist/three-d/TileGrid3D.js.map +1 -1
  68. package/dist/three-d/Vox.d.ts +7 -0
  69. package/dist/three-d/Vox.d.ts.map +1 -1
  70. package/dist/three-d/Vox.js.map +1 -1
  71. package/dist/two-d/presentation/MwlPresentation.d.ts +7 -0
  72. package/dist/two-d/presentation/MwlPresentation.d.ts.map +1 -1
  73. package/dist/two-d/presentation/MwlPresentation.js.map +1 -1
  74. package/dist/version.d.ts +1 -1
  75. package/dist/version.js +1 -1
  76. package/package.json +18 -1
  77. package/sbom.cdx.json +139 -139
  78. package/tools/benchmark-browser.mjs +6 -0
  79. package/tools/bundle-size.mjs +4 -0
  80. package/tools/compile-resources.d.mts +22 -0
  81. package/tools/compile-resources.mjs +116 -3
  82. package/tools/emit-page.d.mts +7 -1
  83. package/tools/emit-page.mjs +20 -2
  84. package/tools/init.mjs +3 -1
  85. package/tools/make-example-assets.mjs +4 -0
  86. package/tools/multiplayer-server.mjs +4 -0
  87. package/tools/mwl.mjs +7 -0
  88. package/tools/play-uci.mjs +4 -0
  89. package/tools/sbom.mjs +4 -0
  90. package/tools/soundfonts.d.mts +79 -0
  91. package/tools/soundfonts.mjs +301 -0
  92. package/tools/verify-scenarios.mjs +6 -2
  93. package/tools/vite.mjs +2 -0
  94. package/tools.md +216 -0
@@ -136,6 +136,12 @@ export async function measurePage({
136
136
  * `benchmark-results/<page>.json`, named by the page so two pages keep separate histories.
137
137
  */
138
138
  async function main(argv) {
139
+ if (argv.includes('--help') || argv.includes('-h')) {
140
+ console.log(
141
+ 'usage: mwg-bench <page.html | dist folder> [--frames=180] [--min-fps=45] [--max-p95=40] [--history=<file>]',
142
+ );
143
+ return;
144
+ }
139
145
  const value = (name, env, fallback) =>
140
146
  argv.find((a) => a.startsWith(`--${name}=`))?.split('=')[1] ?? process.env[env] ?? fallback;
141
147
  const target = argv.find((arg) => !arg.startsWith('--')) ?? 'examples/dungeon/dist/index.html';
@@ -99,6 +99,10 @@ function measureFramework(root) {
99
99
  }
100
100
 
101
101
  function main(argv) {
102
+ if (argv.includes('--help') || argv.includes('-h')) {
103
+ console.log('usage: mwg-size <dist folder> [--baseline=<file>] [--tolerance=0.02] [--update]');
104
+ return;
105
+ }
102
106
  const tolerance = Number(
103
107
  argv.find((a) => a.startsWith('--tolerance='))?.slice(12) ?? process.env.MWG_SIZE_TOLERANCE ?? 0.02,
104
108
  );
@@ -12,6 +12,10 @@ export interface CompileResourcesOptions {
12
12
  toWebp?: boolean;
13
13
  webpLossless?: boolean;
14
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[];
15
19
  }
16
20
 
17
21
  export interface CompileResourcesGroup {
@@ -28,9 +32,27 @@ export interface CompileResourcesResult {
28
32
  skipped: number;
29
33
  /** the `CREDITS.txt` text when the asset folder holds a `credits.json`, else null */
30
34
  credits: string | null;
35
+ /** one entry per embedded `.sf2`: the programs kept (null when embedded whole) and the byte saving */
36
+ soundFonts: SoundFontEmbed[];
31
37
  }
32
38
 
33
39
  /** the plain-text credits for a manifest (an array, or `{ credits: [...] }`, of `{ name, license, author?, url? }`) */
34
40
  export function creditsText(manifest: unknown): string;
35
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
+
36
58
  export function compileResources(options: CompileResourcesOptions): Promise<CompileResourcesResult>;
@@ -38,8 +38,57 @@ 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',
44
+ '.sf3': 'application/octet-stream',
41
45
  };
42
46
 
47
+ const isSoundFontKey = (key) => /\.sf[23]$/i.test(key);
48
+ const isMidiKey = (key) => /\.(mid|midi)$/i.test(key);
49
+
50
+ /**
51
+ * The SoundFont/MIDI implementation lives in the library, not in this tool: in this
52
+ * repository it is the TypeScript source, in an installed package it is the built
53
+ * `dist/` the source compiles to. Only called when a build actually embeds a
54
+ * SoundFont it can subset, so a game without one never pays for the import.
55
+ */
56
+ async function loadSoundFontTools() {
57
+ try {
58
+ const [soundFont, midi] = await Promise.all([
59
+ import('../src/audio/SoundFont.ts'),
60
+ import('../src/audio/Midi.ts'),
61
+ ]);
62
+ return { ...soundFont, ...midi };
63
+ } catch {
64
+ return import('../dist/audio/index.js');
65
+ }
66
+ }
67
+
68
+ function checkedSoundFontUsage(soundFontUsage) {
69
+ if (soundFontUsage === undefined) return undefined;
70
+ if (!Array.isArray(soundFontUsage)) throw new Error('soundFontUsage must be an array of { bank, program }');
71
+ return soundFontUsage.map((entry, index) => {
72
+ const { bank, program } = entry ?? {};
73
+ if (!Number.isInteger(bank) || bank < 0 || !Number.isInteger(program) || program < 0 || program > 127) {
74
+ throw new Error(`soundFontUsage[${index}] needs an integer bank >= 0 and a program between 0 and 127`);
75
+ }
76
+ return { bank, program };
77
+ });
78
+ }
79
+
80
+ /** parses a `--soundfont-usage=bank:program,...` CLI value; undefined when the flag is absent */
81
+ export function soundFontUsageFromFlag(value) {
82
+ if (value === undefined) return undefined;
83
+ return value
84
+ .split(',')
85
+ .filter(Boolean)
86
+ .map((pair) => {
87
+ const [bank, program] = pair.split(':').map(Number);
88
+ return { bank, program };
89
+ });
90
+ }
91
+
43
92
  export async function* walk(dir) {
44
93
  let entries;
45
94
  try {
@@ -72,6 +121,10 @@ export async function* walk(dir) {
72
121
  * pixel-art tileset's exact colours survive; set false only with `webpQuality` to opt into
73
122
  * lossy re-encoding for photographic source art)
74
123
  * @param options.webpQuality 0-100, used only when `webpLossless` is false (default 90)
124
+ * @param options.soundFont subset an embedded `.sf2` or `.sf3` to the programs the embedded
125
+ * `.mid`/`.midi` files use (default true); `false` embeds every SoundFont whole
126
+ * @param options.soundFontUsage explicit `{ bank, program }` list to subset to, for a
127
+ * game that voices notes no shipped MIDI file requests; overrides the MIDI-derived list
75
128
  * @returns the group names, in the order the page should load them
76
129
  *
77
130
  * An `assets/credits.json` (an array, or `{ "credits": [...] }`, of `{ name, license, author?, url? }`)
@@ -88,6 +141,8 @@ export async function compileResources({
88
141
  toWebp: convertToWebp = false,
89
142
  webpLossless = true,
90
143
  webpQuality = 90,
144
+ soundFont = true,
145
+ soundFontUsage,
91
146
  } = {}) {
92
147
  if (!from || !to) throw new Error('compileResources needs both `from` and `to`');
93
148
 
@@ -110,6 +165,38 @@ export async function compileResources({
110
165
  }
111
166
  }
112
167
 
168
+ // A shipped SoundFont carries only the programs the game's shipped MIDI files
169
+ // request: the full bank - even a small one - is mostly
170
+ // instruments no track plays. Usage is resolved before any output is replaced,
171
+ // so an unreadable MIDI or SoundFont fails the build with the old output intact.
172
+ // With no shipped MIDI and no explicit `soundFontUsage` the usage is unknowable
173
+ // (a game may voice notes it generates), so the font embeds whole.
174
+ const isSelected = (file) => !selected || selected.has(keyOf(file));
175
+ const explicitUsage = checkedSoundFontUsage(soundFontUsage);
176
+ const embedsSoundFont =
177
+ soundFont !== false && files.some((file) => isSelected(file) && isSoundFontKey(keyOf(file)));
178
+ let soundFontTools = null;
179
+ let usage = explicitUsage;
180
+ if (embedsSoundFont && usage === undefined) {
181
+ const midiFiles = files.filter((file) => isSelected(file) && isMidiKey(keyOf(file)));
182
+ if (midiFiles.length > 0) {
183
+ soundFontTools = await loadSoundFontTools();
184
+ const parsed = [];
185
+ for (const file of midiFiles) {
186
+ try {
187
+ parsed.push(soundFontTools.parseMidi(await readFile(file)));
188
+ } catch (error) {
189
+ throw new Error(
190
+ `cannot read MIDI asset ${keyOf(file)} to subset a SoundFont: ${error.message ?? error}`,
191
+ );
192
+ }
193
+ }
194
+ usage = soundFontTools.collectSoundFontUsage(parsed);
195
+ }
196
+ }
197
+ if (embedsSoundFont && usage !== undefined && !soundFontTools) soundFontTools = await loadSoundFontTools();
198
+ const soundFonts = [];
199
+
113
200
  await rm(to, { recursive: true, force: true });
114
201
  await mkdir(to, { recursive: true });
115
202
 
@@ -153,6 +240,19 @@ export async function compileResources({
153
240
  }
154
241
  }
155
242
 
243
+ if (isSoundFontKey(key)) {
244
+ if (usage !== undefined && soundFontTools) {
245
+ try {
246
+ bytes = Buffer.from(soundFontTools.subsetSoundFont(data, usage));
247
+ } catch (error) {
248
+ throw new Error(`cannot subset SoundFont asset ${key}: ${error.message ?? error}`);
249
+ }
250
+ soundFonts.push({ key, programs: usage.length, rawBytes: data.length, embeddedBytes: bytes.length });
251
+ } else {
252
+ soundFonts.push({ key, programs: null, rawBytes: data.length, embeddedBytes: data.length });
253
+ }
254
+ }
255
+
156
256
  const name = group(key);
157
257
  if (!groups.has(name)) groups.set(name, {});
158
258
  groups.get(name)[key] = `data:${mimeType};base64,${bytes.toString('base64')}`;
@@ -173,7 +273,7 @@ export async function compileResources({
173
273
 
174
274
  if (credits) await writeFile(join(to, 'CREDITS.txt'), credits, 'utf8');
175
275
 
176
- return { groups: written, rawBytes, embeddedBytes, webpConverted, skipped, credits };
276
+ return { groups: written, rawBytes, embeddedBytes, webpConverted, skipped, credits, soundFonts };
177
277
  }
178
278
 
179
279
  /** the plain-text credits for a manifest, one block per entry; throws on an entry that cannot be attributed */
@@ -202,7 +302,7 @@ async function runCli() {
202
302
  const [from, to] = args.filter((a) => !a.startsWith('--'));
203
303
  if (!from || !to) {
204
304
  console.error(
205
- 'usage: node compile-resources.mjs <asset folder> <output folder> [--to-webp] [--webp-lossy[=quality]]',
305
+ 'usage: node compile-resources.mjs <asset folder> <output folder> [--to-webp] [--webp-lossy[=quality]] [--no-soundfont-subset] [--soundfont-usage=bank:program,...]',
206
306
  );
207
307
  process.exit(1);
208
308
  }
@@ -211,7 +311,11 @@ async function runCli() {
211
311
  const webpLossless = !lossyFlag;
212
312
  const webpQuality = lossyFlag && lossyFlag.includes('=') ? Number(lossyFlag.split('=')[1]) : 90;
213
313
 
214
- const { groups, rawBytes, embeddedBytes, webpConverted, skipped } = await compileResources({
314
+ const soundFontUsage = soundFontUsageFromFlag(
315
+ args.find((a) => a.startsWith('--soundfont-usage='))?.slice('--soundfont-usage='.length),
316
+ );
317
+
318
+ const { groups, rawBytes, embeddedBytes, webpConverted, skipped, soundFonts } = await compileResources({
215
319
  from,
216
320
  to,
217
321
  //a flat folder has no subdirectories to group by, so everything lands in one file
@@ -219,6 +323,8 @@ async function runCli() {
219
323
  toWebp,
220
324
  webpLossless,
221
325
  webpQuality,
326
+ soundFont: !args.includes('--no-soundfont-subset'),
327
+ soundFontUsage,
222
328
  });
223
329
 
224
330
  const kb = (n) => (n / 1024).toFixed(1).padStart(8) + ' KB';
@@ -226,6 +332,13 @@ async function runCli() {
226
332
  console.log(` ${join(relative(process.cwd(), to), name + '.js').padEnd(40)}${kb(size)} (${count} files)`);
227
333
  }
228
334
  console.log(` ${groups.length} script(s), ${kb(rawBytes)} of assets` + (skipped ? `, ${skipped} skipped` : ''));
335
+ for (const font of soundFonts) {
336
+ console.log(
337
+ font.programs === null
338
+ ? ` ${font.key}: SoundFont embedded whole (${kb(font.rawBytes)}), no shipped MIDI to subset against`
339
+ : ` ${font.key}: SoundFont subset to ${font.programs} program(s), ${kb(font.rawBytes)} -> ${kb(font.embeddedBytes)}`,
340
+ );
341
+ }
229
342
  if (toWebp) {
230
343
  console.log(
231
344
  ` ${webpConverted} image(s) converted to WebP (${webpLossless ? 'lossless' : `lossy, quality ${webpQuality}`}), ` +
@@ -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
 
@@ -20,6 +20,10 @@ export interface EmitPageOptions {
20
20
  toWebp?: boolean;
21
21
  webpLossless?: boolean;
22
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[];
23
27
  /** the page's Content-Security-Policy: on by default, `false` to leave it out */
24
28
  csp?: boolean | CspOptions;
25
29
  }
@@ -38,6 +42,8 @@ export interface EmitPageResult {
38
42
  rawBytes: number;
39
43
  embeddedBytes: number;
40
44
  webpConverted: number;
45
+ /** one entry per embedded `.sf2`: the programs kept (null when embedded whole) and the byte saving */
46
+ soundFonts: SoundFontEmbed[];
41
47
  compressed: CompressRow[] | null;
42
48
  single: SingleFileResult | null;
43
49
  }
@@ -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.
@@ -73,6 +74,8 @@ export async function emitPage({
73
74
  toWebp = false,
74
75
  webpLossless = true,
75
76
  webpQuality = 90,
77
+ soundFont = true,
78
+ soundFontUsage,
76
79
  csp = true,
77
80
  }) {
78
81
  if (!dist) throw new Error('emitPage needs the dist folder of a vite build');
@@ -86,8 +89,10 @@ export async function emitPage({
86
89
  toWebp,
87
90
  webpLossless,
88
91
  webpQuality,
92
+ soundFont,
93
+ soundFontUsage,
89
94
  })
90
- : { groups: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0 };
95
+ : { groups: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0, soundFonts: [] };
91
96
 
92
97
  const html = await readFile(join(dist, 'index.html'), 'utf8');
93
98
  const classic = toClassicScript(html);
@@ -142,6 +147,10 @@ async function main(argv) {
142
147
  level: levelFlag?.includes('=') ? Number(levelFlag.split('=')[1]) : undefined,
143
148
  };
144
149
 
150
+ const soundFontUsage = soundFontUsageFromFlag(
151
+ argv.find((a) => a.startsWith('--soundfont-usage='))?.slice('--soundfont-usage='.length),
152
+ );
153
+
145
154
  const result = await emitPage({
146
155
  dist,
147
156
  assets: hasAssets ? assetsDir : undefined,
@@ -153,6 +162,8 @@ async function main(argv) {
153
162
  toWebp: argv.includes('--to-webp'),
154
163
  webpLossless: !webpLossyFlag,
155
164
  webpQuality: webpLossyFlag?.includes('=') ? Number(webpLossyFlag.split('=')[1]) : 90,
165
+ soundFont: !argv.includes('--no-soundfont-subset'),
166
+ soundFontUsage,
156
167
  csp: argv.includes('--no-csp')
157
168
  ? false
158
169
  : { connect: argv.filter((a) => a.startsWith('--connect=')).map((a) => a.slice('--connect='.length)) },
@@ -168,6 +179,13 @@ async function main(argv) {
168
179
  `${kb(result.rawBytes)} -> ${kb(result.embeddedBytes)} embedded`,
169
180
  );
170
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
+ }
171
189
  console.log(' open index.html directly - no server needed');
172
190
  if (result.compressed) {
173
191
  for (const row of result.compressed) {
package/tools/init.mjs CHANGED
@@ -130,7 +130,9 @@ export function initProject(folder) {
130
130
  //no top-level await: this module is also imported, and the real path covers an npm bin symlink
131
131
  if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
132
132
  const folder = process.argv[2];
133
- if (!folder) {
133
+ if (folder === '--help' || folder === '-h') {
134
+ console.log('usage: mwg-init <new folder>');
135
+ } else if (!folder) {
134
136
  console.error('usage: mwg-init <new folder>');
135
137
  process.exitCode = 2;
136
138
  } else {
@@ -717,6 +717,10 @@ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.me
717
717
  const args = process.argv.slice(2);
718
718
  const folder = args.find((arg) => !arg.startsWith('--'));
719
719
  const inRepository = existsSync(join(here, '..', 'examples'));
720
+ if (args.includes('--help') || args.includes('-h')) {
721
+ console.log('usage: mwg-placeholder-assets <folder> [--force]');
722
+ process.exit(0);
723
+ }
720
724
  if (!folder && !inRepository) {
721
725
  console.error('usage: mwg-placeholder-assets <folder> [--force]');
722
726
  process.exit(1);
@@ -187,6 +187,10 @@ export function createLockstepServer(options = {}) {
187
187
  //realpath, since an npm `bin` shim reaches this file through a symlink
188
188
  if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
189
189
  const args = process.argv.slice(2);
190
+ if (args.includes('--help') || args.includes('-h')) {
191
+ console.log('usage: mwg-lockstep-server [port] [--host=<address>]');
192
+ process.exit(0);
193
+ }
190
194
  const port = Number(args.find((arg) => !arg.startsWith('--')) ?? 8787);
191
195
  const host = args.find((arg) => arg.startsWith('--host='))?.slice(7) ?? '127.0.0.1';
192
196
  const server = createLockstepServer({ port, host });
package/tools/mwl.mjs CHANGED
@@ -65,6 +65,13 @@ const assetRoot = assetRootFlag === -1 ? undefined : args[assetRootFlag + 1];
65
65
  const targetFlag = args.indexOf('--to');
66
66
  const target = targetFlag === -1 ? undefined : args[targetFlag + 1];
67
67
 
68
+ if (args.includes('--help') || args.includes('-h')) {
69
+ console.log(
70
+ 'usage: mwg-mwl <validate|compile|format|migrate|extract-i18n|assets|report|diagnostics|ci|hooks|build> input.mwl [-o output] [--to 1.0] [--manifest hooks.json] [--asset-root directory]',
71
+ );
72
+ process.exit(0);
73
+ }
74
+
68
75
  if (!command || !input || !commands.includes(command)) {
69
76
  console.error(
70
77
  'Usage: mwl <validate|compile|format|migrate|extract-i18n|assets|report|diagnostics|ci|hooks|build> input.mwl [-o output] [--to 1.0] [--manifest hooks.json] [--asset-root directory]',
@@ -171,6 +171,10 @@ const isMain = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPat
171
171
 
172
172
  if (isMain) {
173
173
  const args = process.argv.slice(2);
174
+ if (args.includes('--help') || args.includes('-h')) {
175
+ console.log('usage: mwg-uci [--book <book.json>] [--tables <dir>] [--depth N]');
176
+ process.exit(0);
177
+ }
174
178
  const flag = (name) => {
175
179
  const at = args.indexOf(name);
176
180
  return at === -1 ? undefined : args[at + 1];
package/tools/sbom.mjs CHANGED
@@ -301,6 +301,10 @@ async function readJson(path) {
301
301
  * `application`, anything else as a `library`. `--check` compares instead of writing, for CI.
302
302
  */
303
303
  async function main(argv) {
304
+ if (argv.includes('--help') || argv.includes('-h')) {
305
+ console.log('usage: mwg-sbom [project folder] [--out=<file>] [--check]');
306
+ return;
307
+ }
304
308
  const root = resolve(argv.find((arg) => !arg.startsWith('--')) ?? '.');
305
309
  const out = resolve(root, argv.find((arg) => arg.startsWith('--out='))?.slice(6) ?? OUTPUT);
306
310
  const packageJson = await readJson(join(root, 'package.json'));
@@ -0,0 +1,79 @@
1
+ /** how freely a catalogue font may be shipped inside a game */
2
+ export type SoundFontLicenceClass =
3
+ 'public-domain' | 'permissive' | 'attribution' | 'share-alike' | 'copyleft' | 'check';
4
+
5
+ export interface SoundFontCatalogEntry {
6
+ name: string;
7
+ licence: string;
8
+ licenceClass: SoundFontLicenceClass;
9
+ web?: string;
10
+ /** instrument files the collection carries for this font (programs for a bundled bank) */
11
+ programs: number;
12
+ /** megabytes of a full General MIDI bank, SoundFont 3 compressed; absent for the bundled bank */
13
+ gmMB?: number;
14
+ /** megabytes of every instrument file, SoundFont 3 compressed (the bundled bank's own size) */
15
+ fullMB: number;
16
+ /** the file of a bundled bank, relative to the package root */
17
+ file?: string;
18
+ note?: string;
19
+ }
20
+
21
+ export interface SoundFontCatalog {
22
+ description: string;
23
+ /** where the per-instrument files are fetched from by default */
24
+ source: string;
25
+ /** the name of the bank the package ships and games use unless they choose another */
26
+ default: string;
27
+ bundled: SoundFontCatalogEntry[];
28
+ fonts: SoundFontCatalogEntry[];
29
+ }
30
+
31
+ export interface SoundFontProgramRef {
32
+ bank: number;
33
+ program: number;
34
+ }
35
+
36
+ export interface FetchSoundFontOptions {
37
+ /** a catalogue font name, from `mwg-soundfonts list` */
38
+ name: string;
39
+ programs: SoundFontProgramRef[];
40
+ catalog?: SoundFontCatalog;
41
+ /** a folder or `http(s)` URL laid out as `<font>/<bank>/<program>.sf3`; the catalogue's source by default */
42
+ source?: string;
43
+ /** required for a licence class that is not public-domain, permissive or attribution */
44
+ acceptLicence?: boolean;
45
+ /** the decoded font is a SoundFont 2 (`ffmpeg` needed) instead of a SoundFont 3; `sf3` by default */
46
+ format?: 'sf2' | 'sf3';
47
+ /** Ogg Vorbis bytes to mono 16-bit frames, for `format: 'sf2'`; `ffmpeg` by default */
48
+ decode?: (ogg: Uint8Array, rate: number) => Int16Array | Promise<Int16Array>;
49
+ /** replaces the file reader, mainly for tests */
50
+ read?: (path: string) => Promise<Uint8Array | null>;
51
+ }
52
+
53
+ export interface FetchedSoundFont {
54
+ /** one font holding the found programs, in `format` */
55
+ bytes: Uint8Array;
56
+ format: 'sf2' | 'sf3';
57
+ found: SoundFontProgramRef[];
58
+ /** requested programs the collection does not carry */
59
+ missing: SoundFontProgramRef[];
60
+ font: SoundFontCatalogEntry;
61
+ }
62
+
63
+ /** the licence-classified catalogue of fetchable fonts */
64
+ export declare const CATALOG_PATH: string;
65
+ export declare function readCatalog(path?: string): SoundFontCatalog;
66
+ export declare function findFont(catalog: SoundFontCatalog, name: string): SoundFontCatalogEntry | undefined;
67
+ /** `"0,24,128:0"`: a bare number is a program of bank 0, `bank:program` names both */
68
+ export declare function parseProgramList(text: string): SoundFontProgramRef[];
69
+ /** mono 16-bit frames from Ogg Vorbis bytes, through `ffmpeg` */
70
+ export declare function ffmpegDecode(ogg: Uint8Array): Int16Array;
71
+ /** a SoundFont 3 decoded into SoundFont 2 bytes (`ffmpeg` unless `decode` is given) */
72
+ export declare function sf3ToSf2(
73
+ bytes: Uint8Array,
74
+ decode?: (ogg: Uint8Array, rate: number) => Int16Array | Promise<Int16Array>,
75
+ ): Promise<Uint8Array>;
76
+ export declare function readCollectionFile(source: string, path: string): Promise<Uint8Array | null>;
77
+ export declare function fetchSoundFont(options: FetchSoundFontOptions): Promise<FetchedSoundFont>;
78
+ /** the credits note that travels beside a fetched font */
79
+ export declare function creditsFor(font: SoundFontCatalogEntry, programs: SoundFontProgramRef[]): string;