@moku-labs/game 0.13.1 → 0.13.2
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/dist/app/page.d.mts +1 -1
- package/dist/app/system.d.mts +3 -3
- package/dist/app.d.mts +2 -2
- package/dist/app.mjs +1 -1
- package/dist/assets.d.mts +1 -1
- package/dist/assets.mjs +1 -1
- package/dist/cli.d.mts +7 -5
- package/dist/cli.mjs +74 -36
- package/dist/{commands-DvFr8Uwp.d.mts → commands-BrgeMP5y.d.mts} +5 -5
- package/dist/control.d.mts +4 -4
- package/dist/{define-CCc0ozKe.d.mts → define-ZmWbWxon.d.mts} +2 -2
- package/dist/{headless-G4nuZ-xz.d.mts → headless-Dgxvp6Ym.d.mts} +1 -1
- package/dist/hot.d.mts +3 -3
- package/dist/hot.mjs +11 -9
- package/dist/{index-rnOYAriI.d.mts → index-CdbE_jGs.d.mts} +12 -4
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +1 -1
- package/dist/inspect.d.mts +7 -7
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/{memory-DwDhfFWK.d.mts → memory-DJ2sPExv.d.mts} +1 -1
- package/dist/project.mjs +1 -1
- package/dist/{runtime-B276Gg0s.d.mts → runtime-6qakboTm.d.mts} +1 -1
- package/dist/{src-SmT-XxVf.mjs → src-CltFFKtc.mjs} +955 -108
- package/dist/testing.d.mts +4 -4
- package/dist/testing.mjs +1 -1
- package/dist/{types-D_2G6Q68.d.mts → types-B3T2X6To.d.mts} +8 -4
- package/dist/{types-ZQLYXo5X.d.mts → types-C6y8EfEB.d.mts} +54 -5
- package/dist/{types-BrTpz9oL.d.mts → types-CmuwAjkV.d.mts} +19 -6
- package/dist/{types-DAdwEJMp.d.mts → types-D57SntUt.d.mts} +2 -2
- package/dist/{types-DLghaGEy.d.mts → types-DGIkfZB7.d.mts} +4 -4
- package/dist/{types-Bcdo5-2w.d.mts → types-zRGJdFGX.d.mts} +7 -3
- package/dist/visual.d.mts +6 -6
- package/llms.txt +6 -4
- package/package.json +4 -2
- package/dist/{assets-DYiKxPBX.mjs → assets-BrKSg1O6.mjs} +1 -1
- package/dist/{watch-CB8IWDBi.mjs → watch-CWI_on4T.mjs} +1 -1
|
@@ -3722,7 +3722,7 @@ function restorePosition(ctx, _modules, bookmark) {
|
|
|
3722
3722
|
* @param view - The last view.
|
|
3723
3723
|
* @returns True while nothing the state shows moved.
|
|
3724
3724
|
*/
|
|
3725
|
-
function isCurrent(ctx, view) {
|
|
3725
|
+
function isCurrent$1(ctx, view) {
|
|
3726
3726
|
const runner = ctx.state.runner;
|
|
3727
3727
|
return view.running === (runner.running !== void 0) && view.depth === runner.stack.length && view.top === runner.stack.at(-1) && view.open === ctx.state.gate.open && view.mode === ctx.state.fx.mode && view.journalIndex === runner.journalIndex;
|
|
3728
3728
|
}
|
|
@@ -3763,7 +3763,7 @@ function viewOf(ctx) {
|
|
|
3763
3763
|
function readState(ctx) {
|
|
3764
3764
|
const runner = ctx.state.runner;
|
|
3765
3765
|
const view = runner.view;
|
|
3766
|
-
if (view !== void 0 && isCurrent(ctx, view)) return view.state;
|
|
3766
|
+
if (view !== void 0 && isCurrent$1(ctx, view)) return view.state;
|
|
3767
3767
|
const next = viewOf(ctx);
|
|
3768
3768
|
runner.view = next;
|
|
3769
3769
|
return next.state;
|
|
@@ -8269,7 +8269,7 @@ function stopHost(state) {
|
|
|
8269
8269
|
//#endregion
|
|
8270
8270
|
//#region src/plugins/renderer/host/readback.ts
|
|
8271
8271
|
/** Bytes of one pixel: the engine loads PNG and WebP only, so every source is 8-bit RGBA. */
|
|
8272
|
-
const BYTES_PER_PIXEL = 4;
|
|
8272
|
+
const BYTES_PER_PIXEL$1 = 4;
|
|
8273
8273
|
/**
|
|
8274
8274
|
* Estimates the GPU bytes of one texture source: 4 bytes per pixel, every mip level, a level never
|
|
8275
8275
|
* smaller than one pixel.
|
|
@@ -8293,7 +8293,7 @@ function sourceBytes(source) {
|
|
|
8293
8293
|
width = Math.max(1, Math.floor(width / 2));
|
|
8294
8294
|
height = Math.max(1, Math.floor(height / 2));
|
|
8295
8295
|
}
|
|
8296
|
-
return pixels * BYTES_PER_PIXEL;
|
|
8296
|
+
return pixels * BYTES_PER_PIXEL$1;
|
|
8297
8297
|
}
|
|
8298
8298
|
/**
|
|
8299
8299
|
* Counts the texture sources the GPU holds and sums their estimated bytes. Pixi's
|
|
@@ -8561,7 +8561,7 @@ function resetWindow(state) {
|
|
|
8561
8561
|
//#endregion
|
|
8562
8562
|
//#region src/plugins/renderer/monitor/api.ts
|
|
8563
8563
|
/** Bytes of one MiB. */
|
|
8564
|
-
const BYTES_PER_MB = 1048576;
|
|
8564
|
+
const BYTES_PER_MB$1 = 1048576;
|
|
8565
8565
|
/**
|
|
8566
8566
|
* Rounds a number to a fixed count of decimals, so a counter reads well in a panel.
|
|
8567
8567
|
*
|
|
@@ -8599,7 +8599,7 @@ function createMonitorApi(ctx, deps) {
|
|
|
8599
8599
|
fps: state.lastStart !== void 0 && clock.now() - state.lastStart <= 1e3 ? roundTo(state.fps, 1) : 0,
|
|
8600
8600
|
frameMs: roundTo(state.frameMs, 2),
|
|
8601
8601
|
textures: usage.count,
|
|
8602
|
-
textureMb: roundTo(usage.bytes / BYTES_PER_MB, 2),
|
|
8602
|
+
textureMb: roundTo(usage.bytes / BYTES_PER_MB$1, 2),
|
|
8603
8603
|
views: counts.views,
|
|
8604
8604
|
pooled: counts.pooled,
|
|
8605
8605
|
renderPasses: deps.sync.renderPasses()
|
|
@@ -8937,7 +8937,7 @@ function parseFont(pixi, key, fnt) {
|
|
|
8937
8937
|
* @param texture - The page texture `assets` uploaded.
|
|
8938
8938
|
* @throws {Error} When the renderer does not draw.
|
|
8939
8939
|
*/
|
|
8940
|
-
function installFont(sctx, key, fnt, texture) {
|
|
8940
|
+
function installFont$1(sctx, key, fnt, texture) {
|
|
8941
8941
|
const pixi = sctx.deps.host.pixi();
|
|
8942
8942
|
if (pixi === void 0) throw new Error("[game] Fonts need a renderer.\n Call fonts.install after renderer.host.ready().");
|
|
8943
8943
|
const state = sctx.ctx.state.sync;
|
|
@@ -10207,7 +10207,9 @@ function clearFrames(state) {
|
|
|
10207
10207
|
}
|
|
10208
10208
|
/**
|
|
10209
10209
|
* Frees a texture, its source and the crops cut from it. A slice keeps the source: it belongs to
|
|
10210
|
-
* the page, which frees it. A
|
|
10210
|
+
* the page, which frees it. A source that goes lets go of its `change` listeners first, the ones
|
|
10211
|
+
* of its style too, so no bind group Pixi still keeps is told. A texture destroyed twice is a
|
|
10212
|
+
* no-op.
|
|
10211
10213
|
*
|
|
10212
10214
|
* @param state - The sync branch of the plugin state.
|
|
10213
10215
|
* @param texture - The texture to free.
|
|
@@ -10215,7 +10217,13 @@ function clearFrames(state) {
|
|
|
10215
10217
|
function destroyTexture(state, texture) {
|
|
10216
10218
|
releaseFrames$1(state, texture);
|
|
10217
10219
|
if (texture.destroyed) return;
|
|
10218
|
-
|
|
10220
|
+
if (state.slices.has(texture)) {
|
|
10221
|
+
texture.destroy(false);
|
|
10222
|
+
return;
|
|
10223
|
+
}
|
|
10224
|
+
texture.source.style.removeAllListeners("change");
|
|
10225
|
+
texture.source.removeAllListeners("change");
|
|
10226
|
+
texture.destroy(true);
|
|
10219
10227
|
}
|
|
10220
10228
|
//#endregion
|
|
10221
10229
|
//#region src/plugins/renderer/sync/views.ts
|
|
@@ -11839,7 +11847,7 @@ function createSyncApi(ctx, deps) {
|
|
|
11839
11847
|
removed: markRemoved
|
|
11840
11848
|
}) },
|
|
11841
11849
|
fonts: {
|
|
11842
|
-
install: (key, fnt, texture) => installFont(sctx, key, fnt, texture),
|
|
11850
|
+
install: (key, fnt, texture) => installFont$1(sctx, key, fnt, texture),
|
|
11843
11851
|
installed: (key) => isFontInstalled(state, key)
|
|
11844
11852
|
},
|
|
11845
11853
|
debug: {
|
|
@@ -14629,6 +14637,12 @@ const TIERS = [
|
|
|
14629
14637
|
"feature",
|
|
14630
14638
|
"lazy"
|
|
14631
14639
|
];
|
|
14640
|
+
/** The bytes of one megabyte. */
|
|
14641
|
+
const BYTES_PER_MB = 1048576;
|
|
14642
|
+
/** The bytes one pixel takes in GPU memory: RGBA, no mipmaps. */
|
|
14643
|
+
const BYTES_PER_PIXEL = 4;
|
|
14644
|
+
/** Megabytes are kept to three decimals: a value is rounded to a whole number of these parts. */
|
|
14645
|
+
const PARTS_PER_MB = 1e3;
|
|
14632
14646
|
/**
|
|
14633
14647
|
* Creates an empty map. It lives in its own non-exported function because lint rule L5 refuses a
|
|
14634
14648
|
* collection built inside an exported declaration.
|
|
@@ -14923,6 +14937,38 @@ function fileUrl(base, path) {
|
|
|
14923
14937
|
return `${base}${path.startsWith("/") ? path.slice(1) : path}`;
|
|
14924
14938
|
}
|
|
14925
14939
|
/**
|
|
14940
|
+
* Works out what an image costs in texture memory: `width × height × 4` bytes as megabytes,
|
|
14941
|
+
* rounded to 3 decimals. It is the number the scanner writes for a texture and for a font page;
|
|
14942
|
+
* the dev hot swap writes it again for a file that came back with another size.
|
|
14943
|
+
*
|
|
14944
|
+
* @param width - Width of the image in pixels.
|
|
14945
|
+
* @param height - Height of the image in pixels.
|
|
14946
|
+
* @returns The megabytes.
|
|
14947
|
+
* @example
|
|
14948
|
+
* ```ts
|
|
14949
|
+
* textureMb(256, 128); // 0.125
|
|
14950
|
+
* ```
|
|
14951
|
+
*/
|
|
14952
|
+
function textureMb(width, height) {
|
|
14953
|
+
return Math.round(width * height * BYTES_PER_PIXEL / BYTES_PER_MB * PARTS_PER_MB) / PARTS_PER_MB;
|
|
14954
|
+
}
|
|
14955
|
+
/**
|
|
14956
|
+
* Adds up megabytes, rounded to 3 decimals: the pages of a font give the font, the files and the
|
|
14957
|
+
* atlas pages of a bundle give the bundle.
|
|
14958
|
+
*
|
|
14959
|
+
* @param parts - The records to add up.
|
|
14960
|
+
* @returns The sum.
|
|
14961
|
+
* @example
|
|
14962
|
+
* ```ts
|
|
14963
|
+
* sumMb([{ mb: 0.125 }, { mb: 0.063 }, { mb: 0.021 }]); // 0.209
|
|
14964
|
+
* ```
|
|
14965
|
+
*/
|
|
14966
|
+
function sumMb(parts) {
|
|
14967
|
+
let total = 0;
|
|
14968
|
+
for (const part of parts) total += part.mb;
|
|
14969
|
+
return Math.round(total * PARTS_PER_MB) / PARTS_PER_MB;
|
|
14970
|
+
}
|
|
14971
|
+
/**
|
|
14926
14972
|
* Turns the nine-slice metadata of a file into the options `renderer.sync.textures.create` takes.
|
|
14927
14973
|
*
|
|
14928
14974
|
* @param file - One file of a bundle.
|
|
@@ -14947,7 +14993,8 @@ function nineOf(file) {
|
|
|
14947
14993
|
//#region src/plugins/assets/tiers.ts
|
|
14948
14994
|
/**
|
|
14949
14995
|
* @file assets plugin — loading one bundle and the boot sequence. One running load per bundle,
|
|
14950
|
-
* every caller a waiter: the last one to leave aborts the fetches.
|
|
14996
|
+
* every caller a waiter: the last one to leave aborts the fetches. The loaders of one image, one
|
|
14997
|
+
* font and one sound are exported: the dev hot swap of `swap.ts` reads a changed file through them.
|
|
14951
14998
|
*/
|
|
14952
14999
|
/** The extension of an AAC sound in an MP4 container; every other sound is an MP3. */
|
|
14953
15000
|
const M4A = /\.m4a$/i;
|
|
@@ -15102,10 +15149,13 @@ function touch(state, record) {
|
|
|
15102
15149
|
record.lastUsed = state.useCounter;
|
|
15103
15150
|
}
|
|
15104
15151
|
/**
|
|
15105
|
-
* Creates the four empty maps a bundle's assets live in
|
|
15106
|
-
* rule L5 refuses a collection built inside an exported declaration.
|
|
15152
|
+
* Creates the four empty maps a bundle's assets live in: the home of a running load.
|
|
15107
15153
|
*
|
|
15108
15154
|
* @returns Empty maps for textures, fonts, audio and atlas pages.
|
|
15155
|
+
* @example
|
|
15156
|
+
* ```ts
|
|
15157
|
+
* emptyAssets().textures.size; // 0
|
|
15158
|
+
* ```
|
|
15109
15159
|
*/
|
|
15110
15160
|
function emptyAssets() {
|
|
15111
15161
|
return {
|
|
@@ -15202,6 +15252,22 @@ async function loadFont(run, file) {
|
|
|
15202
15252
|
};
|
|
15203
15253
|
}
|
|
15204
15254
|
/**
|
|
15255
|
+
* Loads one sound: the bytes exactly as they were fetched, never decoded here, and the type of
|
|
15256
|
+
* their container.
|
|
15257
|
+
*
|
|
15258
|
+
* @param run - The running load.
|
|
15259
|
+
* @param file - The audio file of the manifest.
|
|
15260
|
+
* @returns The bytes and their MIME type.
|
|
15261
|
+
* @throws {Error} When the response is not ok.
|
|
15262
|
+
*/
|
|
15263
|
+
async function loadSound(run, file) {
|
|
15264
|
+
const soundPath = pathOf(run.bundle, file);
|
|
15265
|
+
return {
|
|
15266
|
+
bytes: await (await fetchFile(run, soundPath)).arrayBuffer(),
|
|
15267
|
+
mime: mimeOf(soundPath)
|
|
15268
|
+
};
|
|
15269
|
+
}
|
|
15270
|
+
/**
|
|
15205
15271
|
* Starts the one load of every atlas page of a bundle. A page lands in the `pages` map of the
|
|
15206
15272
|
* running load, so a failure later frees it with the rest. Nobody may wait for a page, so its
|
|
15207
15273
|
* failure is marked as handled here; the files on it still reject with it.
|
|
@@ -15269,12 +15335,7 @@ async function loadFile(run, file) {
|
|
|
15269
15335
|
return;
|
|
15270
15336
|
}
|
|
15271
15337
|
if (kind === "audio") {
|
|
15272
|
-
|
|
15273
|
-
const response = await fetchFile(run, soundPath);
|
|
15274
|
-
assets.audio.set(file.key, {
|
|
15275
|
-
bytes: await response.arrayBuffer(),
|
|
15276
|
-
mime: mimeOf(soundPath)
|
|
15277
|
-
});
|
|
15338
|
+
assets.audio.set(file.key, await loadSound(run, file));
|
|
15278
15339
|
return;
|
|
15279
15340
|
}
|
|
15280
15341
|
const texture = file.atlas === void 0 ? await loadImage(run, pathOf(run.bundle, file), nineOf(file)) : await sliceFile(run, file, file.atlas);
|
|
@@ -16326,6 +16387,8 @@ function releaseAll(state) {
|
|
|
16326
16387
|
state.warned.clear();
|
|
16327
16388
|
state.current = void 0;
|
|
16328
16389
|
state.sceneBundle = void 0;
|
|
16390
|
+
state.stamps = void 0;
|
|
16391
|
+
state.swapping = void 0;
|
|
16329
16392
|
state.io = void 0;
|
|
16330
16393
|
}
|
|
16331
16394
|
//#endregion
|
|
@@ -16472,36 +16535,551 @@ function createAssetsApi(ctx) {
|
|
|
16472
16535
|
};
|
|
16473
16536
|
}
|
|
16474
16537
|
//#endregion
|
|
16538
|
+
//#region src/plugins/assets/paths.ts
|
|
16539
|
+
/** The name of the folder of a feature or of a layer the scanner reads. */
|
|
16540
|
+
const ASSETS = "assets";
|
|
16541
|
+
/** That folder inside a path. */
|
|
16542
|
+
const ASSETS_FOLDER = `/${ASSETS}/`;
|
|
16543
|
+
/**
|
|
16544
|
+
* Reads the folder the scanner found a manifest path in. The scanner reads the `assets/` folder
|
|
16545
|
+
* of a feature or of a layer, so the path is cut after it; a path with no such folder, as in a
|
|
16546
|
+
* manifest written by hand, gives the folder of the file.
|
|
16547
|
+
*
|
|
16548
|
+
* @param path - A path of the manifest.
|
|
16549
|
+
* @returns The folder, ending in a slash, or `""` for a file at the root.
|
|
16550
|
+
* @example
|
|
16551
|
+
* ```ts
|
|
16552
|
+
* folderOf("features/ui/assets/fx/leaf.webp"); // "features/ui/assets/"
|
|
16553
|
+
* ```
|
|
16554
|
+
*/
|
|
16555
|
+
function folderOf(path) {
|
|
16556
|
+
const scanned = path.indexOf(ASSETS_FOLDER);
|
|
16557
|
+
if (scanned !== -1) return path.slice(0, scanned + ASSETS_FOLDER.length);
|
|
16558
|
+
return path.slice(0, path.lastIndexOf("/") + 1);
|
|
16559
|
+
}
|
|
16560
|
+
/**
|
|
16561
|
+
* Tells whether the scanner would read a path, by its shape alone. It mirrors `scanOwner` of
|
|
16562
|
+
* `scan/scan.ts`, which runtime code does not import: the scanner reads `<layer>/assets/` and
|
|
16563
|
+
* `<features>/<feature>/assets/` under the game root, so `assets` is the second or the third
|
|
16564
|
+
* segment and a file follows it. The page knows neither the layers nor the name of the features
|
|
16565
|
+
* folder, so any folder counts as one. This is what finds the first file of an `assets/` folder
|
|
16566
|
+
* the booted manifest names nowhere.
|
|
16567
|
+
*
|
|
16568
|
+
* @param path - A path of the game, root-relative with `/`.
|
|
16569
|
+
* @returns True for a path under the `assets/` of a layer or of a feature.
|
|
16570
|
+
* @example
|
|
16571
|
+
* ```ts
|
|
16572
|
+
* isScannerPath("features/shop/assets/coin.png"); // true
|
|
16573
|
+
* ```
|
|
16574
|
+
*/
|
|
16575
|
+
function isScannerPath(path) {
|
|
16576
|
+
const segments = path.split("/");
|
|
16577
|
+
const inLayer = segments[1] === ASSETS && segments.length > 2;
|
|
16578
|
+
const inFeature = segments[2] === ASSETS && segments.length > 3;
|
|
16579
|
+
return inLayer || inFeature;
|
|
16580
|
+
}
|
|
16581
|
+
/**
|
|
16582
|
+
* Builds the path index of a manifest: which file and bundle every path leads to, and the folders
|
|
16583
|
+
* the scanner read. A path under one of those folders that leads nowhere is a new asset.
|
|
16584
|
+
*
|
|
16585
|
+
* @param manifest - The manifest the page booted with.
|
|
16586
|
+
* @returns The index.
|
|
16587
|
+
*/
|
|
16588
|
+
function indexPaths(manifest) {
|
|
16589
|
+
const owners = /* @__PURE__ */ new Map();
|
|
16590
|
+
const folders = /* @__PURE__ */ new Set();
|
|
16591
|
+
for (const [bundle, entry] of Object.entries(manifest.bundles)) for (const file of entry.files) {
|
|
16592
|
+
if (file.path === void 0) continue;
|
|
16593
|
+
const owner = {
|
|
16594
|
+
bundle,
|
|
16595
|
+
entry,
|
|
16596
|
+
file,
|
|
16597
|
+
path: file.path
|
|
16598
|
+
};
|
|
16599
|
+
for (const path of [file.path, ...(file.pages ?? []).map((page) => page.path)]) {
|
|
16600
|
+
owners.set(path, owner);
|
|
16601
|
+
folders.add(folderOf(path));
|
|
16602
|
+
}
|
|
16603
|
+
}
|
|
16604
|
+
folders.delete("");
|
|
16605
|
+
return {
|
|
16606
|
+
manifest,
|
|
16607
|
+
owners,
|
|
16608
|
+
folders: [...folders]
|
|
16609
|
+
};
|
|
16610
|
+
}
|
|
16611
|
+
/**
|
|
16612
|
+
* Tells why a stamp cannot be swapped in place: the set of files is not the one of the manifest
|
|
16613
|
+
* the page booted with. A manifest path the watch no longer stamps was removed or renamed, or its
|
|
16614
|
+
* nine-slice tag changed. A changed path the manifest does not know is a new asset when it lies
|
|
16615
|
+
* under a folder the manifest names, or where the scanner reads: the first file of an `assets/`
|
|
16616
|
+
* folder that had none counts too. The watch stamps every image of the game tree, so any other
|
|
16617
|
+
* unknown path (a favicon, an image of a feature outside its `assets/`) refuses nothing.
|
|
16618
|
+
*
|
|
16619
|
+
* @param index - The path index of the manifest.
|
|
16620
|
+
* @param stamps - The stamp that arrived.
|
|
16621
|
+
* @param changed - The paths whose bytes changed.
|
|
16622
|
+
* @returns The reason, or `undefined` when only bytes changed.
|
|
16623
|
+
*/
|
|
16624
|
+
function refusalOf$1(index, stamps, changed) {
|
|
16625
|
+
for (const path of index.owners.keys()) if (!Object.hasOwn(stamps.files, path)) return `"${path}" left the game`;
|
|
16626
|
+
for (const path of changed) {
|
|
16627
|
+
if (index.owners.has(path)) continue;
|
|
16628
|
+
if (isScannerPath(path) || index.folders.some((folder) => path.startsWith(folder))) return `"${path}" is not in the manifest`;
|
|
16629
|
+
}
|
|
16630
|
+
}
|
|
16631
|
+
/**
|
|
16632
|
+
* Groups the changed files by their bundle. A font whose `.fnt` and pages changed in one save is
|
|
16633
|
+
* listed once: a font is replaced as a whole. A path the manifest does not know is left out.
|
|
16634
|
+
*
|
|
16635
|
+
* @param index - The path index of the manifest.
|
|
16636
|
+
* @param changed - The paths whose bytes changed.
|
|
16637
|
+
* @returns One batch per bundle, in the order the paths name them.
|
|
16638
|
+
*/
|
|
16639
|
+
function batchesOf(index, changed) {
|
|
16640
|
+
const batches = /* @__PURE__ */ new Map();
|
|
16641
|
+
for (const path of changed) {
|
|
16642
|
+
const owner = index.owners.get(path);
|
|
16643
|
+
if (owner === void 0) continue;
|
|
16644
|
+
const { bundle, entry } = owner;
|
|
16645
|
+
const batch = batches.get(bundle) ?? {
|
|
16646
|
+
bundle,
|
|
16647
|
+
entry,
|
|
16648
|
+
owners: []
|
|
16649
|
+
};
|
|
16650
|
+
if (!batch.owners.includes(owner)) batch.owners.push(owner);
|
|
16651
|
+
batches.set(bundle, batch);
|
|
16652
|
+
}
|
|
16653
|
+
return [...batches.values()];
|
|
16654
|
+
}
|
|
16655
|
+
//#endregion
|
|
16656
|
+
//#region src/plugins/assets/stamp.ts
|
|
16657
|
+
/** The module the keys watch writes, as the hot footer reports its path. */
|
|
16658
|
+
const STAMP_FILE = "/.moku/assets-stamp.ts";
|
|
16659
|
+
/**
|
|
16660
|
+
* Tells whether a value maps paths to stamps.
|
|
16661
|
+
*
|
|
16662
|
+
* @param value - The `files` member of a stamp module.
|
|
16663
|
+
* @returns True for a plain object whose values are all strings.
|
|
16664
|
+
* @example
|
|
16665
|
+
* ```ts
|
|
16666
|
+
* isStampMap({ "features/ui/assets/fx-spark.webp": "2554:1791536552578" }); // true
|
|
16667
|
+
* ```
|
|
16668
|
+
*/
|
|
16669
|
+
function isStampMap(value) {
|
|
16670
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((stamp) => typeof stamp === "string");
|
|
16671
|
+
}
|
|
16672
|
+
/**
|
|
16673
|
+
* Tells whether a value is a list of paths.
|
|
16674
|
+
*
|
|
16675
|
+
* @param value - The `changed` member of a stamp module.
|
|
16676
|
+
* @returns True for an array of strings.
|
|
16677
|
+
* @example
|
|
16678
|
+
* ```ts
|
|
16679
|
+
* isPathList(["features/ui/assets/fx-spark.webp"]); // true
|
|
16680
|
+
* ```
|
|
16681
|
+
*/
|
|
16682
|
+
function isPathList(value) {
|
|
16683
|
+
return Array.isArray(value) && value.every((path) => typeof path === "string");
|
|
16684
|
+
}
|
|
16685
|
+
/**
|
|
16686
|
+
* Tells the stamp of the keys watch from any other default export.
|
|
16687
|
+
*
|
|
16688
|
+
* @param value - The default export of the stamp module.
|
|
16689
|
+
* @returns True when it carries a `files` map of strings and a `changed` list of strings.
|
|
16690
|
+
* @example
|
|
16691
|
+
* ```ts
|
|
16692
|
+
* isAssetStamps({ files: { "features/ui/assets/popup.mp3": "8120:1791536000000" }, changed: [] }); // true
|
|
16693
|
+
* ```
|
|
16694
|
+
*/
|
|
16695
|
+
function isAssetStamps(value) {
|
|
16696
|
+
if (typeof value !== "object" || value === null) return false;
|
|
16697
|
+
if (!("files" in value) || !("changed" in value)) return false;
|
|
16698
|
+
return isStampMap(value.files) && isPathList(value.changed);
|
|
16699
|
+
}
|
|
16700
|
+
/**
|
|
16701
|
+
* Reads the stamp out of a hot swap, when the saved module is the stamp of the keys watch.
|
|
16702
|
+
*
|
|
16703
|
+
* @param payload - The saved file and its new exports.
|
|
16704
|
+
* @returns The stamp, or `undefined` for any other file and for a module that carries none.
|
|
16705
|
+
* @example
|
|
16706
|
+
* ```ts
|
|
16707
|
+
* stampsOf({ file: "/game/features/hud/view.tsx", module: {} }); // undefined
|
|
16708
|
+
* ```
|
|
16709
|
+
*/
|
|
16710
|
+
function stampsOf(payload) {
|
|
16711
|
+
if (!payload.file.replaceAll("\\", "/").endsWith(STAMP_FILE)) return void 0;
|
|
16712
|
+
const stamps = payload.module.default;
|
|
16713
|
+
return isAssetStamps(stamps) ? stamps : void 0;
|
|
16714
|
+
}
|
|
16715
|
+
/**
|
|
16716
|
+
* Works out which files a stamp changed, and keeps its map for the next one. Against the map
|
|
16717
|
+
* applied last it is every path whose stamp differs. The first update after boot has no map to
|
|
16718
|
+
* compare with, so the list of the keys watch stands.
|
|
16719
|
+
*
|
|
16720
|
+
* @param state - The plugin state.
|
|
16721
|
+
* @param stamps - The stamp that arrived.
|
|
16722
|
+
* @returns The changed paths.
|
|
16723
|
+
*/
|
|
16724
|
+
function changedPaths(state, stamps) {
|
|
16725
|
+
const applied = state.stamps;
|
|
16726
|
+
state.stamps = stamps.files;
|
|
16727
|
+
if (applied === void 0) return stamps.changed;
|
|
16728
|
+
return Object.keys(stamps.files).filter((path) => stamps.files[path] !== applied[path]);
|
|
16729
|
+
}
|
|
16730
|
+
//#endregion
|
|
16731
|
+
//#region src/plugins/assets/swap.ts
|
|
16732
|
+
/**
|
|
16733
|
+
* @file assets plugin — the dev hot swap of asset files. When the stamp of the keys watch changes
|
|
16734
|
+
* (`stamp.ts`), the changed files of the loaded bundles (`paths.ts`) are replaced here: a new
|
|
16735
|
+
* texture, font or sound per key, the old textures destroyed after the new ones are stored, then
|
|
16736
|
+
* `assets:replaced`. A file that fails is logged and its bundle keeps what it had. A changed set
|
|
16737
|
+
* of files reloads the page instead: a hook cannot refuse by throwing, the core bus catches what
|
|
16738
|
+
* a hook throws.
|
|
16739
|
+
*/
|
|
16740
|
+
/**
|
|
16741
|
+
* Reloads the page. This is how a swap refuses, and what it does when a replace breaks after its
|
|
16742
|
+
* files arrived: the page boots again with the new manifest and the new files. Headless and in a
|
|
16743
|
+
* test there is no page, so nothing happens.
|
|
16744
|
+
*/
|
|
16745
|
+
function reloadPage$1() {
|
|
16746
|
+
globalThis.location?.reload();
|
|
16747
|
+
}
|
|
16748
|
+
/**
|
|
16749
|
+
* Says why a file could not be replaced.
|
|
16750
|
+
*
|
|
16751
|
+
* @param error - What the load threw.
|
|
16752
|
+
* @returns The message of an error, or the value as a string.
|
|
16753
|
+
* @example
|
|
16754
|
+
* ```ts
|
|
16755
|
+
* reasonOf(new Error("The source image could not be decoded.")); // "The source image could not be decoded."
|
|
16756
|
+
* ```
|
|
16757
|
+
*/
|
|
16758
|
+
function reasonOf$1(error) {
|
|
16759
|
+
return error instanceof Error ? error.message : String(error);
|
|
16760
|
+
}
|
|
16761
|
+
/**
|
|
16762
|
+
* Copies the io of a swap so every texture it uploads is remembered with the pixel size of its
|
|
16763
|
+
* image. The manifest needs that size, and the loaders of `tiers.ts` answer the texture alone;
|
|
16764
|
+
* the list is also what a swap that does not land gives back. The io is a plain record of
|
|
16765
|
+
* functions, so the copy carries the other four members as they are.
|
|
16766
|
+
*
|
|
16767
|
+
* @param io - The I/O seam of the plugin.
|
|
16768
|
+
* @param sizes - Filled with every texture this io creates.
|
|
16769
|
+
* @returns The io with a measuring `createTexture`.
|
|
16770
|
+
*/
|
|
16771
|
+
function measuringIo(io, sizes) {
|
|
16772
|
+
return {
|
|
16773
|
+
...io,
|
|
16774
|
+
createTexture: (image, options) => {
|
|
16775
|
+
const texture = io.createTexture(image, options);
|
|
16776
|
+
sizes.set(texture, {
|
|
16777
|
+
width: image.width,
|
|
16778
|
+
height: image.height
|
|
16779
|
+
});
|
|
16780
|
+
return texture;
|
|
16781
|
+
}
|
|
16782
|
+
};
|
|
16783
|
+
}
|
|
16784
|
+
/**
|
|
16785
|
+
* Builds the running load the file loaders of `tiers.ts` take, for the files of one bundle.
|
|
16786
|
+
*
|
|
16787
|
+
* @param swap - The running swap.
|
|
16788
|
+
* @param bundle - Name of the bundle.
|
|
16789
|
+
* @param sizes - Filled with every texture the load creates.
|
|
16790
|
+
* @returns The running load.
|
|
16791
|
+
*/
|
|
16792
|
+
function loadingOf(swap, bundle, sizes) {
|
|
16793
|
+
const { config } = swap.ctx;
|
|
16794
|
+
return {
|
|
16795
|
+
io: measuringIo(swap.io, sizes),
|
|
16796
|
+
bundle,
|
|
16797
|
+
base: resolveBaseUrl(config.baseUrl, config.manifest),
|
|
16798
|
+
signal: new AbortController().signal,
|
|
16799
|
+
assets: emptyAssets()
|
|
16800
|
+
};
|
|
16801
|
+
}
|
|
16802
|
+
/**
|
|
16803
|
+
* Loads one changed file again, by what the manifest says it is. A font is read as a whole: its
|
|
16804
|
+
* `.fnt` and every page, whichever of them changed.
|
|
16805
|
+
*
|
|
16806
|
+
* @param run - The running load.
|
|
16807
|
+
* @param owner - The file to load.
|
|
16808
|
+
* @returns What the file brought.
|
|
16809
|
+
* @throws {Error} When a fetch or a decode fails, or the font lists no page.
|
|
16810
|
+
*/
|
|
16811
|
+
async function loadFresh(run, owner) {
|
|
16812
|
+
const kind = kindOf$2(owner.file);
|
|
16813
|
+
if (kind === "font") return {
|
|
16814
|
+
kind,
|
|
16815
|
+
owner,
|
|
16816
|
+
font: await loadFont(run, owner.file)
|
|
16817
|
+
};
|
|
16818
|
+
if (kind === "audio") return {
|
|
16819
|
+
kind,
|
|
16820
|
+
owner,
|
|
16821
|
+
audio: await loadSound(run, owner.file)
|
|
16822
|
+
};
|
|
16823
|
+
return {
|
|
16824
|
+
kind,
|
|
16825
|
+
owner,
|
|
16826
|
+
texture: await loadImage(run, owner.path, nineOf(owner.file))
|
|
16827
|
+
};
|
|
16828
|
+
}
|
|
16829
|
+
/**
|
|
16830
|
+
* Loads every changed file of one bundle in parallel and waits for all of them, so nothing is
|
|
16831
|
+
* still on the way when the bundle is stored or given back.
|
|
16832
|
+
*
|
|
16833
|
+
* @param run - The running load.
|
|
16834
|
+
* @param owners - The files to load.
|
|
16835
|
+
* @returns What arrived, and the files that failed.
|
|
16836
|
+
*/
|
|
16837
|
+
async function loadAll(run, owners) {
|
|
16838
|
+
const fresh = [];
|
|
16839
|
+
const failures = [];
|
|
16840
|
+
await Promise.all(owners.map((owner) => loadFresh(run, owner).then((loaded) => fresh.push(loaded), (error) => failures.push({
|
|
16841
|
+
path: owner.path,
|
|
16842
|
+
reason: reasonOf$1(error)
|
|
16843
|
+
}))));
|
|
16844
|
+
return {
|
|
16845
|
+
fresh,
|
|
16846
|
+
failures
|
|
16847
|
+
};
|
|
16848
|
+
}
|
|
16849
|
+
/**
|
|
16850
|
+
* Reads the record of a bundle once it stands still. A load that runs when the stamp arrives may
|
|
16851
|
+
* have fetched the old bytes, so it lands first.
|
|
16852
|
+
*
|
|
16853
|
+
* @param state - The plugin state.
|
|
16854
|
+
* @param bundle - Name of the bundle.
|
|
16855
|
+
* @returns The record when the bundle is loaded, else `undefined`: its next load reads the new
|
|
16856
|
+
* bytes, the dev server answers every file with `no-store`.
|
|
16857
|
+
*/
|
|
16858
|
+
async function settledRecord(state, bundle) {
|
|
16859
|
+
const record = state.records.get(bundle);
|
|
16860
|
+
if (record?.status === "loading") await record.inflight?.promise;
|
|
16861
|
+
return record?.status === "loaded" ? record : void 0;
|
|
16862
|
+
}
|
|
16863
|
+
/**
|
|
16864
|
+
* Writes the pixel size of a new image, and what it costs, into its manifest record.
|
|
16865
|
+
*
|
|
16866
|
+
* @param target - The record of a texture file or of a font page.
|
|
16867
|
+
* @param size - The size of the decoded image.
|
|
16868
|
+
*/
|
|
16869
|
+
function resize(target, size) {
|
|
16870
|
+
if (target === void 0 || size === void 0) return;
|
|
16871
|
+
target.width = size.width;
|
|
16872
|
+
target.height = size.height;
|
|
16873
|
+
target.mb = textureMb(size.width, size.height);
|
|
16874
|
+
}
|
|
16875
|
+
/**
|
|
16876
|
+
* Stores a new font under its key and frees the pages of the old one. The record of each page and
|
|
16877
|
+
* of the font follow the new images.
|
|
16878
|
+
*
|
|
16879
|
+
* @param io - The I/O seam of the plugin.
|
|
16880
|
+
* @param record - The record of the loaded bundle.
|
|
16881
|
+
* @param owner - The font file.
|
|
16882
|
+
* @param font - The font that was read again.
|
|
16883
|
+
* @param sizes - The pixel size of every new page.
|
|
16884
|
+
*/
|
|
16885
|
+
function storeFont(io, record, owner, font, sizes) {
|
|
16886
|
+
const { file } = owner;
|
|
16887
|
+
const pages = file.pages ?? [];
|
|
16888
|
+
const old = record.fonts.get(file.key);
|
|
16889
|
+
record.fonts.set(file.key, font);
|
|
16890
|
+
for (const page of old?.pages ?? []) io.destroyTexture(page);
|
|
16891
|
+
for (const [at, texture] of font.pages.entries()) resize(pages[at], sizes.get(texture));
|
|
16892
|
+
file.mb = sumMb(pages);
|
|
16893
|
+
}
|
|
16894
|
+
/**
|
|
16895
|
+
* Stores what one changed file brought under its key. An old texture is destroyed only after the
|
|
16896
|
+
* new one is stored, so nothing ever answers a destroyed texture.
|
|
16897
|
+
*
|
|
16898
|
+
* @param io - The I/O seam of the plugin.
|
|
16899
|
+
* @param record - The record of the loaded bundle.
|
|
16900
|
+
* @param fresh - What the file brought.
|
|
16901
|
+
* @param sizes - The pixel size of every new texture.
|
|
16902
|
+
*/
|
|
16903
|
+
function store(io, record, fresh, sizes) {
|
|
16904
|
+
const { file } = fresh.owner;
|
|
16905
|
+
if (fresh.kind === "font") {
|
|
16906
|
+
storeFont(io, record, fresh.owner, fresh.font, sizes);
|
|
16907
|
+
return;
|
|
16908
|
+
}
|
|
16909
|
+
if (fresh.kind === "audio") {
|
|
16910
|
+
record.audio.set(file.key, fresh.audio);
|
|
16911
|
+
return;
|
|
16912
|
+
}
|
|
16913
|
+
const old = record.textures.get(file.key);
|
|
16914
|
+
record.textures.set(file.key, fresh.texture);
|
|
16915
|
+
if (old !== void 0) io.destroyTexture(old);
|
|
16916
|
+
resize(file, sizes.get(fresh.texture));
|
|
16917
|
+
}
|
|
16918
|
+
/**
|
|
16919
|
+
* Makes the numbers and the picture follow the files that were stored: the megabytes of the
|
|
16920
|
+
* bundle, the budget, the views of the keys, the frame loop and the event.
|
|
16921
|
+
*
|
|
16922
|
+
* @param ctx - Domain context of the plugin.
|
|
16923
|
+
* @param batch - The bundle and its replaced files.
|
|
16924
|
+
* @param record - The record of the bundle.
|
|
16925
|
+
*/
|
|
16926
|
+
function publish(ctx, batch, record) {
|
|
16927
|
+
const { bundle, entry } = batch;
|
|
16928
|
+
const keys = batch.owners.map((owner) => owner.file.key);
|
|
16929
|
+
entry.mb = sumMb([...entry.files, ...entry.pages ?? []]);
|
|
16930
|
+
enforceBudget(ctx);
|
|
16931
|
+
if (record.status !== "loaded") return;
|
|
16932
|
+
ctx.deps.renderer.sync.textures.invalidate(keys);
|
|
16933
|
+
ctx.deps.time.wake();
|
|
16934
|
+
ctx.emit("assets:replaced", {
|
|
16935
|
+
bundle,
|
|
16936
|
+
keys
|
|
16937
|
+
});
|
|
16938
|
+
}
|
|
16939
|
+
/**
|
|
16940
|
+
* Gives the new files of one bundle up: every file that failed is logged, and every texture the
|
|
16941
|
+
* swap uploaded goes back. Nothing was stored, so the bundle keeps what it had.
|
|
16942
|
+
*
|
|
16943
|
+
* @param swap - The running swap.
|
|
16944
|
+
* @param failures - The files that could not be read.
|
|
16945
|
+
* @param sizes - Every new texture of the bundle.
|
|
16946
|
+
*/
|
|
16947
|
+
function discard(swap, failures, sizes) {
|
|
16948
|
+
for (const failure of failures) swap.ctx.log.error("assets:replace-failed", failure);
|
|
16949
|
+
for (const texture of sizes.keys()) swap.io.destroyTexture(texture);
|
|
16950
|
+
}
|
|
16951
|
+
/**
|
|
16952
|
+
* Replaces the changed files of one bundle. Every file is loaded first and nothing is stored
|
|
16953
|
+
* before all of them are there. A file that fails is logged and the bundle keeps its old
|
|
16954
|
+
* textures, fonts and bytes: the new textures are given back, nothing is emitted and the page
|
|
16955
|
+
* stays. The stamp of the broken file is applied all the same, so it is read again only when it
|
|
16956
|
+
* is saved again. A bundle that was unloaded on the way gives its new textures back too.
|
|
16957
|
+
*
|
|
16958
|
+
* @param swap - The running swap.
|
|
16959
|
+
* @param batch - The bundle and its changed files.
|
|
16960
|
+
* @throws {Error} What the io, the renderer or the budget throws after the files arrived.
|
|
16961
|
+
*/
|
|
16962
|
+
async function replaceBundle(swap, batch) {
|
|
16963
|
+
const { ctx, io } = swap;
|
|
16964
|
+
const record = await settledRecord(ctx.state, batch.bundle);
|
|
16965
|
+
if (record === void 0) return;
|
|
16966
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
16967
|
+
const { fresh, failures } = await loadAll(loadingOf(swap, batch.bundle, sizes), batch.owners);
|
|
16968
|
+
if (failures.length > 0 || record.status !== "loaded") {
|
|
16969
|
+
discard(swap, failures, sizes);
|
|
16970
|
+
return;
|
|
16971
|
+
}
|
|
16972
|
+
for (const loaded of fresh) store(io, record, loaded, sizes);
|
|
16973
|
+
publish(ctx, batch, record);
|
|
16974
|
+
}
|
|
16975
|
+
/**
|
|
16976
|
+
* Runs one swap, bundle after bundle: a bundle with a file that failed does not stop the next
|
|
16977
|
+
* one. It never rejects, so the chain of swaps in the state cannot break. A throw after the files
|
|
16978
|
+
* arrived (from a destroy, the views or the budget) leaves a bundle whose stored state is not
|
|
16979
|
+
* known: every file of it is logged as failed, the page reloads and the swap stops there.
|
|
16980
|
+
*
|
|
16981
|
+
* @param swap - The running swap.
|
|
16982
|
+
* @param batches - The changed files, by bundle.
|
|
16983
|
+
*/
|
|
16984
|
+
async function replaceAll(swap, batches) {
|
|
16985
|
+
for (const batch of batches) try {
|
|
16986
|
+
await replaceBundle(swap, batch);
|
|
16987
|
+
} catch (error) {
|
|
16988
|
+
const reason = reasonOf$1(error);
|
|
16989
|
+
for (const { path } of batch.owners) swap.ctx.log.error("assets:replace-failed", {
|
|
16990
|
+
path,
|
|
16991
|
+
reason
|
|
16992
|
+
});
|
|
16993
|
+
swap.reload();
|
|
16994
|
+
return;
|
|
16995
|
+
}
|
|
16996
|
+
}
|
|
16997
|
+
/**
|
|
16998
|
+
* Creates the handler of the dev hot swap of asset files. It acts only on the stamp of the keys
|
|
16999
|
+
* watch, and only with an io: a headless page has no file. What changed and whether the swap is
|
|
17000
|
+
* refused is decided at once; the replace itself waits for the swap before it, so two saves in a
|
|
17001
|
+
* row never overlap.
|
|
17002
|
+
*
|
|
17003
|
+
* @param ctx - Domain context of the plugin.
|
|
17004
|
+
* @param reload - What a refused or a broken swap calls. The page reload by default; a test
|
|
17005
|
+
* passes a spy.
|
|
17006
|
+
* @returns The handler of `ui:hot-swap`.
|
|
17007
|
+
*/
|
|
17008
|
+
function createSwap$1(ctx, reload = reloadPage$1) {
|
|
17009
|
+
let index;
|
|
17010
|
+
return (payload) => {
|
|
17011
|
+
const { state } = ctx;
|
|
17012
|
+
const stamps = stampsOf(payload);
|
|
17013
|
+
const io = state.io;
|
|
17014
|
+
if (stamps === void 0 || io === void 0) return;
|
|
17015
|
+
const changed = changedPaths(state, stamps);
|
|
17016
|
+
if (index?.manifest !== state.manifest) index = indexPaths(state.manifest);
|
|
17017
|
+
const reason = refusalOf$1(index, stamps, changed);
|
|
17018
|
+
if (reason !== void 0) {
|
|
17019
|
+
ctx.log.info("assets:swap-refused", { reason });
|
|
17020
|
+
reload();
|
|
17021
|
+
return;
|
|
17022
|
+
}
|
|
17023
|
+
const batches = batchesOf(index, changed);
|
|
17024
|
+
if (batches.length === 0) return;
|
|
17025
|
+
state.swapping = (state.swapping ?? Promise.resolve()).then(() => replaceAll({
|
|
17026
|
+
ctx,
|
|
17027
|
+
io,
|
|
17028
|
+
reload
|
|
17029
|
+
}, batches));
|
|
17030
|
+
};
|
|
17031
|
+
}
|
|
17032
|
+
//#endregion
|
|
16475
17033
|
//#region src/plugins/assets/handlers.ts
|
|
16476
17034
|
/**
|
|
16477
|
-
* @file assets plugin — event handlers
|
|
17035
|
+
* @file assets plugin — event handlers: the rest node that settles the memory, and the dev hot
|
|
17036
|
+
* swap that replaces the asset files a save changed.
|
|
16478
17037
|
*/
|
|
16479
17038
|
/**
|
|
16480
17039
|
* Creates the hook handlers. `flow:rest` is the one moment the graph stands still, so it is where
|
|
16481
17040
|
* the memory is settled and the neighbourhood of the position is queued. The loading itself is a
|
|
16482
|
-
* background queue in the state and is never awaited here: an engine hook is synchronous.
|
|
17041
|
+
* background queue in the state and is never awaited here: an engine hook is synchronous. The
|
|
17042
|
+
* global `ui:hot-swap` is hooked with no `depends` on `ui`, as `effects` does: in a dev build it
|
|
17043
|
+
* brings the stamp of the keys watch, and the changed files are replaced behind it.
|
|
16483
17044
|
*
|
|
16484
|
-
*
|
|
17045
|
+
* A handler builds its domain context when it first fires, not while this factory runs: the
|
|
16485
17046
|
* kernel registers hooks before it builds the plugin APIs, so nothing is resolvable yet.
|
|
16486
17047
|
*
|
|
16487
17048
|
* @param ctx - Kernel context of the assets plugin.
|
|
16488
|
-
* @
|
|
17049
|
+
* @param reload - What a refused or a broken hot swap calls. The kernel passes nothing, which
|
|
17050
|
+
* means the page reload; a test passes a spy.
|
|
17051
|
+
* @returns The two hooks of the plugin.
|
|
16489
17052
|
*/
|
|
16490
|
-
function createHandlers$4(ctx) {
|
|
17053
|
+
function createHandlers$4(ctx, reload) {
|
|
16491
17054
|
let assets;
|
|
16492
|
-
|
|
16493
|
-
|
|
16494
|
-
|
|
16495
|
-
|
|
16496
|
-
|
|
16497
|
-
|
|
16498
|
-
|
|
16499
|
-
|
|
16500
|
-
|
|
16501
|
-
|
|
16502
|
-
|
|
16503
|
-
|
|
16504
|
-
|
|
17055
|
+
let swap;
|
|
17056
|
+
return {
|
|
17057
|
+
/**
|
|
17058
|
+
* Settles the texture memory and rebuilds the background preload.
|
|
17059
|
+
*
|
|
17060
|
+
* @param _payload - Where the graph rests. The position itself comes from the state.
|
|
17061
|
+
*/
|
|
17062
|
+
"flow:rest": (_payload) => {
|
|
17063
|
+
assets ??= withDeps$6(ctx);
|
|
17064
|
+
if (assets.state.io === void 0) return;
|
|
17065
|
+
enforceBudget(assets);
|
|
17066
|
+
if (assets.deps.flow.state().mode === "fast") return;
|
|
17067
|
+
startPreload(assets);
|
|
17068
|
+
},
|
|
17069
|
+
/**
|
|
17070
|
+
* Replaces the asset files a dev save changed, when the saved module is the stamp of the keys
|
|
17071
|
+
* watch. Only dev emits the event; the swap answers at once and works behind it.
|
|
17072
|
+
*
|
|
17073
|
+
* @param payload - The saved file and its new exports.
|
|
17074
|
+
*/
|
|
17075
|
+
"ui:hot-swap": (payload) => {
|
|
17076
|
+
if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__) {
|
|
17077
|
+
assets ??= withDeps$6(ctx);
|
|
17078
|
+
swap ??= createSwap$1(assets, reload);
|
|
17079
|
+
swap(payload);
|
|
17080
|
+
}
|
|
17081
|
+
}
|
|
17082
|
+
};
|
|
16505
17083
|
}
|
|
16506
17084
|
//#endregion
|
|
16507
17085
|
//#region src/plugins/assets/state.ts
|
|
@@ -16550,7 +17128,9 @@ function createAssetsState() {
|
|
|
16550
17128
|
current: void 0,
|
|
16551
17129
|
useCounter: 0,
|
|
16552
17130
|
warned: emptySet$2(),
|
|
16553
|
-
removers: []
|
|
17131
|
+
removers: [],
|
|
17132
|
+
stamps: void 0,
|
|
17133
|
+
swapping: void 0
|
|
16554
17134
|
};
|
|
16555
17135
|
}
|
|
16556
17136
|
/**
|
|
@@ -16584,7 +17164,8 @@ const assetsPlugin = /*#__PURE__*/ createPlugin$1("assets", {
|
|
|
16584
17164
|
events: (register) => register.map({
|
|
16585
17165
|
"assets:bundle-loaded": "Every file of a bundle is a texture now",
|
|
16586
17166
|
"assets:bundle-progress": "One more file of a running bundle load settled",
|
|
16587
|
-
"assets:bundle-unloaded": "The textures of a bundle were destroyed"
|
|
17167
|
+
"assets:bundle-unloaded": "The textures of a bundle were destroyed",
|
|
17168
|
+
"assets:replaced": "Files of a loaded bundle were replaced by a dev hot swap"
|
|
16588
17169
|
}),
|
|
16589
17170
|
createState: createAssetsState,
|
|
16590
17171
|
api: createAssetsApi,
|
|
@@ -17868,9 +18449,9 @@ async function stopAudio(state) {
|
|
|
17868
18449
|
//#endregion
|
|
17869
18450
|
//#region src/plugins/audio/handlers.ts
|
|
17870
18451
|
/**
|
|
17871
|
-
* @file audio plugin — the
|
|
17872
|
-
* sets the volumes, the graph sets the music, the OS sets the pause and the budget
|
|
17873
|
-
* still decoded.
|
|
18452
|
+
* @file audio plugin — the five hooks. They are the whole of what drives this plugin: the player
|
|
18453
|
+
* sets the volumes, the graph sets the music, the OS sets the pause, and the budget and a dev hot
|
|
18454
|
+
* swap set what is still decoded.
|
|
17874
18455
|
*/
|
|
17875
18456
|
/**
|
|
17876
18457
|
* Applies the volumes of the committed player. A `volumes` that throws — a save without the
|
|
@@ -17921,12 +18502,25 @@ function resumeAfterPause(ctx) {
|
|
|
17921
18502
|
});
|
|
17922
18503
|
}
|
|
17923
18504
|
/**
|
|
17924
|
-
*
|
|
18505
|
+
* Forgets the decoded buffer and the one warning of every key named. A key that is no sound is in
|
|
18506
|
+
* neither, so it costs nothing. A source that is playing holds its own buffer and plays on.
|
|
18507
|
+
*
|
|
18508
|
+
* @param state - The plugin state.
|
|
18509
|
+
* @param keys - Asset keys whose bytes left or changed.
|
|
18510
|
+
*/
|
|
18511
|
+
function evict(state, keys) {
|
|
18512
|
+
for (const key of keys) {
|
|
18513
|
+
state.decoded.delete(key);
|
|
18514
|
+
state.warned.delete(key);
|
|
18515
|
+
}
|
|
18516
|
+
}
|
|
18517
|
+
/**
|
|
18518
|
+
* Creates the five hook handlers. Each one builds the domain context when it first fires, not
|
|
17925
18519
|
* while this factory runs: the kernel registers hooks before it builds the plugin APIs, so
|
|
17926
18520
|
* nothing is resolvable yet.
|
|
17927
18521
|
*
|
|
17928
18522
|
* @param ctx - Kernel context of the audio plugin.
|
|
17929
|
-
* @returns The
|
|
18523
|
+
* @returns The five hooks of the plugin.
|
|
17930
18524
|
*/
|
|
17931
18525
|
function createHandlers$3(ctx) {
|
|
17932
18526
|
let audio;
|
|
@@ -17986,11 +18580,17 @@ function createHandlers$3(ctx) {
|
|
|
17986
18580
|
* @param payload - The bundle that left and the keys it carried.
|
|
17987
18581
|
*/
|
|
17988
18582
|
"assets:bundle-unloaded": (payload) => {
|
|
17989
|
-
|
|
17990
|
-
|
|
17991
|
-
|
|
17992
|
-
|
|
17993
|
-
|
|
18583
|
+
evict(domain().state, payload.keys);
|
|
18584
|
+
},
|
|
18585
|
+
/**
|
|
18586
|
+
* Drops the decoded sounds a dev hot swap replaced: `assets.audio(key)` already answers the
|
|
18587
|
+
* new bytes, so the next play of a key decodes them. Nothing that is playing is stopped: a
|
|
18588
|
+
* source keeps the buffer it started with and a streamed track keeps its Blob.
|
|
18589
|
+
*
|
|
18590
|
+
* @param payload - The bundle whose files were replaced and the keys with new bytes.
|
|
18591
|
+
*/
|
|
18592
|
+
"assets:replaced": (payload) => {
|
|
18593
|
+
evict(domain().state, payload.keys);
|
|
17994
18594
|
}
|
|
17995
18595
|
};
|
|
17996
18596
|
}
|
|
@@ -20694,10 +21294,24 @@ function stopEffects(state) {
|
|
|
20694
21294
|
//#endregion
|
|
20695
21295
|
//#region src/plugins/effects/handlers.ts
|
|
20696
21296
|
/**
|
|
20697
|
-
* @file effects plugin — the
|
|
20698
|
-
* textures along, particles and `Displacement` maps alike, whatever their space;
|
|
20699
|
-
*
|
|
21297
|
+
* @file effects plugin — the three hooks: a bundle that left takes the effects drawn with its
|
|
21298
|
+
* textures along, particles and `Displacement` maps alike, whatever their space; so do the files
|
|
21299
|
+
* a dev hot swap replaced, whose effects are built again from the new textures; a dev hot swap of
|
|
21300
|
+
* a module replaces the registered emitters it exports.
|
|
21301
|
+
*/
|
|
21302
|
+
/**
|
|
21303
|
+
* Retires everything drawn with the textures of `keys`: the particle instances and their bakes,
|
|
21304
|
+
* the `Displacement` filters, and the one-shot warning of each key, so it can warn again. The
|
|
21305
|
+
* next frame builds each effect again from the texture its key answers then.
|
|
21306
|
+
*
|
|
21307
|
+
* @param effects - Domain context of the effects plugin.
|
|
21308
|
+
* @param keys - The asset keys whose textures are gone.
|
|
20700
21309
|
*/
|
|
21310
|
+
function retireKeys(effects, keys) {
|
|
21311
|
+
retireParticleKeys(effects, keys);
|
|
21312
|
+
retireFilterMaps(effects, keys);
|
|
21313
|
+
for (const key of keys) effects.state.warned.delete(`texture:${key}`);
|
|
21314
|
+
}
|
|
20701
21315
|
/**
|
|
20702
21316
|
* Replaces every registered emitter the saved module exports and drops its bake, so the next
|
|
20703
21317
|
* instance bakes the new config; a live instance keeps its own bake. An id no feature registered
|
|
@@ -20736,9 +21350,19 @@ function createHandlers$2(ctx) {
|
|
|
20736
21350
|
*/
|
|
20737
21351
|
"assets:bundle-unloaded": (payload) => {
|
|
20738
21352
|
effects ??= withDeps$3(ctx);
|
|
20739
|
-
|
|
20740
|
-
|
|
20741
|
-
|
|
21353
|
+
retireKeys(effects, payload.keys);
|
|
21354
|
+
},
|
|
21355
|
+
/**
|
|
21356
|
+
* Retires the particle instances and the `Displacement` filters drawn with the textures a dev
|
|
21357
|
+
* hot swap replaced and drops their bakes. The old textures are destroyed and the new ones
|
|
21358
|
+
* stand under the same keys, so the next frame builds each effect again from the new one.
|
|
21359
|
+
* Only dev emits it.
|
|
21360
|
+
*
|
|
21361
|
+
* @param payload - The bundle and the keys with new bytes.
|
|
21362
|
+
*/
|
|
21363
|
+
"assets:replaced": (payload) => {
|
|
21364
|
+
effects ??= withDeps$3(ctx);
|
|
21365
|
+
retireKeys(effects, payload.keys);
|
|
20742
21366
|
},
|
|
20743
21367
|
/**
|
|
20744
21368
|
* Replaces the registered emitters a dev hot swap brought. Only dev emits it.
|
|
@@ -24998,6 +25622,8 @@ function parseTags(source, warn) {
|
|
|
24998
25622
|
const CACHE_SEPARATOR = String.fromCodePoint(0);
|
|
24999
25623
|
/** How many laid-out blocks are kept. The oldest goes when a new one does not fit. */
|
|
25000
25624
|
const CACHE_LIMIT = 1024;
|
|
25625
|
+
/** How every inline icon tag starts. A resolved text without it holds no icon. */
|
|
25626
|
+
const ICON_OPEN = "<icon=";
|
|
25001
25627
|
/** The name the system is registered under; it shows up in a world error message. */
|
|
25002
25628
|
const TEXT_SYSTEM_NAME = "text.resolve";
|
|
25003
25629
|
/**
|
|
@@ -25302,9 +25928,36 @@ function rememberBound(ctx, entity, text, unit) {
|
|
|
25302
25928
|
seen.locale = ctx.deps.i18n.locale();
|
|
25303
25929
|
}
|
|
25304
25930
|
/**
|
|
25931
|
+
* Writes the string a bound label already shows once more, and measures it again. A mark on a
|
|
25932
|
+
* label whose number stands still means the picture under it moved: its font arrived or was
|
|
25933
|
+
* replaced, or its style was swapped. The display adapter rebuilds only what `changed(Text)`
|
|
25934
|
+
* names, so the same string is written.
|
|
25935
|
+
*
|
|
25936
|
+
* @param ctx - Domain context of the text plugin.
|
|
25937
|
+
* @param entity - The entity.
|
|
25938
|
+
* @param text - Its component value.
|
|
25939
|
+
*/
|
|
25940
|
+
function showAgain(ctx, entity, text) {
|
|
25941
|
+
ctx.deps.world.ecs.set(entity, Text, { resolved: text.resolved });
|
|
25942
|
+
ctx.state.measured.set(entity, sizeOf(layoutFor(ctx, text.resolved, text.style)));
|
|
25943
|
+
}
|
|
25944
|
+
/**
|
|
25945
|
+
* Reports a bound label whose style is not a digits style, once per style. An unknown style name
|
|
25946
|
+
* is not reported here: `styleOf` reports it by its name.
|
|
25947
|
+
*
|
|
25948
|
+
* @param ctx - Domain context of the text plugin.
|
|
25949
|
+
* @param styleName - The style name of the bound label.
|
|
25950
|
+
*/
|
|
25951
|
+
function warnWithoutDigits(ctx, styleName) {
|
|
25952
|
+
const style = ctx.state.styles.get(styleName);
|
|
25953
|
+
if (style === void 0 || style.digits) return;
|
|
25954
|
+
warnOnce(ctx, `bind-style:${styleName}`, "text: bind on a style without digits", { style: styleName });
|
|
25955
|
+
}
|
|
25956
|
+
/**
|
|
25305
25957
|
* One frame of a bound label: the value read every frame, the string built and written only
|
|
25306
25958
|
* when its unit moved. A label that stands still costs a read and a few compares; no tag is
|
|
25307
|
-
* parsed, and the locale is read only for a `"duration"`.
|
|
25959
|
+
* parsed, and the locale is read only for a `"duration"`. A marked label that stands still is
|
|
25960
|
+
* written once with the string it shows.
|
|
25308
25961
|
*
|
|
25309
25962
|
* @param ctx - Domain context of the text plugin.
|
|
25310
25963
|
* @param entity - The entity.
|
|
@@ -25313,13 +25966,15 @@ function rememberBound(ctx, entity, text, unit) {
|
|
|
25313
25966
|
* @param frame - What this run of the system shares.
|
|
25314
25967
|
*/
|
|
25315
25968
|
function stepBound(ctx, entity, text, bind, frame) {
|
|
25316
|
-
|
|
25317
|
-
if (style !== void 0 && !style.digits) warnOnce(ctx, `bind-style:${text.style}`, "text: bind on a style without digits", { style: text.style });
|
|
25969
|
+
warnWithoutDigits(ctx, text.style);
|
|
25318
25970
|
const value = boundValue(ctx, entity, bind, frame);
|
|
25319
25971
|
const format = value === void 0 ? "int" : formatOf(ctx, entity, bind, value);
|
|
25320
25972
|
const unit = value === void 0 ? void 0 : unitOf(value, format);
|
|
25321
|
-
ctx.state.dirty.delete(entity);
|
|
25322
|
-
if (isShown(ctx, entity, text, unit, format))
|
|
25973
|
+
const marked = ctx.state.dirty.delete(entity);
|
|
25974
|
+
if (isShown(ctx, entity, text, unit, format)) {
|
|
25975
|
+
if (marked) showAgain(ctx, entity, text);
|
|
25976
|
+
return;
|
|
25977
|
+
}
|
|
25323
25978
|
showBound(ctx, entity, text, value === void 0 ? "" : formatBound(value, format, (ms) => ctx.deps.i18n.duration(ms)), value);
|
|
25324
25979
|
rememberBound(ctx, entity, text, unit);
|
|
25325
25980
|
}
|
|
@@ -25391,17 +26046,43 @@ function createTextSystem(ctx) {
|
|
|
25391
26046
|
});
|
|
25392
26047
|
}
|
|
25393
26048
|
/**
|
|
25394
|
-
* Marks labels for the next layout phase: every one of them, or only the ones
|
|
25395
|
-
*
|
|
26049
|
+
* Marks labels for the next layout phase: every one of them, or only the ones `keep` names. A
|
|
26050
|
+
* locale change names the labels whose content is a message; a dev hot swap names the labels
|
|
26051
|
+
* drawn with a replaced font or icon.
|
|
25396
26052
|
*
|
|
25397
26053
|
* @param ctx - Domain context of the text plugin.
|
|
25398
|
-
* @param
|
|
26054
|
+
* @param keep - Tells whether a label is marked. Left out, every label is.
|
|
26055
|
+
* @returns How many labels were marked.
|
|
25399
26056
|
*/
|
|
25400
|
-
function markDirty(ctx,
|
|
26057
|
+
function markDirty(ctx, keep = () => true) {
|
|
26058
|
+
let marked = 0;
|
|
25401
26059
|
for (const [entity, text] of ctx.deps.world.ecs.query(Text)) {
|
|
25402
|
-
if (
|
|
26060
|
+
if (!keep(text)) continue;
|
|
25403
26061
|
ctx.state.dirty.add(entity);
|
|
26062
|
+
marked += 1;
|
|
25404
26063
|
}
|
|
26064
|
+
return marked;
|
|
26065
|
+
}
|
|
26066
|
+
/**
|
|
26067
|
+
* Tells whether a label is drawn with one of the asset keys: a font its style names, or an inline
|
|
26068
|
+
* icon of its resolved text. The text is read through the tag grammar, the way it is drawn, so an
|
|
26069
|
+
* escaped `\<icon=…>` is text and not an icon. A text that holds no `<icon=` at all is not read:
|
|
26070
|
+
* most labels have no icon, and this runs for every label of a swap.
|
|
26071
|
+
*
|
|
26072
|
+
* @param ctx - Domain context of the text plugin.
|
|
26073
|
+
* @param text - The component value of the label.
|
|
26074
|
+
* @param keys - The asset keys to look for.
|
|
26075
|
+
* @returns True when a glyph font or an icon of the label is one of `keys`.
|
|
26076
|
+
*/
|
|
26077
|
+
function drawsWith(ctx, text, keys) {
|
|
26078
|
+
const style = styleOf$1(ctx, text.style);
|
|
26079
|
+
if ([
|
|
26080
|
+
style.font,
|
|
26081
|
+
style.bold,
|
|
26082
|
+
style.italic
|
|
26083
|
+
].some((font) => font !== void 0 && keys.includes(font))) return true;
|
|
26084
|
+
if (!text.resolved.includes(ICON_OPEN)) return false;
|
|
26085
|
+
return parseTags(text.resolved, warnFor(ctx)).some((run) => run.kind === "icon" && keys.includes(run.key));
|
|
25405
26086
|
}
|
|
25406
26087
|
/**
|
|
25407
26088
|
* Forgets an entity whose `Text` left. Works on the state alone, because it is also what the
|
|
@@ -25430,6 +26111,40 @@ const SYNTHETIC_ITALIC = -.2;
|
|
|
25430
26111
|
/** What a shadow copy is painted with, so its tint alone gives it the shadow colour. */
|
|
25431
26112
|
const WHITE = 16777215;
|
|
25432
26113
|
/**
|
|
26114
|
+
* Reads one font when `assets` can answer for it: the advance table first, then the install in the
|
|
26115
|
+
* renderer. A font that has both already is left alone.
|
|
26116
|
+
*
|
|
26117
|
+
* @param ctx - Domain context of the text plugin.
|
|
26118
|
+
* @param key - The asset key of the font.
|
|
26119
|
+
* @returns True when its advance table arrived, so what was measured without it is stale.
|
|
26120
|
+
* @throws {Error} When the `.fnt` is in neither BMFont format.
|
|
26121
|
+
*/
|
|
26122
|
+
function installFont(ctx, key) {
|
|
26123
|
+
const renderer = ctx.deps.renderer;
|
|
26124
|
+
const needsTable = !ctx.state.tables.has(key);
|
|
26125
|
+
const needsInstall = renderer.host.ready() && !ctx.state.installed.has(key);
|
|
26126
|
+
if (!needsTable && !needsInstall) return false;
|
|
26127
|
+
const font = ctx.deps.assets.font(key);
|
|
26128
|
+
if (font === void 0) return false;
|
|
26129
|
+
if (needsTable) ctx.state.tables.set(key, parseAdvances(font.fnt, key));
|
|
26130
|
+
if (needsInstall) {
|
|
26131
|
+
renderer.sync.fonts.install(key, font.fnt, font.texture);
|
|
26132
|
+
ctx.state.installed.add(key);
|
|
26133
|
+
}
|
|
26134
|
+
return needsTable;
|
|
26135
|
+
}
|
|
26136
|
+
/**
|
|
26137
|
+
* Forgets one font: its advance table and the mark that the renderer holds it, so the next
|
|
26138
|
+
* `installFont` reads both again.
|
|
26139
|
+
*
|
|
26140
|
+
* @param ctx - Domain context of the text plugin.
|
|
26141
|
+
* @param key - The asset key of the font.
|
|
26142
|
+
*/
|
|
26143
|
+
function dropFont(ctx, key) {
|
|
26144
|
+
ctx.state.tables.delete(key);
|
|
26145
|
+
ctx.state.installed.delete(key);
|
|
26146
|
+
}
|
|
26147
|
+
/**
|
|
25433
26148
|
* Reads the fonts of `fontKeys` that `assets` can answer for: the advance table first, then the
|
|
25434
26149
|
* install in the renderer. Runs in `onStart` and on every bundle that lands.
|
|
25435
26150
|
*
|
|
@@ -25437,24 +26152,8 @@ const WHITE = 16777215;
|
|
|
25437
26152
|
* @throws {Error} When a `.fnt` is in neither BMFont format.
|
|
25438
26153
|
*/
|
|
25439
26154
|
function installFonts(ctx) {
|
|
25440
|
-
const renderer = ctx.deps.renderer;
|
|
25441
|
-
const ready = renderer.host.ready();
|
|
25442
26155
|
let arrived = false;
|
|
25443
|
-
for (const key of ctx.state.fontKeys)
|
|
25444
|
-
const needsTable = !ctx.state.tables.has(key);
|
|
25445
|
-
const needsInstall = ready && !ctx.state.installed.has(key);
|
|
25446
|
-
if (!needsTable && !needsInstall) continue;
|
|
25447
|
-
const font = ctx.deps.assets.font(key);
|
|
25448
|
-
if (font === void 0) continue;
|
|
25449
|
-
if (needsTable) {
|
|
25450
|
-
ctx.state.tables.set(key, parseAdvances(font.fnt, key));
|
|
25451
|
-
arrived = true;
|
|
25452
|
-
}
|
|
25453
|
-
if (needsInstall) {
|
|
25454
|
-
renderer.sync.fonts.install(key, font.fnt, font.texture);
|
|
25455
|
-
ctx.state.installed.add(key);
|
|
25456
|
-
}
|
|
25457
|
-
}
|
|
26156
|
+
for (const key of ctx.state.fontKeys) if (installFont(ctx, key)) arrived = true;
|
|
25458
26157
|
if (arrived) refresh(ctx);
|
|
25459
26158
|
}
|
|
25460
26159
|
/**
|
|
@@ -25467,8 +26166,7 @@ function releaseFonts(ctx, keys) {
|
|
|
25467
26166
|
let left = false;
|
|
25468
26167
|
for (const key of keys) {
|
|
25469
26168
|
if (!ctx.state.fontKeys.has(key)) continue;
|
|
25470
|
-
ctx
|
|
25471
|
-
ctx.state.installed.delete(key);
|
|
26169
|
+
dropFont(ctx, key);
|
|
25472
26170
|
left = true;
|
|
25473
26171
|
}
|
|
25474
26172
|
if (left) refresh(ctx);
|
|
@@ -25483,6 +26181,73 @@ function refresh(ctx) {
|
|
|
25483
26181
|
markDirty(ctx);
|
|
25484
26182
|
}
|
|
25485
26183
|
/**
|
|
26184
|
+
* Reloads the page. This is how a font replace that failed ends, so the game is not left drawing
|
|
26185
|
+
* with a font whose table was released. Headless and in a test there is no page, so nothing
|
|
26186
|
+
* happens.
|
|
26187
|
+
*/
|
|
26188
|
+
function reloadPage() {
|
|
26189
|
+
globalThis.location?.reload();
|
|
26190
|
+
}
|
|
26191
|
+
/**
|
|
26192
|
+
* Says why a font could not be installed again.
|
|
26193
|
+
*
|
|
26194
|
+
* @param error - What the install threw.
|
|
26195
|
+
* @returns The message of an error, or the value as a string.
|
|
26196
|
+
* @example
|
|
26197
|
+
* ```ts
|
|
26198
|
+
* reasonOf(new Error("The page is gone.")); // "The page is gone."
|
|
26199
|
+
* ```
|
|
26200
|
+
*/
|
|
26201
|
+
function reasonOf(error) {
|
|
26202
|
+
return error instanceof Error ? error.message : String(error);
|
|
26203
|
+
}
|
|
26204
|
+
/**
|
|
26205
|
+
* Installs every replaced font again, one by one: its table and its mark go, then the new file
|
|
26206
|
+
* and the new page are read. No other font is touched and no label is marked here. The stamp of
|
|
26207
|
+
* the keys watch is written after a failed scan too, so a `.fnt` the scanner refused still gets
|
|
26208
|
+
* here, and reading it throws after its table was released. That font is logged and the page
|
|
26209
|
+
* reloads.
|
|
26210
|
+
*
|
|
26211
|
+
* @param ctx - Domain context of the text plugin.
|
|
26212
|
+
* @param fonts - The font keys among what a dev hot swap replaced.
|
|
26213
|
+
* @param reload - What a font that cannot be installed calls.
|
|
26214
|
+
* @returns False when a font failed, so the page is on its way out.
|
|
26215
|
+
*/
|
|
26216
|
+
function reinstallFonts(ctx, fonts, reload) {
|
|
26217
|
+
for (const key of fonts) try {
|
|
26218
|
+
dropFont(ctx, key);
|
|
26219
|
+
installFont(ctx, key);
|
|
26220
|
+
} catch (error) {
|
|
26221
|
+
ctx.log.error("text:font-replace-failed", {
|
|
26222
|
+
key,
|
|
26223
|
+
reason: reasonOf(error)
|
|
26224
|
+
});
|
|
26225
|
+
reload();
|
|
26226
|
+
return false;
|
|
26227
|
+
}
|
|
26228
|
+
return true;
|
|
26229
|
+
}
|
|
26230
|
+
/**
|
|
26231
|
+
* Applies what a dev hot swap replaced. `assets` answers the new font and the new texture of
|
|
26232
|
+
* every key already, and the old pages and textures are destroyed. A font among the keys is
|
|
26233
|
+
* installed again here, so the font registry of the renderer holds the new page at once. Only
|
|
26234
|
+
* the labels drawn with a replaced font or inline icon are marked, and the generation moves: the
|
|
26235
|
+
* next frame writes each of them, and the adapter builds it again from the new textures even when
|
|
26236
|
+
* its string stands still. A label of another font is not written and keeps its objects.
|
|
26237
|
+
*
|
|
26238
|
+
* @param ctx - Domain context of the text plugin.
|
|
26239
|
+
* @param keys - The asset keys with new bytes.
|
|
26240
|
+
* @param reload - What a font that cannot be installed calls. The page reload by default; a test
|
|
26241
|
+
* passes a spy.
|
|
26242
|
+
*/
|
|
26243
|
+
function applyReplaced(ctx, keys, reload = reloadPage) {
|
|
26244
|
+
const fonts = keys.filter((key) => ctx.state.fontKeys.has(key));
|
|
26245
|
+
if (fonts.length > 0) ctx.state.cache.clear();
|
|
26246
|
+
if (!reinstallFonts(ctx, fonts, reload)) return;
|
|
26247
|
+
if (markDirty(ctx, (text) => drawsWith(ctx, text, keys)) === 0) return;
|
|
26248
|
+
ctx.state.generation += 1;
|
|
26249
|
+
}
|
|
26250
|
+
/**
|
|
25486
26251
|
* Where a line starts inside the widest one.
|
|
25487
26252
|
*
|
|
25488
26253
|
* @param style - The style of the label.
|
|
@@ -25698,7 +26463,7 @@ function buildRun(ctx, pixi, run, style, at) {
|
|
|
25698
26463
|
return objects;
|
|
25699
26464
|
}
|
|
25700
26465
|
/**
|
|
25701
|
-
* The style and the laid-out block of one component value.
|
|
26466
|
+
* The style and the laid-out block of one component value, under the generation of this moment.
|
|
25702
26467
|
*
|
|
25703
26468
|
* @param ctx - Domain context of the text plugin.
|
|
25704
26469
|
* @param value - The component value.
|
|
@@ -25707,7 +26472,8 @@ function buildRun(ctx, pixi, run, style, at) {
|
|
|
25707
26472
|
function labelOf(ctx, value) {
|
|
25708
26473
|
return {
|
|
25709
26474
|
style: styleOf$1(ctx, value.style),
|
|
25710
|
-
layout: layoutFor(ctx, value.resolved, value.style)
|
|
26475
|
+
layout: layoutFor(ctx, value.resolved, value.style),
|
|
26476
|
+
generation: ctx.state.generation
|
|
25711
26477
|
};
|
|
25712
26478
|
}
|
|
25713
26479
|
/**
|
|
@@ -25821,10 +26587,27 @@ function samePoint(first, second) {
|
|
|
25821
26587
|
return first.x === second.x && first.y === second.y;
|
|
25822
26588
|
}
|
|
25823
26589
|
/**
|
|
26590
|
+
* Tells whether a container already shows a value: the string, the style name and the anchor it
|
|
26591
|
+
* was last updated with, drawn with the style object that is current and under the generation
|
|
26592
|
+
* that is current. A replaced style is a new object under the same name. A font or an icon a dev
|
|
26593
|
+
* hot swap replaced moves the generation, because the objects hold textures that are destroyed.
|
|
26594
|
+
*
|
|
26595
|
+
* @param ctx - Domain context of the text plugin.
|
|
26596
|
+
* @param container - The container of this label.
|
|
26597
|
+
* @param previous - The value the container was drawn from.
|
|
26598
|
+
* @param next - The value to draw now.
|
|
26599
|
+
* @returns True when nothing has to be drawn.
|
|
26600
|
+
*/
|
|
26601
|
+
function isCurrent(ctx, container, previous, next) {
|
|
26602
|
+
const drawn = ctx.state.drawn.get(container);
|
|
26603
|
+
return previous.resolved === next.resolved && previous.style === next.style && samePoint(previous.anchor, next.anchor) && drawn?.style === styleOf$1(ctx, next.style) && drawn.generation === ctx.state.generation;
|
|
26604
|
+
}
|
|
26605
|
+
/**
|
|
25824
26606
|
* Draws a changed value into a container that holds a label already. A ticking counter keeps its
|
|
25825
|
-
* objects: with the same style object, the same
|
|
25826
|
-
* and spots are written into them. Anything else frees the children and
|
|
25827
|
-
* again.
|
|
26607
|
+
* objects: with the same style object, the same generation, the same anchor and the same lines
|
|
26608
|
+
* and runs, the new text and spots are written into them. Anything else frees the children and
|
|
26609
|
+
* fills the container again. A label of an older generation is always filled again: writing the
|
|
26610
|
+
* string it shows already into a `BitmapText` rebuilds no glyph.
|
|
25828
26611
|
*
|
|
25829
26612
|
* @param ctx - Domain context of the text plugin.
|
|
25830
26613
|
* @param pixi - The Pixi module the renderer loaded.
|
|
@@ -25835,7 +26618,7 @@ function samePoint(first, second) {
|
|
|
25835
26618
|
function redraw(ctx, pixi, container, previous, next) {
|
|
25836
26619
|
const drawn = ctx.state.drawn.get(container);
|
|
25837
26620
|
const label = labelOf(ctx, next);
|
|
25838
|
-
if (drawn?.style === label.style && samePoint(previous.anchor, next.anchor) && sameShape(drawn.layout, label.layout)) {
|
|
26621
|
+
if (drawn?.style === label.style && drawn.generation === label.generation && samePoint(previous.anchor, next.anchor) && sameShape(drawn.layout, label.layout)) {
|
|
25839
26622
|
retext(ctx, pixi, container, next, label);
|
|
25840
26623
|
return;
|
|
25841
26624
|
}
|
|
@@ -25869,7 +26652,7 @@ function createTextAdapter(ctx) {
|
|
|
25869
26652
|
if (pixi === void 0 || object === void 0) return;
|
|
25870
26653
|
const container = asContainer(object);
|
|
25871
26654
|
container.alpha = next.alpha;
|
|
25872
|
-
if (
|
|
26655
|
+
if (isCurrent(ctx, container, previous, next)) return;
|
|
25873
26656
|
redraw(ctx, pixi, container, previous, next);
|
|
25874
26657
|
},
|
|
25875
26658
|
destroy: (object) => {
|
|
@@ -26080,19 +26863,22 @@ function createTextApi(ctx) {
|
|
|
26080
26863
|
//#endregion
|
|
26081
26864
|
//#region src/plugins/text/handlers.ts
|
|
26082
26865
|
/**
|
|
26083
|
-
* @file text plugin — the
|
|
26084
|
-
* bundle takes them away, a locale change marks the messages
|
|
26085
|
-
* is where the strings are built
|
|
26866
|
+
* @file text plugin — the four hooks. None of them draws in place: a bundle brings fonts, a
|
|
26867
|
+
* bundle takes them away, a locale change marks the messages, a dev hot swap marks the labels of
|
|
26868
|
+
* a replaced font or icon. The layout phase of the next frame is where the strings are built
|
|
26869
|
+
* again.
|
|
26086
26870
|
*/
|
|
26087
26871
|
/**
|
|
26088
|
-
* Creates the
|
|
26872
|
+
* Creates the four hook handlers. Each one builds the domain context when it first fires, not
|
|
26089
26873
|
* while this factory runs: the kernel registers hooks before it builds the plugin APIs, so
|
|
26090
26874
|
* nothing is resolvable yet.
|
|
26091
26875
|
*
|
|
26092
26876
|
* @param ctx - Kernel context of the text plugin.
|
|
26093
|
-
* @
|
|
26877
|
+
* @param reload - What a font that a dev hot swap replaced and that cannot be installed calls.
|
|
26878
|
+
* The kernel passes nothing, which means the page reload; a test passes a spy.
|
|
26879
|
+
* @returns The four hooks of the plugin.
|
|
26094
26880
|
*/
|
|
26095
|
-
function createHandlers(ctx) {
|
|
26881
|
+
function createHandlers(ctx, reload) {
|
|
26096
26882
|
let text;
|
|
26097
26883
|
/**
|
|
26098
26884
|
* The domain context, built on the first hook that fires.
|
|
@@ -26122,6 +26908,15 @@ function createHandlers(ctx) {
|
|
|
26122
26908
|
releaseFonts(domain(), payload.keys);
|
|
26123
26909
|
},
|
|
26124
26910
|
/**
|
|
26911
|
+
* Installs the fonts a dev hot swap replaced again and marks the labels drawn with a replaced
|
|
26912
|
+
* font or inline icon. The next frame builds each of them again from the new textures.
|
|
26913
|
+
*
|
|
26914
|
+
* @param payload - The bundle and the asset keys that have new bytes.
|
|
26915
|
+
*/
|
|
26916
|
+
"assets:replaced": (payload) => {
|
|
26917
|
+
applyReplaced(domain(), payload.keys, reload);
|
|
26918
|
+
},
|
|
26919
|
+
/**
|
|
26125
26920
|
* Marks every label whose content is a message. The next layout phase resolves them again,
|
|
26126
26921
|
* and `ui` solves once over the new sizes; no projection is re-run.
|
|
26127
26922
|
*
|
|
@@ -26129,7 +26924,7 @@ function createHandlers(ctx) {
|
|
|
26129
26924
|
*/
|
|
26130
26925
|
"i18n:locale-changed": (_payload) => {
|
|
26131
26926
|
const tctx = domain();
|
|
26132
|
-
markDirty(tctx,
|
|
26927
|
+
markDirty(tctx, (label) => typeof label.content !== "string");
|
|
26133
26928
|
tctx.state.cache.clear();
|
|
26134
26929
|
tctx.deps.time.wake();
|
|
26135
26930
|
}
|
|
@@ -26180,6 +26975,7 @@ function createTextState$1() {
|
|
|
26180
26975
|
cache: emptyMap$2(),
|
|
26181
26976
|
drawn: emptyWeakMap(),
|
|
26182
26977
|
dirty: emptySet$1(),
|
|
26978
|
+
generation: 0,
|
|
26183
26979
|
warned: emptySet$1(),
|
|
26184
26980
|
removers: []
|
|
26185
26981
|
};
|
|
@@ -31844,6 +32640,8 @@ function createLayoutApi(ctx) {
|
|
|
31844
32640
|
const HOT_GLOBAL = "__moku_hot";
|
|
31845
32641
|
/** A strings module `assets:keys` generates; the group is its locale. */
|
|
31846
32642
|
const STRINGS_FILE = /\/generated\/strings\.([\w-]+)\.ts$/;
|
|
32643
|
+
/** The stamp module the keys watch of `moku-game dev` writes: it names asset files, no view. */
|
|
32644
|
+
const ASSETS_STAMP = "/.moku/assets-stamp.ts";
|
|
31847
32645
|
/**
|
|
31848
32646
|
* The `kind` of every value a game registers by value at start: ECS component and tag types
|
|
31849
32647
|
* (filters too), nodes, slots, flows, asset bundles and plugin owners. Any other `kind` marks
|
|
@@ -32002,6 +32800,41 @@ function localeOf(file) {
|
|
|
32002
32800
|
return STRINGS_FILE.exec(file.replaceAll("\\", "/"))?.[1];
|
|
32003
32801
|
}
|
|
32004
32802
|
/**
|
|
32803
|
+
* Tells whether the saved module is the stamp of the keys watch, read from its path with `/`
|
|
32804
|
+
* separators.
|
|
32805
|
+
*
|
|
32806
|
+
* @param file - The path of the saved module.
|
|
32807
|
+
* @returns True for `.moku/assets-stamp.ts` of a game.
|
|
32808
|
+
* @example
|
|
32809
|
+
* ```ts
|
|
32810
|
+
* isAssetsStamp("/game/.moku/assets-stamp.ts"); // true
|
|
32811
|
+
* ```
|
|
32812
|
+
*/
|
|
32813
|
+
function isAssetsStamp(file) {
|
|
32814
|
+
return file.replaceAll("\\", "/").endsWith(ASSETS_STAMP);
|
|
32815
|
+
}
|
|
32816
|
+
/**
|
|
32817
|
+
* Starts the swaps of one module: its exports for the event, and nothing swapped yet.
|
|
32818
|
+
*
|
|
32819
|
+
* @param module - The exports of the saved module.
|
|
32820
|
+
* @returns The swaps, every list empty.
|
|
32821
|
+
* @example
|
|
32822
|
+
* ```ts
|
|
32823
|
+
* noSwaps({ GAP: 8 }).components; // []
|
|
32824
|
+
* ```
|
|
32825
|
+
*/
|
|
32826
|
+
function noSwaps(module) {
|
|
32827
|
+
return {
|
|
32828
|
+
module,
|
|
32829
|
+
components: [],
|
|
32830
|
+
projections: [],
|
|
32831
|
+
animations: [],
|
|
32832
|
+
emitters: [],
|
|
32833
|
+
strings: [],
|
|
32834
|
+
textStyles: []
|
|
32835
|
+
};
|
|
32836
|
+
}
|
|
32837
|
+
/**
|
|
32005
32838
|
* Files one export under the kind it is, in the order component, projection, animation, emitter,
|
|
32006
32839
|
* strings, text styles. The `default` export of a strings file counts as strings.
|
|
32007
32840
|
*
|
|
@@ -32041,15 +32874,7 @@ function sortExports(next, file) {
|
|
|
32041
32874
|
const exports = Object.entries(next);
|
|
32042
32875
|
if (exports.length === 0) return { reason: "no exports" };
|
|
32043
32876
|
const locale = localeOf(file);
|
|
32044
|
-
const swaps =
|
|
32045
|
-
module: Object.fromEntries(exports),
|
|
32046
|
-
components: [],
|
|
32047
|
-
projections: [],
|
|
32048
|
-
animations: [],
|
|
32049
|
-
emitters: [],
|
|
32050
|
-
strings: [],
|
|
32051
|
-
textStyles: []
|
|
32052
|
-
};
|
|
32877
|
+
const swaps = noSwaps(Object.fromEntries(exports));
|
|
32053
32878
|
for (const entry of exports) {
|
|
32054
32879
|
const reason = fileExport(swaps, entry, locale);
|
|
32055
32880
|
if (reason !== void 0) return { reason };
|
|
@@ -32137,10 +32962,28 @@ function summaryOf(file, swaps) {
|
|
|
32137
32962
|
};
|
|
32138
32963
|
}
|
|
32139
32964
|
/**
|
|
32965
|
+
* Hands the stamp of the keys watch on: the event with the exports of the module, and the log
|
|
32966
|
+
* line the editor listens to, every list empty. Nothing is sorted, replaced or repainted: the
|
|
32967
|
+
* stamp names asset files, and `assets` hooks the event to replace them. A listener cannot refuse
|
|
32968
|
+
* by throwing, the bus catches what a hook throws, so `assets` reloads the page itself.
|
|
32969
|
+
*
|
|
32970
|
+
* @param ctx - Domain context of the ui plugin.
|
|
32971
|
+
* @param file - The path of the stamp module.
|
|
32972
|
+
* @param next - Its new module namespace.
|
|
32973
|
+
*/
|
|
32974
|
+
function forwardStamp(ctx, file, next) {
|
|
32975
|
+
const swaps = noSwaps(Object.fromEntries(Object.entries(next)));
|
|
32976
|
+
ctx.emit("ui:hot-swap", {
|
|
32977
|
+
file,
|
|
32978
|
+
module: swaps.module
|
|
32979
|
+
});
|
|
32980
|
+
ctx.log.info("ui:hot-swap", summaryOf(file, swaps));
|
|
32981
|
+
}
|
|
32982
|
+
/**
|
|
32140
32983
|
* Builds the handler of one app: sort the exports, run the replaces that may throw, write the
|
|
32141
32984
|
* components and the text styles, and repaint every view. The repaint runs for every module that
|
|
32142
32985
|
* is not refused, so a module of styles only, whose bindings Bun already patched, shows on the
|
|
32143
|
-
* next frame too.
|
|
32986
|
+
* next frame too. The stamp of the keys watch is the one module that is only forwarded.
|
|
32144
32987
|
*
|
|
32145
32988
|
* @param ctx - Domain context of the ui plugin.
|
|
32146
32989
|
* @param jsx - The jsx module, which owns the component registry and the roots.
|
|
@@ -32148,6 +32991,10 @@ function summaryOf(file, swaps) {
|
|
|
32148
32991
|
*/
|
|
32149
32992
|
function createSwap(ctx, jsx) {
|
|
32150
32993
|
return (next, file) => {
|
|
32994
|
+
if (isAssetsStamp(file) && hasMembers(next)) {
|
|
32995
|
+
forwardStamp(ctx, file, next);
|
|
32996
|
+
return;
|
|
32997
|
+
}
|
|
32151
32998
|
const sorted = sortExports(next, file);
|
|
32152
32999
|
if ("reason" in sorted) refuse(ctx, file, sorted.reason);
|
|
32153
33000
|
replaceRefusable(ctx, file, sorted);
|