@moku-labs/game 0.13.1 → 0.13.3
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/{capture-fpd83Mi5.mjs → capture-D7XvoHdH.mjs} +78 -1
- package/dist/cli.d.mts +7 -5
- package/dist/cli.mjs +74 -36
- package/dist/{commands-DvFr8Uwp.d.mts → commands-DRhJ_LIH.d.mts} +5 -5
- package/dist/control.d.mts +4 -4
- package/dist/control.mjs +1 -1
- package/dist/{define-CCc0ozKe.d.mts → define-Ckp70_GR.d.mts} +2 -2
- package/dist/{headless-BsNdJs9W.mjs → headless-BA6ffzqD.mjs} +30 -9
- package/dist/{headless-G4nuZ-xz.d.mts → headless-CYgYzro0.d.mts} +1 -1
- package/dist/hot.d.mts +3 -3
- package/dist/hot.mjs +11 -9
- package/dist/{index-rnOYAriI.d.mts → index-CjuObHhF.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-DKo9rRWW.d.mts} +1 -1
- package/dist/project.mjs +1 -1
- package/dist/{run-wYty_Lwo.mjs → run-BtyrJjmW.mjs} +130 -40
- package/dist/{runtime-B276Gg0s.d.mts → runtime-iTxTcukZ.d.mts} +1 -1
- package/dist/{src-SmT-XxVf.mjs → src-DlyYl_9X.mjs} +1414 -277
- package/dist/testing.d.mts +4 -4
- package/dist/testing.mjs +2 -2
- package/dist/{types-ZQLYXo5X.d.mts → types-BSMmnpFm.d.mts} +138 -29
- package/dist/{types-Bcdo5-2w.d.mts → types-B_Bp2tWK.d.mts} +7 -3
- package/dist/{types-DAdwEJMp.d.mts → types-Bonifz5M.d.mts} +2 -2
- package/dist/{types-DLghaGEy.d.mts → types-DkO3s9YX.d.mts} +4 -4
- package/dist/{types-BrTpz9oL.d.mts → types-Dm5Oh5bH.d.mts} +19 -6
- package/dist/{types-D_2G6Q68.d.mts → types-dJMOXXL3.d.mts} +21 -13
- package/dist/visual.d.mts +6 -6
- package/dist/visual.mjs +1 -1
- package/llms.txt +10 -7
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
2
|
import { r as systemSource, t as memory } from "./memory-CWlqhS7l.mjs";
|
|
3
3
|
import { a as finishDrafts, i as enableDraftPatches, n as deepFreeze, o as openDrafts, r as dropDrafts } from "./drafts-BawpSaVp.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { d as graphHash, f as isRestCheckpoint, h as pushEntry, i as servePaused, l as findNode, m as compact, n as captureFrame, o as collectFlows, p as slotNames, r as serveCaptures, s as describeGraph, t as cancelCaptures, u as framePath } from "./capture-D7XvoHdH.mjs";
|
|
5
5
|
import { A as system, C as Exiting, D as component, E as Tree, T as Order, _ as Parent, a as visualOf$1, b as Transform, c as Text, g as NineSlice, h as Display, j as tag, k as resource, m as textFor, n as fitScaleOf, o as visualRectOf, p as readStyle, r as samePose, s as Countdown, t as CONTENT, u as builtInStyles, v as Shape, w as Layer, x as componentsFor, y as Sprite } from "./visual-dMW8iqAE.mjs";
|
|
6
6
|
import { n as isDev } from "./dev-Cc50Dk7H.mjs";
|
|
7
7
|
import { i as isComponentDefinition, n as flatten, r as defineComponent } from "./flatten-BkXuscAV.mjs";
|
|
@@ -1933,19 +1933,6 @@ function connectFlow(ctx) {
|
|
|
1933
1933
|
registerSchedule(flowCtx, fx);
|
|
1934
1934
|
}
|
|
1935
1935
|
//#endregion
|
|
1936
|
-
//#region src/plugins/flow/runner/loop-types.ts
|
|
1937
|
-
/** The stages of entering a node, in the order they run. */
|
|
1938
|
-
const stages = ["load", "scene"];
|
|
1939
|
-
/**
|
|
1940
|
-
* Does nothing. It stands in for an unsubscribe function until the real one arrives.
|
|
1941
|
-
*
|
|
1942
|
-
* @example
|
|
1943
|
-
* ```ts
|
|
1944
|
-
* const watch = { off: noop };
|
|
1945
|
-
* ```
|
|
1946
|
-
*/
|
|
1947
|
-
function noop() {}
|
|
1948
|
-
//#endregion
|
|
1949
1936
|
//#region src/plugins/flow/runner/validate.ts
|
|
1950
1937
|
/** Nodes of one flow above which `validate` warns. A flow holds 7 to 15 nodes. */
|
|
1951
1938
|
const MAX_NODES = 15;
|
|
@@ -2400,6 +2387,16 @@ function collectGraph(ctx, features) {
|
|
|
2400
2387
|
return graph.flows;
|
|
2401
2388
|
}
|
|
2402
2389
|
/**
|
|
2390
|
+
* Renders the whole graph as JSON. It reads the flows as data, so it works before `run()`.
|
|
2391
|
+
*
|
|
2392
|
+
* @param ctx - Domain context of the flow plugin.
|
|
2393
|
+
* @param features - Features API: owners and slot contributions.
|
|
2394
|
+
* @returns The graph.
|
|
2395
|
+
*/
|
|
2396
|
+
function graphOf(ctx, features) {
|
|
2397
|
+
return describeGraph(collectGraph(ctx, features), features);
|
|
2398
|
+
}
|
|
2399
|
+
/**
|
|
2403
2400
|
* Validates the collected graph. Warnings are logged, problems stop `run()` in one error.
|
|
2404
2401
|
*
|
|
2405
2402
|
* @param ctx - Domain context of the flow plugin.
|
|
@@ -2412,6 +2409,169 @@ function checkGraph(ctx, features) {
|
|
|
2412
2409
|
if (report.problems.length > 0) throw new Error(report.problems.join("\n"));
|
|
2413
2410
|
}
|
|
2414
2411
|
//#endregion
|
|
2412
|
+
//#region src/plugins/flow/runner/bookmark.ts
|
|
2413
|
+
/**
|
|
2414
|
+
* Reads the main flow of the config.
|
|
2415
|
+
*
|
|
2416
|
+
* @param ctx - Domain context of the flow plugin.
|
|
2417
|
+
* @param method - Name of the API method that needs it, for the message.
|
|
2418
|
+
* @returns The top-level flow.
|
|
2419
|
+
* @throws {Error} When no main flow was configured.
|
|
2420
|
+
*/
|
|
2421
|
+
function mainFlow(ctx, method) {
|
|
2422
|
+
const main = ctx.config.mainFlow;
|
|
2423
|
+
if (main === void 0) throw new Error(`[game] flow.${method}() needs a main flow.\n Pass it as pluginConfigs.flow.mainFlow.`);
|
|
2424
|
+
return main;
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2427
|
+
* Names a checkpoint of the graph, for the message of a refused bookmark. Only a rest node is
|
|
2428
|
+
* one: a transit node that carries the flag is no place a restore may be sent to.
|
|
2429
|
+
*
|
|
2430
|
+
* @param ctx - Domain context of the flow plugin.
|
|
2431
|
+
* @param modules - Injected sibling APIs.
|
|
2432
|
+
* @returns The path of the first rest checkpoint, or the start of the main flow.
|
|
2433
|
+
*/
|
|
2434
|
+
function someCheckpoint(ctx, modules) {
|
|
2435
|
+
const main = mainFlow(ctx, "restore");
|
|
2436
|
+
for (const flow of collectGraph(ctx, modules.features).values()) for (const [name, entry] of Object.entries(flow.nodes)) if (isRestCheckpoint(entry)) return flow.id === main.id ? name : `${flow.id}/${name}`;
|
|
2437
|
+
return main.start;
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Looks up the node at a path. A sub-flow and a slot are no nodes: nothing runs at them.
|
|
2441
|
+
*
|
|
2442
|
+
* @param ctx - Domain context of the flow plugin.
|
|
2443
|
+
* @param modules - Injected sibling APIs.
|
|
2444
|
+
* @param path - Path of a bookmark, or of its rest point.
|
|
2445
|
+
* @returns The node there, a rest node or a transit node, or `undefined` when the path names none.
|
|
2446
|
+
*/
|
|
2447
|
+
function nodeAt(ctx, modules, path) {
|
|
2448
|
+
const entry = findNode(mainFlow(ctx, "restore"), path, modules.features.contributions)?.entry;
|
|
2449
|
+
return entry?.kind === "node" ? entry : void 0;
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Finds the node a bookmark names.
|
|
2453
|
+
*
|
|
2454
|
+
* @param ctx - Domain context of the flow plugin.
|
|
2455
|
+
* @param modules - Injected sibling APIs.
|
|
2456
|
+
* @param bookmark - The bookmark to enter.
|
|
2457
|
+
* @returns The node at the bookmark's path, a rest node or a transit node.
|
|
2458
|
+
* @throws {Error} When the path names no node of the graph.
|
|
2459
|
+
*/
|
|
2460
|
+
function bookmarkNode(ctx, modules, bookmark) {
|
|
2461
|
+
const node = nodeAt(ctx, modules, bookmark.path);
|
|
2462
|
+
if (node === void 0) throw new Error(`[game] The bookmark "${bookmark.path}" is not a node of the graph.\n Restore a rest node, for example the checkpoint "${someCheckpoint(ctx, modules)}".`);
|
|
2463
|
+
return node;
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Tells whether the graph is the one a bookmark was made for. The answer costs a description and
|
|
2467
|
+
* a hash of the whole graph, so one acceptance works it out once: see `askedOnce`.
|
|
2468
|
+
*
|
|
2469
|
+
* @param ctx - Domain context of the flow plugin.
|
|
2470
|
+
* @param modules - Injected sibling APIs.
|
|
2471
|
+
* @param bookmark - The bookmark to compare with.
|
|
2472
|
+
* @returns True while the hash of `describe()` is the bookmark's.
|
|
2473
|
+
*/
|
|
2474
|
+
function sameGraph(ctx, modules, bookmark) {
|
|
2475
|
+
return graphHash(graphOf(ctx, modules.features)) === bookmark.graph;
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Makes a question that is worked out at most once: at the first ask, and never when nobody asks.
|
|
2479
|
+
*
|
|
2480
|
+
* @param ask - Works the answer out.
|
|
2481
|
+
* @returns The question. Every ask after the first gets the first answer.
|
|
2482
|
+
* @example
|
|
2483
|
+
* ```ts
|
|
2484
|
+
* const same = askedOnce(() => true);
|
|
2485
|
+
* same() && same(); // true, and the answer was worked out once
|
|
2486
|
+
* ```
|
|
2487
|
+
*/
|
|
2488
|
+
function askedOnce(ask) {
|
|
2489
|
+
const answer = { value: void 0 };
|
|
2490
|
+
return () => {
|
|
2491
|
+
answer.value ??= ask();
|
|
2492
|
+
return answer.value;
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Checks that a bookmark may be entered: a rest checkpoint always, any other node only while the
|
|
2497
|
+
* graph is the one the bookmark was made for. A transit node is never taken for a checkpoint,
|
|
2498
|
+
* whatever its flag says: it is a place inside a transition, not a place a save may stand on.
|
|
2499
|
+
*
|
|
2500
|
+
* @param ctx - Domain context of the flow plugin.
|
|
2501
|
+
* @param modules - Injected sibling APIs.
|
|
2502
|
+
* @param bookmark - The bookmark to enter.
|
|
2503
|
+
* @param unchanged - Asks whether the graph is the bookmark's own. Not asked for a checkpoint.
|
|
2504
|
+
* @returns The node the bookmark names.
|
|
2505
|
+
* @throws {Error} When the path is no node, or the graph changed since the bookmark.
|
|
2506
|
+
*/
|
|
2507
|
+
function checkBookmark(ctx, modules, bookmark, unchanged) {
|
|
2508
|
+
const node = bookmarkNode(ctx, modules, bookmark);
|
|
2509
|
+
if (isRestCheckpoint(node)) return node;
|
|
2510
|
+
if (unchanged()) return node;
|
|
2511
|
+
throw new Error(`[game] The bookmark "${bookmark.path}" was made for another graph.\n Restore the checkpoint "${someCheckpoint(ctx, modules)}" instead.`);
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* Builds the bookmark of the rest point before a transit node, for a bookmark that carries `rest`
|
|
2515
|
+
* and was made for another graph. That is one whose path names a transit node while the hash
|
|
2516
|
+
* differs, and one whose path names no node any more: a renamed or removed node is a changed
|
|
2517
|
+
* graph too. Only the position changes: the state stays the bookmark's.
|
|
2518
|
+
*
|
|
2519
|
+
* @param ctx - Domain context of the flow plugin.
|
|
2520
|
+
* @param modules - Injected sibling APIs.
|
|
2521
|
+
* @param bookmark - The bookmark to enter.
|
|
2522
|
+
* @param unchanged - Asks whether the graph is the bookmark's own. Asked for a transit node only.
|
|
2523
|
+
* @returns The bookmark to enter instead, or `undefined` when the bookmark stands for itself.
|
|
2524
|
+
*/
|
|
2525
|
+
function restFallback(ctx, modules, bookmark, unchanged) {
|
|
2526
|
+
const rest = bookmark.rest;
|
|
2527
|
+
if (rest === void 0) return void 0;
|
|
2528
|
+
const node = nodeAt(ctx, modules, bookmark.path);
|
|
2529
|
+
if (node !== void 0 && (node.rest || unchanged())) return void 0;
|
|
2530
|
+
return {
|
|
2531
|
+
...bookmark,
|
|
2532
|
+
path: rest.path,
|
|
2533
|
+
input: rest.input
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Decides what the loop enters for a bookmark: the bookmark itself, or the rest point before it
|
|
2538
|
+
* when the graph changed under a bookmark that has one. Either one passes the same check, so the
|
|
2539
|
+
* fallback is entered only when it is a checkpoint, and the log says that it was taken.
|
|
2540
|
+
*
|
|
2541
|
+
* @param ctx - Domain context of the flow plugin.
|
|
2542
|
+
* @param modules - Injected sibling APIs.
|
|
2543
|
+
* @param bookmark - The bookmark to enter.
|
|
2544
|
+
* @returns The bookmark the loop enters, and whether its node is a rest node.
|
|
2545
|
+
* @throws {Error} When neither the bookmark nor its rest point may be entered.
|
|
2546
|
+
*/
|
|
2547
|
+
function acceptBookmark(ctx, modules, bookmark) {
|
|
2548
|
+
const unchanged = askedOnce(() => sameGraph(ctx, modules, bookmark));
|
|
2549
|
+
const fallback = restFallback(ctx, modules, bookmark, unchanged);
|
|
2550
|
+
const entered = fallback ?? bookmark;
|
|
2551
|
+
const node = checkBookmark(ctx, modules, entered, unchanged);
|
|
2552
|
+
if (fallback !== void 0) ctx.log.info("flow:restore-fell-back", {
|
|
2553
|
+
from: bookmark.path,
|
|
2554
|
+
to: fallback.path
|
|
2555
|
+
});
|
|
2556
|
+
return {
|
|
2557
|
+
bookmark: entered,
|
|
2558
|
+
rest: node.rest
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
//#endregion
|
|
2562
|
+
//#region src/plugins/flow/runner/loop-types.ts
|
|
2563
|
+
/** The stages of entering a node, in the order they run. */
|
|
2564
|
+
const stages = ["load", "scene"];
|
|
2565
|
+
/**
|
|
2566
|
+
* Does nothing. It stands in for an unsubscribe function until the real one arrives.
|
|
2567
|
+
*
|
|
2568
|
+
* @example
|
|
2569
|
+
* ```ts
|
|
2570
|
+
* const watch = { off: noop };
|
|
2571
|
+
* ```
|
|
2572
|
+
*/
|
|
2573
|
+
function noop() {}
|
|
2574
|
+
//#endregion
|
|
2415
2575
|
//#region src/plugins/flow/runner/position.ts
|
|
2416
2576
|
/**
|
|
2417
2577
|
* Resolves what the deepest frame points at.
|
|
@@ -3075,8 +3235,27 @@ function guideAllow(payload) {
|
|
|
3075
3235
|
};
|
|
3076
3236
|
}
|
|
3077
3237
|
/**
|
|
3238
|
+
* Refuses an effect a body starts after its node was aborted. The rejection counts as handled
|
|
3239
|
+
* from the start: a body that does not await the effect leaves no unhandled rejection behind,
|
|
3240
|
+
* and a body that awaits it still gets the reason.
|
|
3241
|
+
*
|
|
3242
|
+
* @param signal - The aborted signal of the node.
|
|
3243
|
+
* @returns A promise rejected with the abort reason.
|
|
3244
|
+
* @example
|
|
3245
|
+
* ```ts
|
|
3246
|
+
* refuseEffect(AbortSignal.abort("restore")); // a promise rejected with "restore"
|
|
3247
|
+
* ```
|
|
3248
|
+
*/
|
|
3249
|
+
function refuseEffect(signal) {
|
|
3250
|
+
const refused = Promise.reject(abortReason(signal));
|
|
3251
|
+
refused.catch(noop);
|
|
3252
|
+
return refused;
|
|
3253
|
+
}
|
|
3254
|
+
/**
|
|
3078
3255
|
* Builds the `fx` of a node context: awaited effects by call, cosmetic hints by `emit`. A `guide`
|
|
3079
|
-
* narrows the gate while it runs; the runner lifts the narrow when the node is left.
|
|
3256
|
+
* narrows the gate while it runs; the runner lifts the narrow when the node is left. A body that
|
|
3257
|
+
* was aborted keeps running, since the loop only stops waiting for it, so its `fx` starts nothing
|
|
3258
|
+
* any more: the gate and the narrow belong to the node the loop entered after it.
|
|
3080
3259
|
*
|
|
3081
3260
|
* @param modules - Injected sibling APIs.
|
|
3082
3261
|
* @param step - The node run in progress.
|
|
@@ -3084,12 +3263,13 @@ function guideAllow(payload) {
|
|
|
3084
3263
|
*/
|
|
3085
3264
|
function nodeFx(modules, step) {
|
|
3086
3265
|
/**
|
|
3087
|
-
* Awaits one effect.
|
|
3266
|
+
* Awaits one effect. After the abort of the node it rejects with the abort reason instead.
|
|
3088
3267
|
*
|
|
3089
3268
|
* @param descriptor - The descriptor the node awaits.
|
|
3090
3269
|
* @returns The answer, the handler's value, or `undefined`.
|
|
3091
3270
|
*/
|
|
3092
3271
|
const run = (descriptor) => {
|
|
3272
|
+
if (step.signal.aborted) return refuseEffect(step.signal);
|
|
3093
3273
|
if (descriptor.kind === "guide") {
|
|
3094
3274
|
const allow = guideAllow(descriptor.payload);
|
|
3095
3275
|
if (allow !== void 0) modules.gate.narrow(allow);
|
|
@@ -3599,19 +3779,23 @@ function enterSlot(ctx, modules, location, slot, safe) {
|
|
|
3599
3779
|
return "continue";
|
|
3600
3780
|
}
|
|
3601
3781
|
/**
|
|
3602
|
-
* Enters the node of a bookmark: the state is replaced, the rest
|
|
3603
|
-
* becomes the bookmark's path.
|
|
3782
|
+
* Enters the node of a bookmark, a rest node or a transit node: the state is replaced, the rest
|
|
3783
|
+
* point is marked and the position becomes the bookmark's path. The mark is made for a transit
|
|
3784
|
+
* node too. A restore leaves the whole document waiting for the provider, and a rollback of the
|
|
3785
|
+
* node would drop it unsent; and the frames of the transit node are what a failure of it must
|
|
3786
|
+
* retry, since the restored state belongs to them. `flow:rest` and the rest seam fire once for
|
|
3787
|
+
* every node entered here; only a rest node is announced as a checkpoint.
|
|
3604
3788
|
*
|
|
3605
3789
|
* @param ctx - Domain context of the flow plugin.
|
|
3606
3790
|
* @param modules - Injected sibling APIs; the features API resolves a path through a slot.
|
|
3607
3791
|
* @param bookmark - The bookmark to enter.
|
|
3608
3792
|
* @throws {Error} When the graph has no node at the bookmark's path.
|
|
3609
3793
|
*/
|
|
3610
|
-
function enterBookmark
|
|
3794
|
+
function enterBookmark(ctx, modules, bookmark) {
|
|
3611
3795
|
const state = ctx.state.runner;
|
|
3612
3796
|
const location = findNode(requireMainFlow(ctx), bookmark.path, modules.features.contributions);
|
|
3613
3797
|
loopSeam(state).restoring = void 0;
|
|
3614
|
-
if (location === void 0) throw new Error(`[game] The bookmark names no node "${bookmark.path}".\n Bookmark a
|
|
3798
|
+
if (location === void 0) throw new Error(`[game] The bookmark names no node "${bookmark.path}".\n Bookmark a node of the running graph.`);
|
|
3615
3799
|
ctx.deps.model.store.restore({
|
|
3616
3800
|
player: bookmark.player,
|
|
3617
3801
|
session: bookmark.session,
|
|
@@ -3622,10 +3806,9 @@ function enterBookmark$1(ctx, modules, bookmark) {
|
|
|
3622
3806
|
state.slotAfter = void 0;
|
|
3623
3807
|
state.restFrame = [...state.stack];
|
|
3624
3808
|
state.failures = 0;
|
|
3625
|
-
const entry = location.entry;
|
|
3626
3809
|
ctx.emit("flow:rest", {
|
|
3627
3810
|
path: bookmark.path,
|
|
3628
|
-
checkpoint:
|
|
3811
|
+
checkpoint: isRestCheckpoint(location.entry)
|
|
3629
3812
|
});
|
|
3630
3813
|
notifyRest(state, bookmark.path);
|
|
3631
3814
|
}
|
|
@@ -3643,7 +3826,7 @@ async function advance(ctx, modules, safe) {
|
|
|
3643
3826
|
if (stopped(state)) return "stop";
|
|
3644
3827
|
const restoring = state.seam?.restoring;
|
|
3645
3828
|
if (restoring !== void 0) {
|
|
3646
|
-
enterBookmark
|
|
3829
|
+
enterBookmark(ctx, modules, restoring);
|
|
3647
3830
|
return "continue";
|
|
3648
3831
|
}
|
|
3649
3832
|
const location = locateFrames(ctx, state.stack);
|
|
@@ -3685,12 +3868,15 @@ async function runLoop(ctx, modules) {
|
|
|
3685
3868
|
}
|
|
3686
3869
|
/**
|
|
3687
3870
|
* Restores a bookmark through the running loop: the active node is aborted with reason
|
|
3688
|
-
* `"restore"`, its transaction is discarded, the state is replaced and the node is entered.
|
|
3871
|
+
* `"restore"`, its transaction is discarded, the state is replaced and the node is entered. An
|
|
3872
|
+
* edge that was already committed still arrives first, and the rest point it reaches is not the
|
|
3873
|
+
* bookmark's: the promise waits for the turn in which the loop takes the bookmark.
|
|
3689
3874
|
*
|
|
3690
3875
|
* @param ctx - Domain context of the flow plugin.
|
|
3691
3876
|
* @param _modules - Injected sibling APIs.
|
|
3692
3877
|
* @param bookmark - The bookmark to enter.
|
|
3693
|
-
* @returns A promise that resolves once the loop
|
|
3878
|
+
* @returns A promise that resolves once the loop entered the bookmark's node, before that node
|
|
3879
|
+
* ran: the rest seam fired.
|
|
3694
3880
|
* @throws {Error} When the loop is not running.
|
|
3695
3881
|
*/
|
|
3696
3882
|
function restorePosition(ctx, _modules, bookmark) {
|
|
@@ -3702,6 +3888,7 @@ function restorePosition(ctx, _modules, bookmark) {
|
|
|
3702
3888
|
* Resolves once, when the loop rests at the restored node.
|
|
3703
3889
|
*/
|
|
3704
3890
|
const listener = () => {
|
|
3891
|
+
if (seam.restoring !== void 0) return;
|
|
3705
3892
|
const index = seam.rest.indexOf(listener);
|
|
3706
3893
|
if (index !== -1) seam.rest.splice(index, 1);
|
|
3707
3894
|
resolve();
|
|
@@ -3722,7 +3909,7 @@ function restorePosition(ctx, _modules, bookmark) {
|
|
|
3722
3909
|
* @param view - The last view.
|
|
3723
3910
|
* @returns True while nothing the state shows moved.
|
|
3724
3911
|
*/
|
|
3725
|
-
function isCurrent(ctx, view) {
|
|
3912
|
+
function isCurrent$1(ctx, view) {
|
|
3726
3913
|
const runner = ctx.state.runner;
|
|
3727
3914
|
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
3915
|
}
|
|
@@ -3763,7 +3950,7 @@ function viewOf(ctx) {
|
|
|
3763
3950
|
function readState(ctx) {
|
|
3764
3951
|
const runner = ctx.state.runner;
|
|
3765
3952
|
const view = runner.view;
|
|
3766
|
-
if (view !== void 0 && isCurrent(ctx, view)) return view.state;
|
|
3953
|
+
if (view !== void 0 && isCurrent$1(ctx, view)) return view.state;
|
|
3767
3954
|
const next = viewOf(ctx);
|
|
3768
3955
|
runner.view = next;
|
|
3769
3956
|
return next.state;
|
|
@@ -3871,24 +4058,37 @@ function watchEnd(running) {
|
|
|
3871
4058
|
return end;
|
|
3872
4059
|
}
|
|
3873
4060
|
/**
|
|
3874
|
-
*
|
|
3875
|
-
* that ended
|
|
4061
|
+
* Watches for the next gate the loop opens. The listener leaves the seam when a gate wakes it. A
|
|
4062
|
+
* wait that something else ended calls `off()`: the gate that would take the listener off may
|
|
4063
|
+
* never come.
|
|
3876
4064
|
*
|
|
3877
4065
|
* @param seam - The seam of the running loop.
|
|
3878
|
-
* @returns
|
|
4066
|
+
* @returns The promise of the next open gate, and the way to stop watching.
|
|
3879
4067
|
*/
|
|
3880
4068
|
function nextGateOpen(seam) {
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
4069
|
+
const watch = { wake: void 0 };
|
|
4070
|
+
/**
|
|
4071
|
+
* Takes the listener off the seam. It may be gone already.
|
|
4072
|
+
*/
|
|
4073
|
+
const off = () => {
|
|
4074
|
+
const index = seam.gateOpen.indexOf(listener);
|
|
4075
|
+
if (index !== -1) seam.gateOpen.splice(index, 1);
|
|
4076
|
+
};
|
|
4077
|
+
/**
|
|
4078
|
+
* Wakes the waiting side once the loop opened a gate, and leaves.
|
|
4079
|
+
*/
|
|
4080
|
+
function listener() {
|
|
4081
|
+
off();
|
|
4082
|
+
watch.wake?.();
|
|
4083
|
+
}
|
|
4084
|
+
const promise = new Promise((resolve) => {
|
|
4085
|
+
watch.wake = resolve;
|
|
3891
4086
|
});
|
|
4087
|
+
seam.gateOpen.push(listener);
|
|
4088
|
+
return {
|
|
4089
|
+
promise,
|
|
4090
|
+
off
|
|
4091
|
+
};
|
|
3892
4092
|
}
|
|
3893
4093
|
/**
|
|
3894
4094
|
* Waits until the loop opens its gate. Between the edge it commits and the gate of the rest node
|
|
@@ -3902,7 +4102,40 @@ function nextGateOpen(seam) {
|
|
|
3902
4102
|
async function reachGate(ctx, seam, ended) {
|
|
3903
4103
|
if (ctx.state.gate.open !== void 0) return;
|
|
3904
4104
|
if (ended.done) return;
|
|
3905
|
-
|
|
4105
|
+
const opened = nextGateOpen(seam);
|
|
4106
|
+
try {
|
|
4107
|
+
await Promise.race([opened.promise, ended.settled]);
|
|
4108
|
+
} finally {
|
|
4109
|
+
opened.off();
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
/**
|
|
4113
|
+
* Waits until a transit node the loop was sent into shows what it waits for: the gate it opens,
|
|
4114
|
+
* or, when it opens none, the next rest point or the end of the loop. `restore` waits here after
|
|
4115
|
+
* it entered a transit bookmark, so its caller reads a state with the open gate in it. There is no
|
|
4116
|
+
* timeout: a node that opens no gate, never rests and never ends keeps the wait pending, as its
|
|
4117
|
+
* body would. Call it only after the loop entered the node: a gate opened before that belongs to
|
|
4118
|
+
* the node the restore aborted.
|
|
4119
|
+
*
|
|
4120
|
+
* @param ctx - Domain context of the flow plugin.
|
|
4121
|
+
* @returns A promise that resolves at the open gate, at a rest point or at the end of the loop.
|
|
4122
|
+
*/
|
|
4123
|
+
async function reachGateOrRest(ctx) {
|
|
4124
|
+
const running = ctx.state.runner.running;
|
|
4125
|
+
if (running === void 0 || ctx.state.gate.open !== void 0) return;
|
|
4126
|
+
const seam = loopSeam(ctx.state.runner);
|
|
4127
|
+
const opened = nextGateOpen(seam);
|
|
4128
|
+
const rested = watchRest(seam);
|
|
4129
|
+
try {
|
|
4130
|
+
await Promise.race([
|
|
4131
|
+
opened.promise,
|
|
4132
|
+
rested.next(),
|
|
4133
|
+
watchEnd(running).settled
|
|
4134
|
+
]);
|
|
4135
|
+
} finally {
|
|
4136
|
+
opened.off();
|
|
4137
|
+
rested.off();
|
|
4138
|
+
}
|
|
3906
4139
|
}
|
|
3907
4140
|
/**
|
|
3908
4141
|
* The error of a step the loop never reached, naming where it stands instead.
|
|
@@ -4020,14 +4253,17 @@ async function playRoute(ctx, modules, route, running) {
|
|
|
4020
4253
|
/**
|
|
4021
4254
|
* Walks a route through the running loop: restores `from` when given, switches to fast mode,
|
|
4022
4255
|
* waits until the loop rests at each step's `at` and answers through the gate, and substitutes
|
|
4023
|
-
* the result of every sub-flow node the route skips.
|
|
4024
|
-
*
|
|
4025
|
-
* it
|
|
4256
|
+
* the result of every sub-flow node the route skips. A rest node `from` names is entered before
|
|
4257
|
+
* the switch: the stages of that node see the mode of the caller. A transit node is entered
|
|
4258
|
+
* after the switch, so it never starts live and never shows its popup. The mode of the caller
|
|
4259
|
+
* is put back afterwards, also when the bookmark is refused or a step rejects, and a
|
|
4260
|
+
* substitution the route never reached is disarmed: it must not skip a sub-flow in the live play
|
|
4261
|
+
* that follows.
|
|
4026
4262
|
*
|
|
4027
4263
|
* @param ctx - Domain context of the flow plugin.
|
|
4028
4264
|
* @param modules - Injected sibling APIs: features, fx, gate, inbox.
|
|
4029
4265
|
* @param route - The player's answers and substituted results, in order.
|
|
4030
|
-
* @param start - Bookmark to enter first
|
|
4266
|
+
* @param start - Bookmark to enter first, the restore that enters it and the kind of its node.
|
|
4031
4267
|
* @returns The state the walk ended in.
|
|
4032
4268
|
* @throws {Error} Before `run()` was called, when the bookmark is refused, and when a step's `at`
|
|
4033
4269
|
* is never reached.
|
|
@@ -4035,10 +4271,13 @@ async function playRoute(ctx, modules, route, running) {
|
|
|
4035
4271
|
async function walkRoute(ctx, modules, route, start) {
|
|
4036
4272
|
const running = ctx.state.runner.running;
|
|
4037
4273
|
if (running === void 0) throw new Error("[game] flow.walk() needs a running graph.\n Call flow.run() from the createApp onStart callback first.");
|
|
4038
|
-
|
|
4274
|
+
const entersBeforeSwitch = start !== void 0 && !start.transit;
|
|
4275
|
+
const entersAfterSwitch = start !== void 0 && !entersBeforeSwitch;
|
|
4276
|
+
if (entersBeforeSwitch) await start.restore(start.from);
|
|
4039
4277
|
const previous = ctx.state.fx.mode;
|
|
4040
4278
|
modules.fx.setMode("fast");
|
|
4041
4279
|
try {
|
|
4280
|
+
if (entersAfterSwitch) await start.restore(start.from);
|
|
4042
4281
|
await playRoute(ctx, modules, route, running);
|
|
4043
4282
|
} finally {
|
|
4044
4283
|
loopSeam(ctx.state.runner).substitutions.clear();
|
|
@@ -4050,69 +4289,71 @@ async function walkRoute(ctx, modules, route, start) {
|
|
|
4050
4289
|
//#region src/plugins/flow/runner/api.ts
|
|
4051
4290
|
const noPayload = null;
|
|
4052
4291
|
/**
|
|
4053
|
-
*
|
|
4292
|
+
* Tells whether a frame points at a rest node.
|
|
4054
4293
|
*
|
|
4055
4294
|
* @param ctx - Domain context of the flow plugin.
|
|
4056
|
-
* @param
|
|
4057
|
-
* @returns
|
|
4058
|
-
* @throws {Error} When no main flow was configured.
|
|
4295
|
+
* @param frame - One level of a position.
|
|
4296
|
+
* @returns True when the entry of that frame is a node with `rest`.
|
|
4059
4297
|
*/
|
|
4060
|
-
function
|
|
4061
|
-
const
|
|
4062
|
-
|
|
4063
|
-
return main;
|
|
4298
|
+
function restsAt(ctx, frame) {
|
|
4299
|
+
const entry = ctx.state.runner.flows.get(frame.flow)?.nodes[frame.node];
|
|
4300
|
+
return entry !== void 0 && entry.kind === "node" && entry.rest;
|
|
4064
4301
|
}
|
|
4065
4302
|
/**
|
|
4066
|
-
*
|
|
4303
|
+
* Tells whether the graph stands on a rest node. `setMode` is legal there and before `run()`.
|
|
4067
4304
|
*
|
|
4068
4305
|
* @param ctx - Domain context of the flow plugin.
|
|
4069
|
-
* @
|
|
4070
|
-
* @returns The graph.
|
|
4306
|
+
* @returns True while the position is empty or points at a rest node.
|
|
4071
4307
|
*/
|
|
4072
|
-
function
|
|
4073
|
-
|
|
4308
|
+
function resting(ctx) {
|
|
4309
|
+
const frame = ctx.state.runner.stack.at(-1);
|
|
4310
|
+
return frame === void 0 || restsAt(ctx, frame);
|
|
4074
4311
|
}
|
|
4075
4312
|
/**
|
|
4076
|
-
*
|
|
4313
|
+
* Tells whether a transit node waits at the gate: an effect that takes answers is open inside
|
|
4314
|
+
* it. A rest node with its own gate open rests, so it does not count.
|
|
4077
4315
|
*
|
|
4078
4316
|
* @param ctx - Domain context of the flow plugin.
|
|
4079
|
-
* @
|
|
4080
|
-
* @returns The path of the first checkpoint, or the start of the main flow.
|
|
4317
|
+
* @returns True while the gate is open and the position is no rest node.
|
|
4081
4318
|
*/
|
|
4082
|
-
function
|
|
4083
|
-
|
|
4084
|
-
for (const flow of collectGraph(ctx, modules.features).values()) for (const [name, entry] of Object.entries(flow.nodes)) if (entry.kind === "node" && entry.checkpoint) return flow.id === main.id ? name : `${flow.id}/${name}`;
|
|
4085
|
-
return main.start;
|
|
4319
|
+
function waitsInTransit(ctx) {
|
|
4320
|
+
return ctx.state.gate.open !== void 0 && !resting(ctx);
|
|
4086
4321
|
}
|
|
4087
4322
|
/**
|
|
4088
|
-
*
|
|
4089
|
-
*
|
|
4323
|
+
* Reads the rest point before the transit node the graph stands in. The rest frame names a
|
|
4324
|
+
* transit node when the graph started at one or a restore entered one: no rest point came
|
|
4325
|
+
* before it then.
|
|
4090
4326
|
*
|
|
4091
4327
|
* @param ctx - Domain context of the flow plugin.
|
|
4092
|
-
* @
|
|
4093
|
-
* @param bookmark - The bookmark to enter.
|
|
4094
|
-
* @throws {Error} When the path is no rest node, or the graph changed since the bookmark.
|
|
4328
|
+
* @returns The last rest node and its input, or `undefined` when the rest frame names none.
|
|
4095
4329
|
*/
|
|
4096
|
-
function
|
|
4097
|
-
const
|
|
4098
|
-
|
|
4099
|
-
if (
|
|
4100
|
-
|
|
4101
|
-
|
|
4330
|
+
function restBefore(ctx) {
|
|
4331
|
+
const frames = ctx.state.runner.restFrame ?? [];
|
|
4332
|
+
const frame = frames.at(-1);
|
|
4333
|
+
if (frame === void 0 || !restsAt(ctx, frame)) return void 0;
|
|
4334
|
+
return {
|
|
4335
|
+
path: framePath(frames),
|
|
4336
|
+
input: frame.input
|
|
4337
|
+
};
|
|
4102
4338
|
}
|
|
4103
4339
|
/**
|
|
4104
|
-
* Makes a bookmark of the
|
|
4340
|
+
* Makes a bookmark of where the graph stands. While a transit node waits at the gate for the
|
|
4341
|
+
* answer of an effect it is that node, with the rest point before it; in every other moment it is
|
|
4342
|
+
* the last rest point. The state is the committed one in both cases: for a waiting transit node
|
|
4343
|
+
* that is the state it was entered with, since its own writes are still in the open transaction.
|
|
4105
4344
|
*
|
|
4106
4345
|
* @param ctx - Domain context of the flow plugin.
|
|
4107
4346
|
* @param modules - Injected sibling APIs.
|
|
4108
|
-
* @returns A
|
|
4347
|
+
* @returns A node plus a state that really existed there.
|
|
4109
4348
|
* @throws {Error} When the graph has no position yet.
|
|
4110
4349
|
*/
|
|
4111
4350
|
function makeBookmark(ctx, modules) {
|
|
4112
4351
|
const state = ctx.state.runner;
|
|
4113
|
-
const
|
|
4352
|
+
const waiting = waitsInTransit(ctx);
|
|
4353
|
+
const frames = waiting ? state.stack : state.restFrame ?? state.stack;
|
|
4114
4354
|
if (frames.length === 0) throw new Error("[game] flow.bookmark() needs a position.\n Call it while the graph rests at a node.");
|
|
4115
4355
|
const snapshot = ctx.deps.model.store.snapshot();
|
|
4356
|
+
const rest = waiting ? restBefore(ctx) : void 0;
|
|
4116
4357
|
return {
|
|
4117
4358
|
path: framePath(frames),
|
|
4118
4359
|
input: frames.at(-1)?.input ?? noPayload,
|
|
@@ -4122,35 +4363,68 @@ function makeBookmark(ctx, modules) {
|
|
|
4122
4363
|
seed: snapshot.rng.seed,
|
|
4123
4364
|
streams: { ...snapshot.rng.streams }
|
|
4124
4365
|
},
|
|
4125
|
-
graph: graphHash(graphOf(ctx, modules))
|
|
4366
|
+
graph: graphHash(graphOf(ctx, modules.features)),
|
|
4367
|
+
...rest === void 0 ? {} : { rest }
|
|
4126
4368
|
};
|
|
4127
4369
|
}
|
|
4128
4370
|
/**
|
|
4129
|
-
*
|
|
4130
|
-
*
|
|
4131
|
-
*
|
|
4371
|
+
* Sends the running loop to a bookmark that passed the check, the short way a walk needs: the
|
|
4372
|
+
* promise resolves as soon as the loop stands at the node, before the node ran. It stays an
|
|
4373
|
+
* `async` function of its own: the microtask turn it adds lets the loop start the stages of a
|
|
4374
|
+
* rest node before the walk goes on and switches to fast mode.
|
|
4132
4375
|
*
|
|
4133
4376
|
* @param ctx - Domain context of the flow plugin.
|
|
4134
4377
|
* @param modules - Injected sibling APIs.
|
|
4135
|
-
* @param bookmark - The bookmark
|
|
4136
|
-
* @returns A promise that resolves once the loop
|
|
4137
|
-
* @throws {Error} When the bookmark names no rest node of this graph.
|
|
4378
|
+
* @param bookmark - The accepted bookmark.
|
|
4379
|
+
* @returns A promise that resolves once the loop entered the node.
|
|
4138
4380
|
*/
|
|
4139
|
-
async function
|
|
4140
|
-
checkBookmark(ctx, modules, bookmark);
|
|
4381
|
+
async function enterAccepted(ctx, modules, bookmark) {
|
|
4141
4382
|
await restorePosition(ctx, modules, bookmark);
|
|
4142
4383
|
}
|
|
4143
4384
|
/**
|
|
4144
|
-
*
|
|
4385
|
+
* Walks a route from a bookmark. The bookmark passes the check `restore` runs, so both ways into
|
|
4386
|
+
* a position refuse the same bookmarks and take the same fallback. The walk is told which kind of
|
|
4387
|
+
* node it enters: a rest node is entered before the switch to fast mode, a transit node after it.
|
|
4145
4388
|
*
|
|
4146
4389
|
* @param ctx - Domain context of the flow plugin.
|
|
4147
|
-
* @
|
|
4390
|
+
* @param modules - Injected sibling APIs.
|
|
4391
|
+
* @param route - The player's answers and substituted results, in order.
|
|
4392
|
+
* @param from - The bookmark to enter first.
|
|
4393
|
+
* @returns The state the walk ended in.
|
|
4394
|
+
* @throws {Error} When the bookmark may not be entered, and when a step's `at` is never reached.
|
|
4148
4395
|
*/
|
|
4149
|
-
function
|
|
4150
|
-
const
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4396
|
+
async function walkFrom(ctx, modules, route, from) {
|
|
4397
|
+
const accepted = acceptBookmark(ctx, modules, from);
|
|
4398
|
+
return walkRoute(ctx, modules, route, {
|
|
4399
|
+
/**
|
|
4400
|
+
* Enters the accepted bookmark. The walk never waits for a gate here: it answers the gates
|
|
4401
|
+
* of its own route.
|
|
4402
|
+
*
|
|
4403
|
+
* @param bookmark - The bookmark to enter.
|
|
4404
|
+
* @returns A promise that resolves once the loop entered its node.
|
|
4405
|
+
*/
|
|
4406
|
+
restore: (bookmark) => enterAccepted(ctx, modules, bookmark),
|
|
4407
|
+
from: accepted.bookmark,
|
|
4408
|
+
transit: !accepted.rest
|
|
4409
|
+
});
|
|
4410
|
+
}
|
|
4411
|
+
/**
|
|
4412
|
+
* Restores a bookmark, the way the plugin's `restore` does. A rest node is entered as `walk`
|
|
4413
|
+
* enters it. A transit node shows nothing until it ran up to its gate, so for one the promise
|
|
4414
|
+
* waits on: for the gate the node opens, or the rest point or the end of the loop it reaches
|
|
4415
|
+
* without one. The wait starts only after the loop entered the node, so a gate the aborted old
|
|
4416
|
+
* node still opened is not taken for the new one.
|
|
4417
|
+
*
|
|
4418
|
+
* @param ctx - Domain context of the flow plugin.
|
|
4419
|
+
* @param modules - Injected sibling APIs.
|
|
4420
|
+
* @param bookmark - The bookmark to enter.
|
|
4421
|
+
* @returns A promise that resolves once a rest node is entered, or a transit node waits.
|
|
4422
|
+
* @throws {Error} When the bookmark may not be entered, and before `run()`.
|
|
4423
|
+
*/
|
|
4424
|
+
async function restoreBookmark(ctx, modules, bookmark) {
|
|
4425
|
+
const accepted = acceptBookmark(ctx, modules, bookmark);
|
|
4426
|
+
await restorePosition(ctx, modules, accepted.bookmark);
|
|
4427
|
+
if (!accepted.rest) await reachGateOrRest(ctx);
|
|
4154
4428
|
}
|
|
4155
4429
|
/**
|
|
4156
4430
|
* Creates the runner API: `run` owns the one loop, `walk` and `restore` enter a position through
|
|
@@ -4179,21 +4453,12 @@ function createRunnerApi(ctx, modules) {
|
|
|
4179
4453
|
},
|
|
4180
4454
|
walk: (route, options) => {
|
|
4181
4455
|
const from = options?.from;
|
|
4182
|
-
if (from === void 0) return walkRoute(ctx, modules, route);
|
|
4183
|
-
return
|
|
4184
|
-
/**
|
|
4185
|
-
* Enters the bookmark the walk starts from, checked the way `restore` checks it.
|
|
4186
|
-
*
|
|
4187
|
-
* @param bookmark - The bookmark to enter.
|
|
4188
|
-
* @returns A promise that resolves once the loop rests at its node.
|
|
4189
|
-
*/
|
|
4190
|
-
restore: (bookmark) => enterBookmark(ctx, modules, bookmark),
|
|
4191
|
-
from
|
|
4192
|
-
});
|
|
4456
|
+
if (from === void 0 || state.running === void 0) return walkRoute(ctx, modules, route);
|
|
4457
|
+
return walkFrom(ctx, modules, route, from);
|
|
4193
4458
|
},
|
|
4194
4459
|
bookmark: () => makeBookmark(ctx, modules),
|
|
4195
|
-
restore: (bookmark) =>
|
|
4196
|
-
describe: () => graphOf(ctx, modules),
|
|
4460
|
+
restore: (bookmark) => restoreBookmark(ctx, modules, bookmark),
|
|
4461
|
+
describe: () => graphOf(ctx, modules.features),
|
|
4197
4462
|
state: () => readState(ctx),
|
|
4198
4463
|
history: () => [...state.journal],
|
|
4199
4464
|
setMode: (mode) => {
|
|
@@ -4521,7 +4786,7 @@ const flowPlugin = /*#__PURE__*/ createPlugin$1("flow", {
|
|
|
4521
4786
|
},
|
|
4522
4787
|
events: (register) => register.map({
|
|
4523
4788
|
"flow:edge": "An edge was taken and its state committed",
|
|
4524
|
-
"flow:rest": "The graph reached a rest node",
|
|
4789
|
+
"flow:rest": "The graph reached a rest node, or entered the node of a bookmark",
|
|
4525
4790
|
"flow:error": "A node failed and the graph rolled back"
|
|
4526
4791
|
}),
|
|
4527
4792
|
createState: createFlowState,
|
|
@@ -8269,7 +8534,7 @@ function stopHost(state) {
|
|
|
8269
8534
|
//#endregion
|
|
8270
8535
|
//#region src/plugins/renderer/host/readback.ts
|
|
8271
8536
|
/** Bytes of one pixel: the engine loads PNG and WebP only, so every source is 8-bit RGBA. */
|
|
8272
|
-
const BYTES_PER_PIXEL = 4;
|
|
8537
|
+
const BYTES_PER_PIXEL$1 = 4;
|
|
8273
8538
|
/**
|
|
8274
8539
|
* Estimates the GPU bytes of one texture source: 4 bytes per pixel, every mip level, a level never
|
|
8275
8540
|
* smaller than one pixel.
|
|
@@ -8293,7 +8558,7 @@ function sourceBytes(source) {
|
|
|
8293
8558
|
width = Math.max(1, Math.floor(width / 2));
|
|
8294
8559
|
height = Math.max(1, Math.floor(height / 2));
|
|
8295
8560
|
}
|
|
8296
|
-
return pixels * BYTES_PER_PIXEL;
|
|
8561
|
+
return pixels * BYTES_PER_PIXEL$1;
|
|
8297
8562
|
}
|
|
8298
8563
|
/**
|
|
8299
8564
|
* Counts the texture sources the GPU holds and sums their estimated bytes. Pixi's
|
|
@@ -8561,7 +8826,7 @@ function resetWindow(state) {
|
|
|
8561
8826
|
//#endregion
|
|
8562
8827
|
//#region src/plugins/renderer/monitor/api.ts
|
|
8563
8828
|
/** Bytes of one MiB. */
|
|
8564
|
-
const BYTES_PER_MB = 1048576;
|
|
8829
|
+
const BYTES_PER_MB$1 = 1048576;
|
|
8565
8830
|
/**
|
|
8566
8831
|
* Rounds a number to a fixed count of decimals, so a counter reads well in a panel.
|
|
8567
8832
|
*
|
|
@@ -8599,7 +8864,7 @@ function createMonitorApi(ctx, deps) {
|
|
|
8599
8864
|
fps: state.lastStart !== void 0 && clock.now() - state.lastStart <= 1e3 ? roundTo(state.fps, 1) : 0,
|
|
8600
8865
|
frameMs: roundTo(state.frameMs, 2),
|
|
8601
8866
|
textures: usage.count,
|
|
8602
|
-
textureMb: roundTo(usage.bytes / BYTES_PER_MB, 2),
|
|
8867
|
+
textureMb: roundTo(usage.bytes / BYTES_PER_MB$1, 2),
|
|
8603
8868
|
views: counts.views,
|
|
8604
8869
|
pooled: counts.pooled,
|
|
8605
8870
|
renderPasses: deps.sync.renderPasses()
|
|
@@ -8937,7 +9202,7 @@ function parseFont(pixi, key, fnt) {
|
|
|
8937
9202
|
* @param texture - The page texture `assets` uploaded.
|
|
8938
9203
|
* @throws {Error} When the renderer does not draw.
|
|
8939
9204
|
*/
|
|
8940
|
-
function installFont(sctx, key, fnt, texture) {
|
|
9205
|
+
function installFont$1(sctx, key, fnt, texture) {
|
|
8941
9206
|
const pixi = sctx.deps.host.pixi();
|
|
8942
9207
|
if (pixi === void 0) throw new Error("[game] Fonts need a renderer.\n Call fonts.install after renderer.host.ready().");
|
|
8943
9208
|
const state = sctx.ctx.state.sync;
|
|
@@ -10207,7 +10472,9 @@ function clearFrames(state) {
|
|
|
10207
10472
|
}
|
|
10208
10473
|
/**
|
|
10209
10474
|
* 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
|
|
10475
|
+
* the page, which frees it. A source that goes lets go of its `change` listeners first, the ones
|
|
10476
|
+
* of its style too, so no bind group Pixi still keeps is told. A texture destroyed twice is a
|
|
10477
|
+
* no-op.
|
|
10211
10478
|
*
|
|
10212
10479
|
* @param state - The sync branch of the plugin state.
|
|
10213
10480
|
* @param texture - The texture to free.
|
|
@@ -10215,7 +10482,13 @@ function clearFrames(state) {
|
|
|
10215
10482
|
function destroyTexture(state, texture) {
|
|
10216
10483
|
releaseFrames$1(state, texture);
|
|
10217
10484
|
if (texture.destroyed) return;
|
|
10218
|
-
|
|
10485
|
+
if (state.slices.has(texture)) {
|
|
10486
|
+
texture.destroy(false);
|
|
10487
|
+
return;
|
|
10488
|
+
}
|
|
10489
|
+
texture.source.style.removeAllListeners("change");
|
|
10490
|
+
texture.source.removeAllListeners("change");
|
|
10491
|
+
texture.destroy(true);
|
|
10219
10492
|
}
|
|
10220
10493
|
//#endregion
|
|
10221
10494
|
//#region src/plugins/renderer/sync/views.ts
|
|
@@ -11839,7 +12112,7 @@ function createSyncApi(ctx, deps) {
|
|
|
11839
12112
|
removed: markRemoved
|
|
11840
12113
|
}) },
|
|
11841
12114
|
fonts: {
|
|
11842
|
-
install: (key, fnt, texture) => installFont(sctx, key, fnt, texture),
|
|
12115
|
+
install: (key, fnt, texture) => installFont$1(sctx, key, fnt, texture),
|
|
11843
12116
|
installed: (key) => isFontInstalled(state, key)
|
|
11844
12117
|
},
|
|
11845
12118
|
debug: {
|
|
@@ -14629,6 +14902,12 @@ const TIERS = [
|
|
|
14629
14902
|
"feature",
|
|
14630
14903
|
"lazy"
|
|
14631
14904
|
];
|
|
14905
|
+
/** The bytes of one megabyte. */
|
|
14906
|
+
const BYTES_PER_MB = 1048576;
|
|
14907
|
+
/** The bytes one pixel takes in GPU memory: RGBA, no mipmaps. */
|
|
14908
|
+
const BYTES_PER_PIXEL = 4;
|
|
14909
|
+
/** Megabytes are kept to three decimals: a value is rounded to a whole number of these parts. */
|
|
14910
|
+
const PARTS_PER_MB = 1e3;
|
|
14632
14911
|
/**
|
|
14633
14912
|
* Creates an empty map. It lives in its own non-exported function because lint rule L5 refuses a
|
|
14634
14913
|
* collection built inside an exported declaration.
|
|
@@ -14923,6 +15202,38 @@ function fileUrl(base, path) {
|
|
|
14923
15202
|
return `${base}${path.startsWith("/") ? path.slice(1) : path}`;
|
|
14924
15203
|
}
|
|
14925
15204
|
/**
|
|
15205
|
+
* Works out what an image costs in texture memory: `width × height × 4` bytes as megabytes,
|
|
15206
|
+
* rounded to 3 decimals. It is the number the scanner writes for a texture and for a font page;
|
|
15207
|
+
* the dev hot swap writes it again for a file that came back with another size.
|
|
15208
|
+
*
|
|
15209
|
+
* @param width - Width of the image in pixels.
|
|
15210
|
+
* @param height - Height of the image in pixels.
|
|
15211
|
+
* @returns The megabytes.
|
|
15212
|
+
* @example
|
|
15213
|
+
* ```ts
|
|
15214
|
+
* textureMb(256, 128); // 0.125
|
|
15215
|
+
* ```
|
|
15216
|
+
*/
|
|
15217
|
+
function textureMb(width, height) {
|
|
15218
|
+
return Math.round(width * height * BYTES_PER_PIXEL / BYTES_PER_MB * PARTS_PER_MB) / PARTS_PER_MB;
|
|
15219
|
+
}
|
|
15220
|
+
/**
|
|
15221
|
+
* Adds up megabytes, rounded to 3 decimals: the pages of a font give the font, the files and the
|
|
15222
|
+
* atlas pages of a bundle give the bundle.
|
|
15223
|
+
*
|
|
15224
|
+
* @param parts - The records to add up.
|
|
15225
|
+
* @returns The sum.
|
|
15226
|
+
* @example
|
|
15227
|
+
* ```ts
|
|
15228
|
+
* sumMb([{ mb: 0.125 }, { mb: 0.063 }, { mb: 0.021 }]); // 0.209
|
|
15229
|
+
* ```
|
|
15230
|
+
*/
|
|
15231
|
+
function sumMb(parts) {
|
|
15232
|
+
let total = 0;
|
|
15233
|
+
for (const part of parts) total += part.mb;
|
|
15234
|
+
return Math.round(total * PARTS_PER_MB) / PARTS_PER_MB;
|
|
15235
|
+
}
|
|
15236
|
+
/**
|
|
14926
15237
|
* Turns the nine-slice metadata of a file into the options `renderer.sync.textures.create` takes.
|
|
14927
15238
|
*
|
|
14928
15239
|
* @param file - One file of a bundle.
|
|
@@ -14947,7 +15258,8 @@ function nineOf(file) {
|
|
|
14947
15258
|
//#region src/plugins/assets/tiers.ts
|
|
14948
15259
|
/**
|
|
14949
15260
|
* @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.
|
|
15261
|
+
* every caller a waiter: the last one to leave aborts the fetches. The loaders of one image, one
|
|
15262
|
+
* font and one sound are exported: the dev hot swap of `swap.ts` reads a changed file through them.
|
|
14951
15263
|
*/
|
|
14952
15264
|
/** The extension of an AAC sound in an MP4 container; every other sound is an MP3. */
|
|
14953
15265
|
const M4A = /\.m4a$/i;
|
|
@@ -15102,10 +15414,13 @@ function touch(state, record) {
|
|
|
15102
15414
|
record.lastUsed = state.useCounter;
|
|
15103
15415
|
}
|
|
15104
15416
|
/**
|
|
15105
|
-
* Creates the four empty maps a bundle's assets live in
|
|
15106
|
-
* rule L5 refuses a collection built inside an exported declaration.
|
|
15417
|
+
* Creates the four empty maps a bundle's assets live in: the home of a running load.
|
|
15107
15418
|
*
|
|
15108
15419
|
* @returns Empty maps for textures, fonts, audio and atlas pages.
|
|
15420
|
+
* @example
|
|
15421
|
+
* ```ts
|
|
15422
|
+
* emptyAssets().textures.size; // 0
|
|
15423
|
+
* ```
|
|
15109
15424
|
*/
|
|
15110
15425
|
function emptyAssets() {
|
|
15111
15426
|
return {
|
|
@@ -15202,6 +15517,22 @@ async function loadFont(run, file) {
|
|
|
15202
15517
|
};
|
|
15203
15518
|
}
|
|
15204
15519
|
/**
|
|
15520
|
+
* Loads one sound: the bytes exactly as they were fetched, never decoded here, and the type of
|
|
15521
|
+
* their container.
|
|
15522
|
+
*
|
|
15523
|
+
* @param run - The running load.
|
|
15524
|
+
* @param file - The audio file of the manifest.
|
|
15525
|
+
* @returns The bytes and their MIME type.
|
|
15526
|
+
* @throws {Error} When the response is not ok.
|
|
15527
|
+
*/
|
|
15528
|
+
async function loadSound(run, file) {
|
|
15529
|
+
const soundPath = pathOf(run.bundle, file);
|
|
15530
|
+
return {
|
|
15531
|
+
bytes: await (await fetchFile(run, soundPath)).arrayBuffer(),
|
|
15532
|
+
mime: mimeOf(soundPath)
|
|
15533
|
+
};
|
|
15534
|
+
}
|
|
15535
|
+
/**
|
|
15205
15536
|
* Starts the one load of every atlas page of a bundle. A page lands in the `pages` map of the
|
|
15206
15537
|
* running load, so a failure later frees it with the rest. Nobody may wait for a page, so its
|
|
15207
15538
|
* failure is marked as handled here; the files on it still reject with it.
|
|
@@ -15269,12 +15600,7 @@ async function loadFile(run, file) {
|
|
|
15269
15600
|
return;
|
|
15270
15601
|
}
|
|
15271
15602
|
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
|
-
});
|
|
15603
|
+
assets.audio.set(file.key, await loadSound(run, file));
|
|
15278
15604
|
return;
|
|
15279
15605
|
}
|
|
15280
15606
|
const texture = file.atlas === void 0 ? await loadImage(run, pathOf(run.bundle, file), nineOf(file)) : await sliceFile(run, file, file.atlas);
|
|
@@ -16326,6 +16652,8 @@ function releaseAll(state) {
|
|
|
16326
16652
|
state.warned.clear();
|
|
16327
16653
|
state.current = void 0;
|
|
16328
16654
|
state.sceneBundle = void 0;
|
|
16655
|
+
state.stamps = void 0;
|
|
16656
|
+
state.swapping = void 0;
|
|
16329
16657
|
state.io = void 0;
|
|
16330
16658
|
}
|
|
16331
16659
|
//#endregion
|
|
@@ -16406,102 +16734,617 @@ function unload(ctx, bundle) {
|
|
|
16406
16734
|
});
|
|
16407
16735
|
return;
|
|
16408
16736
|
}
|
|
16409
|
-
if (isPermanent(entry.tier)) {
|
|
16410
|
-
ctx.log.warn(REFUSED, {
|
|
16411
|
-
bundle,
|
|
16412
|
-
reason: "tier"
|
|
16413
|
-
});
|
|
16737
|
+
if (isPermanent(entry.tier)) {
|
|
16738
|
+
ctx.log.warn(REFUSED, {
|
|
16739
|
+
bundle,
|
|
16740
|
+
reason: "tier"
|
|
16741
|
+
});
|
|
16742
|
+
return;
|
|
16743
|
+
}
|
|
16744
|
+
state.records.get(bundle)?.inflight?.controller.abort();
|
|
16745
|
+
unloadBundle(ctx, bundle, "request");
|
|
16746
|
+
}
|
|
16747
|
+
/**
|
|
16748
|
+
* Tells whether a bundle is there. Headless every bundle of the manifest counts as loaded.
|
|
16749
|
+
*
|
|
16750
|
+
* @param ctx - Domain context of the plugin.
|
|
16751
|
+
* @param bundle - Name of the bundle.
|
|
16752
|
+
* @returns True when the bundle is loaded.
|
|
16753
|
+
*/
|
|
16754
|
+
function isLoaded(ctx, bundle) {
|
|
16755
|
+
const state = ctx.state;
|
|
16756
|
+
if (state.io === void 0) return state.manifest.bundles[bundle] !== void 0;
|
|
16757
|
+
return state.records.get(bundle)?.status === "loaded";
|
|
16758
|
+
}
|
|
16759
|
+
/**
|
|
16760
|
+
* Reports what the loaded bundles cost.
|
|
16761
|
+
*
|
|
16762
|
+
* @param ctx - Domain context of the plugin.
|
|
16763
|
+
* @returns The used and allowed megabytes and one entry per loaded bundle, sorted by name.
|
|
16764
|
+
*/
|
|
16765
|
+
function usage(ctx) {
|
|
16766
|
+
const bundles = [];
|
|
16767
|
+
for (const [name, record] of ctx.state.records) {
|
|
16768
|
+
const entry = ctx.state.manifest.bundles[name];
|
|
16769
|
+
if (record.status !== "loaded" || entry === void 0) continue;
|
|
16770
|
+
bundles.push({
|
|
16771
|
+
name,
|
|
16772
|
+
tier: entry.tier,
|
|
16773
|
+
mb: entry.mb,
|
|
16774
|
+
lastUsed: record.lastUsed
|
|
16775
|
+
});
|
|
16776
|
+
}
|
|
16777
|
+
bundles.sort((left, right) => left.name.localeCompare(right.name));
|
|
16778
|
+
return {
|
|
16779
|
+
textureMb: usedMb(ctx.state),
|
|
16780
|
+
budgetMb: ctx.config.textureBudgetMb,
|
|
16781
|
+
bundles
|
|
16782
|
+
};
|
|
16783
|
+
}
|
|
16784
|
+
/**
|
|
16785
|
+
* Creates the assets API: `app.assets`.
|
|
16786
|
+
*
|
|
16787
|
+
* @param ctx - Kernel context of the assets plugin.
|
|
16788
|
+
* @returns The plugin API.
|
|
16789
|
+
*/
|
|
16790
|
+
function createAssetsApi(ctx) {
|
|
16791
|
+
const assets = withDeps$6(ctx);
|
|
16792
|
+
return {
|
|
16793
|
+
load: (bundle) => loadBundle(assets, bundle, void 0, "request"),
|
|
16794
|
+
unload: (bundle) => unload(assets, bundle),
|
|
16795
|
+
isLoaded: (bundle) => isLoaded(assets, bundle),
|
|
16796
|
+
texture: (key) => lookupTexture(assets, key),
|
|
16797
|
+
font: (key) => loadedRecord(assets, key)?.fonts.get(key),
|
|
16798
|
+
audio: (key) => loadedRecord(assets, key)?.audio.get(key),
|
|
16799
|
+
usage: () => usage(assets)
|
|
16800
|
+
};
|
|
16801
|
+
}
|
|
16802
|
+
//#endregion
|
|
16803
|
+
//#region src/plugins/assets/paths.ts
|
|
16804
|
+
/** The name of the folder of a feature or of a layer the scanner reads. */
|
|
16805
|
+
const ASSETS = "assets";
|
|
16806
|
+
/** That folder inside a path. */
|
|
16807
|
+
const ASSETS_FOLDER = `/${ASSETS}/`;
|
|
16808
|
+
/**
|
|
16809
|
+
* Reads the folder the scanner found a manifest path in. The scanner reads the `assets/` folder
|
|
16810
|
+
* of a feature or of a layer, so the path is cut after it; a path with no such folder, as in a
|
|
16811
|
+
* manifest written by hand, gives the folder of the file.
|
|
16812
|
+
*
|
|
16813
|
+
* @param path - A path of the manifest.
|
|
16814
|
+
* @returns The folder, ending in a slash, or `""` for a file at the root.
|
|
16815
|
+
* @example
|
|
16816
|
+
* ```ts
|
|
16817
|
+
* folderOf("features/ui/assets/fx/leaf.webp"); // "features/ui/assets/"
|
|
16818
|
+
* ```
|
|
16819
|
+
*/
|
|
16820
|
+
function folderOf(path) {
|
|
16821
|
+
const scanned = path.indexOf(ASSETS_FOLDER);
|
|
16822
|
+
if (scanned !== -1) return path.slice(0, scanned + ASSETS_FOLDER.length);
|
|
16823
|
+
return path.slice(0, path.lastIndexOf("/") + 1);
|
|
16824
|
+
}
|
|
16825
|
+
/**
|
|
16826
|
+
* Tells whether the scanner would read a path, by its shape alone. It mirrors `scanOwner` of
|
|
16827
|
+
* `scan/scan.ts`, which runtime code does not import: the scanner reads `<layer>/assets/` and
|
|
16828
|
+
* `<features>/<feature>/assets/` under the game root, so `assets` is the second or the third
|
|
16829
|
+
* segment and a file follows it. The page knows neither the layers nor the name of the features
|
|
16830
|
+
* folder, so any folder counts as one. This is what finds the first file of an `assets/` folder
|
|
16831
|
+
* the booted manifest names nowhere.
|
|
16832
|
+
*
|
|
16833
|
+
* @param path - A path of the game, root-relative with `/`.
|
|
16834
|
+
* @returns True for a path under the `assets/` of a layer or of a feature.
|
|
16835
|
+
* @example
|
|
16836
|
+
* ```ts
|
|
16837
|
+
* isScannerPath("features/shop/assets/coin.png"); // true
|
|
16838
|
+
* ```
|
|
16839
|
+
*/
|
|
16840
|
+
function isScannerPath(path) {
|
|
16841
|
+
const segments = path.split("/");
|
|
16842
|
+
const inLayer = segments[1] === ASSETS && segments.length > 2;
|
|
16843
|
+
const inFeature = segments[2] === ASSETS && segments.length > 3;
|
|
16844
|
+
return inLayer || inFeature;
|
|
16845
|
+
}
|
|
16846
|
+
/**
|
|
16847
|
+
* Builds the path index of a manifest: which file and bundle every path leads to, and the folders
|
|
16848
|
+
* the scanner read. A path under one of those folders that leads nowhere is a new asset.
|
|
16849
|
+
*
|
|
16850
|
+
* @param manifest - The manifest the page booted with.
|
|
16851
|
+
* @returns The index.
|
|
16852
|
+
*/
|
|
16853
|
+
function indexPaths(manifest) {
|
|
16854
|
+
const owners = /* @__PURE__ */ new Map();
|
|
16855
|
+
const folders = /* @__PURE__ */ new Set();
|
|
16856
|
+
for (const [bundle, entry] of Object.entries(manifest.bundles)) for (const file of entry.files) {
|
|
16857
|
+
if (file.path === void 0) continue;
|
|
16858
|
+
const owner = {
|
|
16859
|
+
bundle,
|
|
16860
|
+
entry,
|
|
16861
|
+
file,
|
|
16862
|
+
path: file.path
|
|
16863
|
+
};
|
|
16864
|
+
for (const path of [file.path, ...(file.pages ?? []).map((page) => page.path)]) {
|
|
16865
|
+
owners.set(path, owner);
|
|
16866
|
+
folders.add(folderOf(path));
|
|
16867
|
+
}
|
|
16868
|
+
}
|
|
16869
|
+
folders.delete("");
|
|
16870
|
+
return {
|
|
16871
|
+
manifest,
|
|
16872
|
+
owners,
|
|
16873
|
+
folders: [...folders]
|
|
16874
|
+
};
|
|
16875
|
+
}
|
|
16876
|
+
/**
|
|
16877
|
+
* Tells why a stamp cannot be swapped in place: the set of files is not the one of the manifest
|
|
16878
|
+
* the page booted with. A manifest path the watch no longer stamps was removed or renamed, or its
|
|
16879
|
+
* nine-slice tag changed. A changed path the manifest does not know is a new asset when it lies
|
|
16880
|
+
* under a folder the manifest names, or where the scanner reads: the first file of an `assets/`
|
|
16881
|
+
* folder that had none counts too. The watch stamps every image of the game tree, so any other
|
|
16882
|
+
* unknown path (a favicon, an image of a feature outside its `assets/`) refuses nothing.
|
|
16883
|
+
*
|
|
16884
|
+
* @param index - The path index of the manifest.
|
|
16885
|
+
* @param stamps - The stamp that arrived.
|
|
16886
|
+
* @param changed - The paths whose bytes changed.
|
|
16887
|
+
* @returns The reason, or `undefined` when only bytes changed.
|
|
16888
|
+
*/
|
|
16889
|
+
function refusalOf$1(index, stamps, changed) {
|
|
16890
|
+
for (const path of index.owners.keys()) if (!Object.hasOwn(stamps.files, path)) return `"${path}" left the game`;
|
|
16891
|
+
for (const path of changed) {
|
|
16892
|
+
if (index.owners.has(path)) continue;
|
|
16893
|
+
if (isScannerPath(path) || index.folders.some((folder) => path.startsWith(folder))) return `"${path}" is not in the manifest`;
|
|
16894
|
+
}
|
|
16895
|
+
}
|
|
16896
|
+
/**
|
|
16897
|
+
* Groups the changed files by their bundle. A font whose `.fnt` and pages changed in one save is
|
|
16898
|
+
* listed once: a font is replaced as a whole. A path the manifest does not know is left out.
|
|
16899
|
+
*
|
|
16900
|
+
* @param index - The path index of the manifest.
|
|
16901
|
+
* @param changed - The paths whose bytes changed.
|
|
16902
|
+
* @returns One batch per bundle, in the order the paths name them.
|
|
16903
|
+
*/
|
|
16904
|
+
function batchesOf(index, changed) {
|
|
16905
|
+
const batches = /* @__PURE__ */ new Map();
|
|
16906
|
+
for (const path of changed) {
|
|
16907
|
+
const owner = index.owners.get(path);
|
|
16908
|
+
if (owner === void 0) continue;
|
|
16909
|
+
const { bundle, entry } = owner;
|
|
16910
|
+
const batch = batches.get(bundle) ?? {
|
|
16911
|
+
bundle,
|
|
16912
|
+
entry,
|
|
16913
|
+
owners: []
|
|
16914
|
+
};
|
|
16915
|
+
if (!batch.owners.includes(owner)) batch.owners.push(owner);
|
|
16916
|
+
batches.set(bundle, batch);
|
|
16917
|
+
}
|
|
16918
|
+
return [...batches.values()];
|
|
16919
|
+
}
|
|
16920
|
+
//#endregion
|
|
16921
|
+
//#region src/plugins/assets/stamp.ts
|
|
16922
|
+
/** The module the keys watch writes, as the hot footer reports its path. */
|
|
16923
|
+
const STAMP_FILE = "/.moku/assets-stamp.ts";
|
|
16924
|
+
/**
|
|
16925
|
+
* Tells whether a value maps paths to stamps.
|
|
16926
|
+
*
|
|
16927
|
+
* @param value - The `files` member of a stamp module.
|
|
16928
|
+
* @returns True for a plain object whose values are all strings.
|
|
16929
|
+
* @example
|
|
16930
|
+
* ```ts
|
|
16931
|
+
* isStampMap({ "features/ui/assets/fx-spark.webp": "2554:1791536552578" }); // true
|
|
16932
|
+
* ```
|
|
16933
|
+
*/
|
|
16934
|
+
function isStampMap(value) {
|
|
16935
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((stamp) => typeof stamp === "string");
|
|
16936
|
+
}
|
|
16937
|
+
/**
|
|
16938
|
+
* Tells whether a value is a list of paths.
|
|
16939
|
+
*
|
|
16940
|
+
* @param value - The `changed` member of a stamp module.
|
|
16941
|
+
* @returns True for an array of strings.
|
|
16942
|
+
* @example
|
|
16943
|
+
* ```ts
|
|
16944
|
+
* isPathList(["features/ui/assets/fx-spark.webp"]); // true
|
|
16945
|
+
* ```
|
|
16946
|
+
*/
|
|
16947
|
+
function isPathList(value) {
|
|
16948
|
+
return Array.isArray(value) && value.every((path) => typeof path === "string");
|
|
16949
|
+
}
|
|
16950
|
+
/**
|
|
16951
|
+
* Tells the stamp of the keys watch from any other default export.
|
|
16952
|
+
*
|
|
16953
|
+
* @param value - The default export of the stamp module.
|
|
16954
|
+
* @returns True when it carries a `files` map of strings and a `changed` list of strings.
|
|
16955
|
+
* @example
|
|
16956
|
+
* ```ts
|
|
16957
|
+
* isAssetStamps({ files: { "features/ui/assets/popup.mp3": "8120:1791536000000" }, changed: [] }); // true
|
|
16958
|
+
* ```
|
|
16959
|
+
*/
|
|
16960
|
+
function isAssetStamps(value) {
|
|
16961
|
+
if (typeof value !== "object" || value === null) return false;
|
|
16962
|
+
if (!("files" in value) || !("changed" in value)) return false;
|
|
16963
|
+
return isStampMap(value.files) && isPathList(value.changed);
|
|
16964
|
+
}
|
|
16965
|
+
/**
|
|
16966
|
+
* Reads the stamp out of a hot swap, when the saved module is the stamp of the keys watch.
|
|
16967
|
+
*
|
|
16968
|
+
* @param payload - The saved file and its new exports.
|
|
16969
|
+
* @returns The stamp, or `undefined` for any other file and for a module that carries none.
|
|
16970
|
+
* @example
|
|
16971
|
+
* ```ts
|
|
16972
|
+
* stampsOf({ file: "/game/features/hud/view.tsx", module: {} }); // undefined
|
|
16973
|
+
* ```
|
|
16974
|
+
*/
|
|
16975
|
+
function stampsOf(payload) {
|
|
16976
|
+
if (!payload.file.replaceAll("\\", "/").endsWith(STAMP_FILE)) return void 0;
|
|
16977
|
+
const stamps = payload.module.default;
|
|
16978
|
+
return isAssetStamps(stamps) ? stamps : void 0;
|
|
16979
|
+
}
|
|
16980
|
+
/**
|
|
16981
|
+
* Works out which files a stamp changed, and keeps its map for the next one. Against the map
|
|
16982
|
+
* applied last it is every path whose stamp differs. The first update after boot has no map to
|
|
16983
|
+
* compare with, so the list of the keys watch stands.
|
|
16984
|
+
*
|
|
16985
|
+
* @param state - The plugin state.
|
|
16986
|
+
* @param stamps - The stamp that arrived.
|
|
16987
|
+
* @returns The changed paths.
|
|
16988
|
+
*/
|
|
16989
|
+
function changedPaths(state, stamps) {
|
|
16990
|
+
const applied = state.stamps;
|
|
16991
|
+
state.stamps = stamps.files;
|
|
16992
|
+
if (applied === void 0) return stamps.changed;
|
|
16993
|
+
return Object.keys(stamps.files).filter((path) => stamps.files[path] !== applied[path]);
|
|
16994
|
+
}
|
|
16995
|
+
//#endregion
|
|
16996
|
+
//#region src/plugins/assets/swap.ts
|
|
16997
|
+
/**
|
|
16998
|
+
* @file assets plugin — the dev hot swap of asset files. When the stamp of the keys watch changes
|
|
16999
|
+
* (`stamp.ts`), the changed files of the loaded bundles (`paths.ts`) are replaced here: a new
|
|
17000
|
+
* texture, font or sound per key, the old textures destroyed after the new ones are stored, then
|
|
17001
|
+
* `assets:replaced`. A file that fails is logged and its bundle keeps what it had. A changed set
|
|
17002
|
+
* of files reloads the page instead: a hook cannot refuse by throwing, the core bus catches what
|
|
17003
|
+
* a hook throws.
|
|
17004
|
+
*/
|
|
17005
|
+
/**
|
|
17006
|
+
* Reloads the page. This is how a swap refuses, and what it does when a replace breaks after its
|
|
17007
|
+
* files arrived: the page boots again with the new manifest and the new files. Headless and in a
|
|
17008
|
+
* test there is no page, so nothing happens.
|
|
17009
|
+
*/
|
|
17010
|
+
function reloadPage$1() {
|
|
17011
|
+
globalThis.location?.reload();
|
|
17012
|
+
}
|
|
17013
|
+
/**
|
|
17014
|
+
* Says why a file could not be replaced.
|
|
17015
|
+
*
|
|
17016
|
+
* @param error - What the load threw.
|
|
17017
|
+
* @returns The message of an error, or the value as a string.
|
|
17018
|
+
* @example
|
|
17019
|
+
* ```ts
|
|
17020
|
+
* reasonOf(new Error("The source image could not be decoded.")); // "The source image could not be decoded."
|
|
17021
|
+
* ```
|
|
17022
|
+
*/
|
|
17023
|
+
function reasonOf$1(error) {
|
|
17024
|
+
return error instanceof Error ? error.message : String(error);
|
|
17025
|
+
}
|
|
17026
|
+
/**
|
|
17027
|
+
* Copies the io of a swap so every texture it uploads is remembered with the pixel size of its
|
|
17028
|
+
* image. The manifest needs that size, and the loaders of `tiers.ts` answer the texture alone;
|
|
17029
|
+
* the list is also what a swap that does not land gives back. The io is a plain record of
|
|
17030
|
+
* functions, so the copy carries the other four members as they are.
|
|
17031
|
+
*
|
|
17032
|
+
* @param io - The I/O seam of the plugin.
|
|
17033
|
+
* @param sizes - Filled with every texture this io creates.
|
|
17034
|
+
* @returns The io with a measuring `createTexture`.
|
|
17035
|
+
*/
|
|
17036
|
+
function measuringIo(io, sizes) {
|
|
17037
|
+
return {
|
|
17038
|
+
...io,
|
|
17039
|
+
createTexture: (image, options) => {
|
|
17040
|
+
const texture = io.createTexture(image, options);
|
|
17041
|
+
sizes.set(texture, {
|
|
17042
|
+
width: image.width,
|
|
17043
|
+
height: image.height
|
|
17044
|
+
});
|
|
17045
|
+
return texture;
|
|
17046
|
+
}
|
|
17047
|
+
};
|
|
17048
|
+
}
|
|
17049
|
+
/**
|
|
17050
|
+
* Builds the running load the file loaders of `tiers.ts` take, for the files of one bundle.
|
|
17051
|
+
*
|
|
17052
|
+
* @param swap - The running swap.
|
|
17053
|
+
* @param bundle - Name of the bundle.
|
|
17054
|
+
* @param sizes - Filled with every texture the load creates.
|
|
17055
|
+
* @returns The running load.
|
|
17056
|
+
*/
|
|
17057
|
+
function loadingOf(swap, bundle, sizes) {
|
|
17058
|
+
const { config } = swap.ctx;
|
|
17059
|
+
return {
|
|
17060
|
+
io: measuringIo(swap.io, sizes),
|
|
17061
|
+
bundle,
|
|
17062
|
+
base: resolveBaseUrl(config.baseUrl, config.manifest),
|
|
17063
|
+
signal: new AbortController().signal,
|
|
17064
|
+
assets: emptyAssets()
|
|
17065
|
+
};
|
|
17066
|
+
}
|
|
17067
|
+
/**
|
|
17068
|
+
* Loads one changed file again, by what the manifest says it is. A font is read as a whole: its
|
|
17069
|
+
* `.fnt` and every page, whichever of them changed.
|
|
17070
|
+
*
|
|
17071
|
+
* @param run - The running load.
|
|
17072
|
+
* @param owner - The file to load.
|
|
17073
|
+
* @returns What the file brought.
|
|
17074
|
+
* @throws {Error} When a fetch or a decode fails, or the font lists no page.
|
|
17075
|
+
*/
|
|
17076
|
+
async function loadFresh(run, owner) {
|
|
17077
|
+
const kind = kindOf$2(owner.file);
|
|
17078
|
+
if (kind === "font") return {
|
|
17079
|
+
kind,
|
|
17080
|
+
owner,
|
|
17081
|
+
font: await loadFont(run, owner.file)
|
|
17082
|
+
};
|
|
17083
|
+
if (kind === "audio") return {
|
|
17084
|
+
kind,
|
|
17085
|
+
owner,
|
|
17086
|
+
audio: await loadSound(run, owner.file)
|
|
17087
|
+
};
|
|
17088
|
+
return {
|
|
17089
|
+
kind,
|
|
17090
|
+
owner,
|
|
17091
|
+
texture: await loadImage(run, owner.path, nineOf(owner.file))
|
|
17092
|
+
};
|
|
17093
|
+
}
|
|
17094
|
+
/**
|
|
17095
|
+
* Loads every changed file of one bundle in parallel and waits for all of them, so nothing is
|
|
17096
|
+
* still on the way when the bundle is stored or given back.
|
|
17097
|
+
*
|
|
17098
|
+
* @param run - The running load.
|
|
17099
|
+
* @param owners - The files to load.
|
|
17100
|
+
* @returns What arrived, and the files that failed.
|
|
17101
|
+
*/
|
|
17102
|
+
async function loadAll(run, owners) {
|
|
17103
|
+
const fresh = [];
|
|
17104
|
+
const failures = [];
|
|
17105
|
+
await Promise.all(owners.map((owner) => loadFresh(run, owner).then((loaded) => fresh.push(loaded), (error) => failures.push({
|
|
17106
|
+
path: owner.path,
|
|
17107
|
+
reason: reasonOf$1(error)
|
|
17108
|
+
}))));
|
|
17109
|
+
return {
|
|
17110
|
+
fresh,
|
|
17111
|
+
failures
|
|
17112
|
+
};
|
|
17113
|
+
}
|
|
17114
|
+
/**
|
|
17115
|
+
* Reads the record of a bundle once it stands still. A load that runs when the stamp arrives may
|
|
17116
|
+
* have fetched the old bytes, so it lands first.
|
|
17117
|
+
*
|
|
17118
|
+
* @param state - The plugin state.
|
|
17119
|
+
* @param bundle - Name of the bundle.
|
|
17120
|
+
* @returns The record when the bundle is loaded, else `undefined`: its next load reads the new
|
|
17121
|
+
* bytes, the dev server answers every file with `no-store`.
|
|
17122
|
+
*/
|
|
17123
|
+
async function settledRecord(state, bundle) {
|
|
17124
|
+
const record = state.records.get(bundle);
|
|
17125
|
+
if (record?.status === "loading") await record.inflight?.promise;
|
|
17126
|
+
return record?.status === "loaded" ? record : void 0;
|
|
17127
|
+
}
|
|
17128
|
+
/**
|
|
17129
|
+
* Writes the pixel size of a new image, and what it costs, into its manifest record.
|
|
17130
|
+
*
|
|
17131
|
+
* @param target - The record of a texture file or of a font page.
|
|
17132
|
+
* @param size - The size of the decoded image.
|
|
17133
|
+
*/
|
|
17134
|
+
function resize(target, size) {
|
|
17135
|
+
if (target === void 0 || size === void 0) return;
|
|
17136
|
+
target.width = size.width;
|
|
17137
|
+
target.height = size.height;
|
|
17138
|
+
target.mb = textureMb(size.width, size.height);
|
|
17139
|
+
}
|
|
17140
|
+
/**
|
|
17141
|
+
* Stores a new font under its key and frees the pages of the old one. The record of each page and
|
|
17142
|
+
* of the font follow the new images.
|
|
17143
|
+
*
|
|
17144
|
+
* @param io - The I/O seam of the plugin.
|
|
17145
|
+
* @param record - The record of the loaded bundle.
|
|
17146
|
+
* @param owner - The font file.
|
|
17147
|
+
* @param font - The font that was read again.
|
|
17148
|
+
* @param sizes - The pixel size of every new page.
|
|
17149
|
+
*/
|
|
17150
|
+
function storeFont(io, record, owner, font, sizes) {
|
|
17151
|
+
const { file } = owner;
|
|
17152
|
+
const pages = file.pages ?? [];
|
|
17153
|
+
const old = record.fonts.get(file.key);
|
|
17154
|
+
record.fonts.set(file.key, font);
|
|
17155
|
+
for (const page of old?.pages ?? []) io.destroyTexture(page);
|
|
17156
|
+
for (const [at, texture] of font.pages.entries()) resize(pages[at], sizes.get(texture));
|
|
17157
|
+
file.mb = sumMb(pages);
|
|
17158
|
+
}
|
|
17159
|
+
/**
|
|
17160
|
+
* Stores what one changed file brought under its key. An old texture is destroyed only after the
|
|
17161
|
+
* new one is stored, so nothing ever answers a destroyed texture.
|
|
17162
|
+
*
|
|
17163
|
+
* @param io - The I/O seam of the plugin.
|
|
17164
|
+
* @param record - The record of the loaded bundle.
|
|
17165
|
+
* @param fresh - What the file brought.
|
|
17166
|
+
* @param sizes - The pixel size of every new texture.
|
|
17167
|
+
*/
|
|
17168
|
+
function store(io, record, fresh, sizes) {
|
|
17169
|
+
const { file } = fresh.owner;
|
|
17170
|
+
if (fresh.kind === "font") {
|
|
17171
|
+
storeFont(io, record, fresh.owner, fresh.font, sizes);
|
|
17172
|
+
return;
|
|
17173
|
+
}
|
|
17174
|
+
if (fresh.kind === "audio") {
|
|
17175
|
+
record.audio.set(file.key, fresh.audio);
|
|
16414
17176
|
return;
|
|
16415
17177
|
}
|
|
16416
|
-
|
|
16417
|
-
|
|
17178
|
+
const old = record.textures.get(file.key);
|
|
17179
|
+
record.textures.set(file.key, fresh.texture);
|
|
17180
|
+
if (old !== void 0) io.destroyTexture(old);
|
|
17181
|
+
resize(file, sizes.get(fresh.texture));
|
|
16418
17182
|
}
|
|
16419
17183
|
/**
|
|
16420
|
-
*
|
|
17184
|
+
* Makes the numbers and the picture follow the files that were stored: the megabytes of the
|
|
17185
|
+
* bundle, the budget, the views of the keys, the frame loop and the event.
|
|
16421
17186
|
*
|
|
16422
17187
|
* @param ctx - Domain context of the plugin.
|
|
16423
|
-
* @param
|
|
16424
|
-
* @
|
|
17188
|
+
* @param batch - The bundle and its replaced files.
|
|
17189
|
+
* @param record - The record of the bundle.
|
|
16425
17190
|
*/
|
|
16426
|
-
function
|
|
16427
|
-
const
|
|
16428
|
-
|
|
16429
|
-
|
|
17191
|
+
function publish(ctx, batch, record) {
|
|
17192
|
+
const { bundle, entry } = batch;
|
|
17193
|
+
const keys = batch.owners.map((owner) => owner.file.key);
|
|
17194
|
+
entry.mb = sumMb([...entry.files, ...entry.pages ?? []]);
|
|
17195
|
+
enforceBudget(ctx);
|
|
17196
|
+
if (record.status !== "loaded") return;
|
|
17197
|
+
ctx.deps.renderer.sync.textures.invalidate(keys);
|
|
17198
|
+
ctx.deps.time.wake();
|
|
17199
|
+
ctx.emit("assets:replaced", {
|
|
17200
|
+
bundle,
|
|
17201
|
+
keys
|
|
17202
|
+
});
|
|
16430
17203
|
}
|
|
16431
17204
|
/**
|
|
16432
|
-
*
|
|
17205
|
+
* Gives the new files of one bundle up: every file that failed is logged, and every texture the
|
|
17206
|
+
* swap uploaded goes back. Nothing was stored, so the bundle keeps what it had.
|
|
16433
17207
|
*
|
|
16434
|
-
* @param
|
|
16435
|
-
* @
|
|
17208
|
+
* @param swap - The running swap.
|
|
17209
|
+
* @param failures - The files that could not be read.
|
|
17210
|
+
* @param sizes - Every new texture of the bundle.
|
|
16436
17211
|
*/
|
|
16437
|
-
function
|
|
16438
|
-
const
|
|
16439
|
-
for (const
|
|
16440
|
-
|
|
16441
|
-
|
|
16442
|
-
|
|
16443
|
-
|
|
16444
|
-
|
|
16445
|
-
|
|
16446
|
-
|
|
16447
|
-
|
|
17212
|
+
function discard(swap, failures, sizes) {
|
|
17213
|
+
for (const failure of failures) swap.ctx.log.error("assets:replace-failed", failure);
|
|
17214
|
+
for (const texture of sizes.keys()) swap.io.destroyTexture(texture);
|
|
17215
|
+
}
|
|
17216
|
+
/**
|
|
17217
|
+
* Replaces the changed files of one bundle. Every file is loaded first and nothing is stored
|
|
17218
|
+
* before all of them are there. A file that fails is logged and the bundle keeps its old
|
|
17219
|
+
* textures, fonts and bytes: the new textures are given back, nothing is emitted and the page
|
|
17220
|
+
* stays. The stamp of the broken file is applied all the same, so it is read again only when it
|
|
17221
|
+
* is saved again. A bundle that was unloaded on the way gives its new textures back too.
|
|
17222
|
+
*
|
|
17223
|
+
* @param swap - The running swap.
|
|
17224
|
+
* @param batch - The bundle and its changed files.
|
|
17225
|
+
* @throws {Error} What the io, the renderer or the budget throws after the files arrived.
|
|
17226
|
+
*/
|
|
17227
|
+
async function replaceBundle(swap, batch) {
|
|
17228
|
+
const { ctx, io } = swap;
|
|
17229
|
+
const record = await settledRecord(ctx.state, batch.bundle);
|
|
17230
|
+
if (record === void 0) return;
|
|
17231
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
17232
|
+
const { fresh, failures } = await loadAll(loadingOf(swap, batch.bundle, sizes), batch.owners);
|
|
17233
|
+
if (failures.length > 0 || record.status !== "loaded") {
|
|
17234
|
+
discard(swap, failures, sizes);
|
|
17235
|
+
return;
|
|
16448
17236
|
}
|
|
16449
|
-
|
|
16450
|
-
|
|
16451
|
-
textureMb: usedMb(ctx.state),
|
|
16452
|
-
budgetMb: ctx.config.textureBudgetMb,
|
|
16453
|
-
bundles
|
|
16454
|
-
};
|
|
17237
|
+
for (const loaded of fresh) store(io, record, loaded, sizes);
|
|
17238
|
+
publish(ctx, batch, record);
|
|
16455
17239
|
}
|
|
16456
17240
|
/**
|
|
16457
|
-
*
|
|
17241
|
+
* Runs one swap, bundle after bundle: a bundle with a file that failed does not stop the next
|
|
17242
|
+
* one. It never rejects, so the chain of swaps in the state cannot break. A throw after the files
|
|
17243
|
+
* arrived (from a destroy, the views or the budget) leaves a bundle whose stored state is not
|
|
17244
|
+
* known: every file of it is logged as failed, the page reloads and the swap stops there.
|
|
16458
17245
|
*
|
|
16459
|
-
* @param
|
|
16460
|
-
* @
|
|
17246
|
+
* @param swap - The running swap.
|
|
17247
|
+
* @param batches - The changed files, by bundle.
|
|
16461
17248
|
*/
|
|
16462
|
-
function
|
|
16463
|
-
const
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
|
|
16468
|
-
|
|
16469
|
-
|
|
16470
|
-
|
|
16471
|
-
|
|
17249
|
+
async function replaceAll(swap, batches) {
|
|
17250
|
+
for (const batch of batches) try {
|
|
17251
|
+
await replaceBundle(swap, batch);
|
|
17252
|
+
} catch (error) {
|
|
17253
|
+
const reason = reasonOf$1(error);
|
|
17254
|
+
for (const { path } of batch.owners) swap.ctx.log.error("assets:replace-failed", {
|
|
17255
|
+
path,
|
|
17256
|
+
reason
|
|
17257
|
+
});
|
|
17258
|
+
swap.reload();
|
|
17259
|
+
return;
|
|
17260
|
+
}
|
|
17261
|
+
}
|
|
17262
|
+
/**
|
|
17263
|
+
* Creates the handler of the dev hot swap of asset files. It acts only on the stamp of the keys
|
|
17264
|
+
* watch, and only with an io: a headless page has no file. What changed and whether the swap is
|
|
17265
|
+
* refused is decided at once; the replace itself waits for the swap before it, so two saves in a
|
|
17266
|
+
* row never overlap.
|
|
17267
|
+
*
|
|
17268
|
+
* @param ctx - Domain context of the plugin.
|
|
17269
|
+
* @param reload - What a refused or a broken swap calls. The page reload by default; a test
|
|
17270
|
+
* passes a spy.
|
|
17271
|
+
* @returns The handler of `ui:hot-swap`.
|
|
17272
|
+
*/
|
|
17273
|
+
function createSwap$1(ctx, reload = reloadPage$1) {
|
|
17274
|
+
let index;
|
|
17275
|
+
return (payload) => {
|
|
17276
|
+
const { state } = ctx;
|
|
17277
|
+
const stamps = stampsOf(payload);
|
|
17278
|
+
const io = state.io;
|
|
17279
|
+
if (stamps === void 0 || io === void 0) return;
|
|
17280
|
+
const changed = changedPaths(state, stamps);
|
|
17281
|
+
if (index?.manifest !== state.manifest) index = indexPaths(state.manifest);
|
|
17282
|
+
const reason = refusalOf$1(index, stamps, changed);
|
|
17283
|
+
if (reason !== void 0) {
|
|
17284
|
+
ctx.log.info("assets:swap-refused", { reason });
|
|
17285
|
+
reload();
|
|
17286
|
+
return;
|
|
17287
|
+
}
|
|
17288
|
+
const batches = batchesOf(index, changed);
|
|
17289
|
+
if (batches.length === 0) return;
|
|
17290
|
+
state.swapping = (state.swapping ?? Promise.resolve()).then(() => replaceAll({
|
|
17291
|
+
ctx,
|
|
17292
|
+
io,
|
|
17293
|
+
reload
|
|
17294
|
+
}, batches));
|
|
16472
17295
|
};
|
|
16473
17296
|
}
|
|
16474
17297
|
//#endregion
|
|
16475
17298
|
//#region src/plugins/assets/handlers.ts
|
|
16476
17299
|
/**
|
|
16477
|
-
* @file assets plugin — event handlers
|
|
17300
|
+
* @file assets plugin — event handlers: the rest node that settles the memory, and the dev hot
|
|
17301
|
+
* swap that replaces the asset files a save changed.
|
|
16478
17302
|
*/
|
|
16479
17303
|
/**
|
|
16480
17304
|
* Creates the hook handlers. `flow:rest` is the one moment the graph stands still, so it is where
|
|
16481
17305
|
* 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.
|
|
17306
|
+
* background queue in the state and is never awaited here: an engine hook is synchronous. The
|
|
17307
|
+
* global `ui:hot-swap` is hooked with no `depends` on `ui`, as `effects` does: in a dev build it
|
|
17308
|
+
* brings the stamp of the keys watch, and the changed files are replaced behind it.
|
|
16483
17309
|
*
|
|
16484
|
-
*
|
|
17310
|
+
* A handler builds its domain context when it first fires, not while this factory runs: the
|
|
16485
17311
|
* kernel registers hooks before it builds the plugin APIs, so nothing is resolvable yet.
|
|
16486
17312
|
*
|
|
16487
17313
|
* @param ctx - Kernel context of the assets plugin.
|
|
16488
|
-
* @
|
|
17314
|
+
* @param reload - What a refused or a broken hot swap calls. The kernel passes nothing, which
|
|
17315
|
+
* means the page reload; a test passes a spy.
|
|
17316
|
+
* @returns The two hooks of the plugin.
|
|
16489
17317
|
*/
|
|
16490
|
-
function createHandlers$4(ctx) {
|
|
17318
|
+
function createHandlers$4(ctx, reload) {
|
|
16491
17319
|
let assets;
|
|
16492
|
-
|
|
16493
|
-
|
|
16494
|
-
|
|
16495
|
-
|
|
16496
|
-
|
|
16497
|
-
|
|
16498
|
-
|
|
16499
|
-
|
|
16500
|
-
|
|
16501
|
-
|
|
16502
|
-
|
|
16503
|
-
|
|
16504
|
-
|
|
17320
|
+
let swap;
|
|
17321
|
+
return {
|
|
17322
|
+
/**
|
|
17323
|
+
* Settles the texture memory and rebuilds the background preload.
|
|
17324
|
+
*
|
|
17325
|
+
* @param _payload - Where the graph rests. The position itself comes from the state.
|
|
17326
|
+
*/
|
|
17327
|
+
"flow:rest": (_payload) => {
|
|
17328
|
+
assets ??= withDeps$6(ctx);
|
|
17329
|
+
if (assets.state.io === void 0) return;
|
|
17330
|
+
enforceBudget(assets);
|
|
17331
|
+
if (assets.deps.flow.state().mode === "fast") return;
|
|
17332
|
+
startPreload(assets);
|
|
17333
|
+
},
|
|
17334
|
+
/**
|
|
17335
|
+
* Replaces the asset files a dev save changed, when the saved module is the stamp of the keys
|
|
17336
|
+
* watch. Only dev emits the event; the swap answers at once and works behind it.
|
|
17337
|
+
*
|
|
17338
|
+
* @param payload - The saved file and its new exports.
|
|
17339
|
+
*/
|
|
17340
|
+
"ui:hot-swap": (payload) => {
|
|
17341
|
+
if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__) {
|
|
17342
|
+
assets ??= withDeps$6(ctx);
|
|
17343
|
+
swap ??= createSwap$1(assets, reload);
|
|
17344
|
+
swap(payload);
|
|
17345
|
+
}
|
|
17346
|
+
}
|
|
17347
|
+
};
|
|
16505
17348
|
}
|
|
16506
17349
|
//#endregion
|
|
16507
17350
|
//#region src/plugins/assets/state.ts
|
|
@@ -16550,7 +17393,9 @@ function createAssetsState() {
|
|
|
16550
17393
|
current: void 0,
|
|
16551
17394
|
useCounter: 0,
|
|
16552
17395
|
warned: emptySet$2(),
|
|
16553
|
-
removers: []
|
|
17396
|
+
removers: [],
|
|
17397
|
+
stamps: void 0,
|
|
17398
|
+
swapping: void 0
|
|
16554
17399
|
};
|
|
16555
17400
|
}
|
|
16556
17401
|
/**
|
|
@@ -16584,7 +17429,8 @@ const assetsPlugin = /*#__PURE__*/ createPlugin$1("assets", {
|
|
|
16584
17429
|
events: (register) => register.map({
|
|
16585
17430
|
"assets:bundle-loaded": "Every file of a bundle is a texture now",
|
|
16586
17431
|
"assets:bundle-progress": "One more file of a running bundle load settled",
|
|
16587
|
-
"assets:bundle-unloaded": "The textures of a bundle were destroyed"
|
|
17432
|
+
"assets:bundle-unloaded": "The textures of a bundle were destroyed",
|
|
17433
|
+
"assets:replaced": "Files of a loaded bundle were replaced by a dev hot swap"
|
|
16588
17434
|
}),
|
|
16589
17435
|
createState: createAssetsState,
|
|
16590
17436
|
api: createAssetsApi,
|
|
@@ -16655,8 +17501,32 @@ async function untilAborted(ctx, bundle, signal) {
|
|
|
16655
17501
|
if (load.failed && !signal.aborted) throw load.error;
|
|
16656
17502
|
}
|
|
16657
17503
|
/**
|
|
17504
|
+
* Takes the expected scene and clears the record, so one `expect` mounts one scene.
|
|
17505
|
+
*
|
|
17506
|
+
* @param ctx - Domain context of the plugin.
|
|
17507
|
+
* @returns The scene id that was recorded or expected, or `undefined` when there was none.
|
|
17508
|
+
*/
|
|
17509
|
+
function takePending(ctx) {
|
|
17510
|
+
const target = ctx.state.pending;
|
|
17511
|
+
ctx.state.pending = void 0;
|
|
17512
|
+
return target;
|
|
17513
|
+
}
|
|
17514
|
+
/**
|
|
17515
|
+
* Tells whether the node is a transit node of a fast walk: the walk passes through it and builds
|
|
17516
|
+
* no scene there.
|
|
17517
|
+
*
|
|
17518
|
+
* @param node - The node being entered.
|
|
17519
|
+
* @param run - The runner's mode and signal.
|
|
17520
|
+
* @returns True for a node without `rest` entered in fast mode.
|
|
17521
|
+
*/
|
|
17522
|
+
function isFastTransit(node, run) {
|
|
17523
|
+
return run.mode === "fast" && !node.rest;
|
|
17524
|
+
}
|
|
17525
|
+
/**
|
|
16658
17526
|
* Reads the scene the node asks for. In fast mode a transit node only records it: the walk passes
|
|
16659
|
-
* through many of them and the picture is built once, at the rest point.
|
|
17527
|
+
* through many of them and the picture is built once, at the rest point. In live mode a transit
|
|
17528
|
+
* node without a scene of its own takes the expected one, so a restore at a popup that waits for
|
|
17529
|
+
* its answer builds the scene under it; one with its own scene leaves the record alone.
|
|
16660
17530
|
*
|
|
16661
17531
|
* @param ctx - Domain context of the plugin.
|
|
16662
17532
|
* @param node - The node being entered.
|
|
@@ -16665,32 +17535,33 @@ async function untilAborted(ctx, bundle, signal) {
|
|
|
16665
17535
|
*/
|
|
16666
17536
|
function targetOf(ctx, node, run) {
|
|
16667
17537
|
const state = ctx.state;
|
|
16668
|
-
if (run
|
|
16669
|
-
|
|
17538
|
+
if (isFastTransit(node, run)) {
|
|
17539
|
+
state.pending = node.scene ?? state.pending;
|
|
16670
17540
|
return;
|
|
16671
17541
|
}
|
|
16672
|
-
if (!node.rest) return node.scene;
|
|
17542
|
+
if (!node.rest) return node.scene ?? takePending(ctx);
|
|
16673
17543
|
const target = node.scene ?? state.pending;
|
|
16674
17544
|
state.pending = void 0;
|
|
16675
17545
|
if (target === void 0 && state.current === void 0) ctx.log.warn("scenes: a rest node was entered with no scene", { path: node.path });
|
|
16676
17546
|
return target;
|
|
16677
17547
|
}
|
|
16678
17548
|
/**
|
|
16679
|
-
* Reads the scene an `over` node asks for. An over node never switches a mounted scene;
|
|
16680
|
-
*
|
|
16681
|
-
*
|
|
17549
|
+
* Reads the scene an `over` node asks for. An over node never switches a mounted scene; it takes
|
|
17550
|
+
* the expected scene while nothing is mounted, so a restore at a popup builds the scene under it,
|
|
17551
|
+
* whether the popup rests or waits for its answer. In fast mode an over transit node takes
|
|
17552
|
+
* nothing and leaves the record alone: a walk builds no scene on the way. A scene the over node
|
|
17553
|
+
* names itself is only warned about.
|
|
16682
17554
|
*
|
|
16683
17555
|
* @param ctx - Domain context of the plugin.
|
|
16684
17556
|
* @param node - The over node being entered.
|
|
17557
|
+
* @param run - The runner's mode and signal.
|
|
16685
17558
|
* @returns The expected scene id, or `undefined` when nothing has to happen.
|
|
16686
17559
|
*/
|
|
16687
|
-
function overTargetOf(ctx, node) {
|
|
16688
|
-
const state = ctx.state;
|
|
17560
|
+
function overTargetOf(ctx, node, run) {
|
|
16689
17561
|
if (node.scene !== void 0) ctx.log.warn("scenes: an over node cannot name a scene", { path: node.path });
|
|
16690
|
-
if (
|
|
16691
|
-
|
|
16692
|
-
|
|
16693
|
-
return target;
|
|
17562
|
+
if (isFastTransit(node, run)) return void 0;
|
|
17563
|
+
if (ctx.state.current !== void 0) return void 0;
|
|
17564
|
+
return takePending(ctx);
|
|
16694
17565
|
}
|
|
16695
17566
|
/**
|
|
16696
17567
|
* Builds the scene in one synchronous block: the layers, then the projections of the old scene
|
|
@@ -16736,7 +17607,7 @@ function apply(ctx, scene) {
|
|
|
16736
17607
|
* ```
|
|
16737
17608
|
*/
|
|
16738
17609
|
async function enterScene(ctx, node, run) {
|
|
16739
|
-
const target = node.over ? overTargetOf(ctx, node) : targetOf(ctx, node, run);
|
|
17610
|
+
const target = node.over ? overTargetOf(ctx, node, run) : targetOf(ctx, node, run);
|
|
16740
17611
|
if (target === void 0 || target === ctx.state.current) return;
|
|
16741
17612
|
const scene = ctx.state.scenes.get(target);
|
|
16742
17613
|
if (scene === void 0) throw new Error(`[game] Scene "${target}" is not registered.\n List it in the scenes of a feature.`);
|
|
@@ -17868,9 +18739,9 @@ async function stopAudio(state) {
|
|
|
17868
18739
|
//#endregion
|
|
17869
18740
|
//#region src/plugins/audio/handlers.ts
|
|
17870
18741
|
/**
|
|
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.
|
|
18742
|
+
* @file audio plugin — the five hooks. They are the whole of what drives this plugin: the player
|
|
18743
|
+
* sets the volumes, the graph sets the music, the OS sets the pause, and the budget and a dev hot
|
|
18744
|
+
* swap set what is still decoded.
|
|
17874
18745
|
*/
|
|
17875
18746
|
/**
|
|
17876
18747
|
* Applies the volumes of the committed player. A `volumes` that throws — a save without the
|
|
@@ -17921,12 +18792,25 @@ function resumeAfterPause(ctx) {
|
|
|
17921
18792
|
});
|
|
17922
18793
|
}
|
|
17923
18794
|
/**
|
|
17924
|
-
*
|
|
18795
|
+
* Forgets the decoded buffer and the one warning of every key named. A key that is no sound is in
|
|
18796
|
+
* neither, so it costs nothing. A source that is playing holds its own buffer and plays on.
|
|
18797
|
+
*
|
|
18798
|
+
* @param state - The plugin state.
|
|
18799
|
+
* @param keys - Asset keys whose bytes left or changed.
|
|
18800
|
+
*/
|
|
18801
|
+
function evict(state, keys) {
|
|
18802
|
+
for (const key of keys) {
|
|
18803
|
+
state.decoded.delete(key);
|
|
18804
|
+
state.warned.delete(key);
|
|
18805
|
+
}
|
|
18806
|
+
}
|
|
18807
|
+
/**
|
|
18808
|
+
* Creates the five hook handlers. Each one builds the domain context when it first fires, not
|
|
17925
18809
|
* while this factory runs: the kernel registers hooks before it builds the plugin APIs, so
|
|
17926
18810
|
* nothing is resolvable yet.
|
|
17927
18811
|
*
|
|
17928
18812
|
* @param ctx - Kernel context of the audio plugin.
|
|
17929
|
-
* @returns The
|
|
18813
|
+
* @returns The five hooks of the plugin.
|
|
17930
18814
|
*/
|
|
17931
18815
|
function createHandlers$3(ctx) {
|
|
17932
18816
|
let audio;
|
|
@@ -17986,11 +18870,17 @@ function createHandlers$3(ctx) {
|
|
|
17986
18870
|
* @param payload - The bundle that left and the keys it carried.
|
|
17987
18871
|
*/
|
|
17988
18872
|
"assets:bundle-unloaded": (payload) => {
|
|
17989
|
-
|
|
17990
|
-
|
|
17991
|
-
|
|
17992
|
-
|
|
17993
|
-
|
|
18873
|
+
evict(domain().state, payload.keys);
|
|
18874
|
+
},
|
|
18875
|
+
/**
|
|
18876
|
+
* Drops the decoded sounds a dev hot swap replaced: `assets.audio(key)` already answers the
|
|
18877
|
+
* new bytes, so the next play of a key decodes them. Nothing that is playing is stopped: a
|
|
18878
|
+
* source keeps the buffer it started with and a streamed track keeps its Blob.
|
|
18879
|
+
*
|
|
18880
|
+
* @param payload - The bundle whose files were replaced and the keys with new bytes.
|
|
18881
|
+
*/
|
|
18882
|
+
"assets:replaced": (payload) => {
|
|
18883
|
+
evict(domain().state, payload.keys);
|
|
17994
18884
|
}
|
|
17995
18885
|
};
|
|
17996
18886
|
}
|
|
@@ -20694,11 +21584,25 @@ function stopEffects(state) {
|
|
|
20694
21584
|
//#endregion
|
|
20695
21585
|
//#region src/plugins/effects/handlers.ts
|
|
20696
21586
|
/**
|
|
20697
|
-
* @file effects plugin — the
|
|
20698
|
-
* textures along, particles and `Displacement` maps alike, whatever their space;
|
|
20699
|
-
*
|
|
21587
|
+
* @file effects plugin — the three hooks: a bundle that left takes the effects drawn with its
|
|
21588
|
+
* textures along, particles and `Displacement` maps alike, whatever their space; so do the files
|
|
21589
|
+
* a dev hot swap replaced, whose effects are built again from the new textures; a dev hot swap of
|
|
21590
|
+
* a module replaces the registered emitters it exports.
|
|
20700
21591
|
*/
|
|
20701
21592
|
/**
|
|
21593
|
+
* Retires everything drawn with the textures of `keys`: the particle instances and their bakes,
|
|
21594
|
+
* the `Displacement` filters, and the one-shot warning of each key, so it can warn again. The
|
|
21595
|
+
* next frame builds each effect again from the texture its key answers then.
|
|
21596
|
+
*
|
|
21597
|
+
* @param effects - Domain context of the effects plugin.
|
|
21598
|
+
* @param keys - The asset keys whose textures are gone.
|
|
21599
|
+
*/
|
|
21600
|
+
function retireKeys(effects, keys) {
|
|
21601
|
+
retireParticleKeys(effects, keys);
|
|
21602
|
+
retireFilterMaps(effects, keys);
|
|
21603
|
+
for (const key of keys) effects.state.warned.delete(`texture:${key}`);
|
|
21604
|
+
}
|
|
21605
|
+
/**
|
|
20702
21606
|
* Replaces every registered emitter the saved module exports and drops its bake, so the next
|
|
20703
21607
|
* instance bakes the new config; a live instance keeps its own bake. An id no feature registered
|
|
20704
21608
|
* warns `effects:hot-unknown-emitter`: a new emitter needs a feature, so a reload.
|
|
@@ -20736,9 +21640,19 @@ function createHandlers$2(ctx) {
|
|
|
20736
21640
|
*/
|
|
20737
21641
|
"assets:bundle-unloaded": (payload) => {
|
|
20738
21642
|
effects ??= withDeps$3(ctx);
|
|
20739
|
-
|
|
20740
|
-
|
|
20741
|
-
|
|
21643
|
+
retireKeys(effects, payload.keys);
|
|
21644
|
+
},
|
|
21645
|
+
/**
|
|
21646
|
+
* Retires the particle instances and the `Displacement` filters drawn with the textures a dev
|
|
21647
|
+
* hot swap replaced and drops their bakes. The old textures are destroyed and the new ones
|
|
21648
|
+
* stand under the same keys, so the next frame builds each effect again from the new one.
|
|
21649
|
+
* Only dev emits it.
|
|
21650
|
+
*
|
|
21651
|
+
* @param payload - The bundle and the keys with new bytes.
|
|
21652
|
+
*/
|
|
21653
|
+
"assets:replaced": (payload) => {
|
|
21654
|
+
effects ??= withDeps$3(ctx);
|
|
21655
|
+
retireKeys(effects, payload.keys);
|
|
20742
21656
|
},
|
|
20743
21657
|
/**
|
|
20744
21658
|
* Replaces the registered emitters a dev hot swap brought. Only dev emits it.
|
|
@@ -24998,6 +25912,8 @@ function parseTags(source, warn) {
|
|
|
24998
25912
|
const CACHE_SEPARATOR = String.fromCodePoint(0);
|
|
24999
25913
|
/** How many laid-out blocks are kept. The oldest goes when a new one does not fit. */
|
|
25000
25914
|
const CACHE_LIMIT = 1024;
|
|
25915
|
+
/** How every inline icon tag starts. A resolved text without it holds no icon. */
|
|
25916
|
+
const ICON_OPEN = "<icon=";
|
|
25001
25917
|
/** The name the system is registered under; it shows up in a world error message. */
|
|
25002
25918
|
const TEXT_SYSTEM_NAME = "text.resolve";
|
|
25003
25919
|
/**
|
|
@@ -25302,9 +26218,36 @@ function rememberBound(ctx, entity, text, unit) {
|
|
|
25302
26218
|
seen.locale = ctx.deps.i18n.locale();
|
|
25303
26219
|
}
|
|
25304
26220
|
/**
|
|
26221
|
+
* Writes the string a bound label already shows once more, and measures it again. A mark on a
|
|
26222
|
+
* label whose number stands still means the picture under it moved: its font arrived or was
|
|
26223
|
+
* replaced, or its style was swapped. The display adapter rebuilds only what `changed(Text)`
|
|
26224
|
+
* names, so the same string is written.
|
|
26225
|
+
*
|
|
26226
|
+
* @param ctx - Domain context of the text plugin.
|
|
26227
|
+
* @param entity - The entity.
|
|
26228
|
+
* @param text - Its component value.
|
|
26229
|
+
*/
|
|
26230
|
+
function showAgain(ctx, entity, text) {
|
|
26231
|
+
ctx.deps.world.ecs.set(entity, Text, { resolved: text.resolved });
|
|
26232
|
+
ctx.state.measured.set(entity, sizeOf(layoutFor(ctx, text.resolved, text.style)));
|
|
26233
|
+
}
|
|
26234
|
+
/**
|
|
26235
|
+
* Reports a bound label whose style is not a digits style, once per style. An unknown style name
|
|
26236
|
+
* is not reported here: `styleOf` reports it by its name.
|
|
26237
|
+
*
|
|
26238
|
+
* @param ctx - Domain context of the text plugin.
|
|
26239
|
+
* @param styleName - The style name of the bound label.
|
|
26240
|
+
*/
|
|
26241
|
+
function warnWithoutDigits(ctx, styleName) {
|
|
26242
|
+
const style = ctx.state.styles.get(styleName);
|
|
26243
|
+
if (style === void 0 || style.digits) return;
|
|
26244
|
+
warnOnce(ctx, `bind-style:${styleName}`, "text: bind on a style without digits", { style: styleName });
|
|
26245
|
+
}
|
|
26246
|
+
/**
|
|
25305
26247
|
* One frame of a bound label: the value read every frame, the string built and written only
|
|
25306
26248
|
* 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"`.
|
|
26249
|
+
* parsed, and the locale is read only for a `"duration"`. A marked label that stands still is
|
|
26250
|
+
* written once with the string it shows.
|
|
25308
26251
|
*
|
|
25309
26252
|
* @param ctx - Domain context of the text plugin.
|
|
25310
26253
|
* @param entity - The entity.
|
|
@@ -25313,13 +26256,15 @@ function rememberBound(ctx, entity, text, unit) {
|
|
|
25313
26256
|
* @param frame - What this run of the system shares.
|
|
25314
26257
|
*/
|
|
25315
26258
|
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 });
|
|
26259
|
+
warnWithoutDigits(ctx, text.style);
|
|
25318
26260
|
const value = boundValue(ctx, entity, bind, frame);
|
|
25319
26261
|
const format = value === void 0 ? "int" : formatOf(ctx, entity, bind, value);
|
|
25320
26262
|
const unit = value === void 0 ? void 0 : unitOf(value, format);
|
|
25321
|
-
ctx.state.dirty.delete(entity);
|
|
25322
|
-
if (isShown(ctx, entity, text, unit, format))
|
|
26263
|
+
const marked = ctx.state.dirty.delete(entity);
|
|
26264
|
+
if (isShown(ctx, entity, text, unit, format)) {
|
|
26265
|
+
if (marked) showAgain(ctx, entity, text);
|
|
26266
|
+
return;
|
|
26267
|
+
}
|
|
25323
26268
|
showBound(ctx, entity, text, value === void 0 ? "" : formatBound(value, format, (ms) => ctx.deps.i18n.duration(ms)), value);
|
|
25324
26269
|
rememberBound(ctx, entity, text, unit);
|
|
25325
26270
|
}
|
|
@@ -25391,17 +26336,43 @@ function createTextSystem(ctx) {
|
|
|
25391
26336
|
});
|
|
25392
26337
|
}
|
|
25393
26338
|
/**
|
|
25394
|
-
* Marks labels for the next layout phase: every one of them, or only the ones
|
|
25395
|
-
*
|
|
26339
|
+
* Marks labels for the next layout phase: every one of them, or only the ones `keep` names. A
|
|
26340
|
+
* locale change names the labels whose content is a message; a dev hot swap names the labels
|
|
26341
|
+
* drawn with a replaced font or icon.
|
|
25396
26342
|
*
|
|
25397
26343
|
* @param ctx - Domain context of the text plugin.
|
|
25398
|
-
* @param
|
|
26344
|
+
* @param keep - Tells whether a label is marked. Left out, every label is.
|
|
26345
|
+
* @returns How many labels were marked.
|
|
25399
26346
|
*/
|
|
25400
|
-
function markDirty(ctx,
|
|
26347
|
+
function markDirty(ctx, keep = () => true) {
|
|
26348
|
+
let marked = 0;
|
|
25401
26349
|
for (const [entity, text] of ctx.deps.world.ecs.query(Text)) {
|
|
25402
|
-
if (
|
|
26350
|
+
if (!keep(text)) continue;
|
|
25403
26351
|
ctx.state.dirty.add(entity);
|
|
26352
|
+
marked += 1;
|
|
25404
26353
|
}
|
|
26354
|
+
return marked;
|
|
26355
|
+
}
|
|
26356
|
+
/**
|
|
26357
|
+
* Tells whether a label is drawn with one of the asset keys: a font its style names, or an inline
|
|
26358
|
+
* icon of its resolved text. The text is read through the tag grammar, the way it is drawn, so an
|
|
26359
|
+
* escaped `\<icon=…>` is text and not an icon. A text that holds no `<icon=` at all is not read:
|
|
26360
|
+
* most labels have no icon, and this runs for every label of a swap.
|
|
26361
|
+
*
|
|
26362
|
+
* @param ctx - Domain context of the text plugin.
|
|
26363
|
+
* @param text - The component value of the label.
|
|
26364
|
+
* @param keys - The asset keys to look for.
|
|
26365
|
+
* @returns True when a glyph font or an icon of the label is one of `keys`.
|
|
26366
|
+
*/
|
|
26367
|
+
function drawsWith(ctx, text, keys) {
|
|
26368
|
+
const style = styleOf$1(ctx, text.style);
|
|
26369
|
+
if ([
|
|
26370
|
+
style.font,
|
|
26371
|
+
style.bold,
|
|
26372
|
+
style.italic
|
|
26373
|
+
].some((font) => font !== void 0 && keys.includes(font))) return true;
|
|
26374
|
+
if (!text.resolved.includes(ICON_OPEN)) return false;
|
|
26375
|
+
return parseTags(text.resolved, warnFor(ctx)).some((run) => run.kind === "icon" && keys.includes(run.key));
|
|
25405
26376
|
}
|
|
25406
26377
|
/**
|
|
25407
26378
|
* Forgets an entity whose `Text` left. Works on the state alone, because it is also what the
|
|
@@ -25430,6 +26401,40 @@ const SYNTHETIC_ITALIC = -.2;
|
|
|
25430
26401
|
/** What a shadow copy is painted with, so its tint alone gives it the shadow colour. */
|
|
25431
26402
|
const WHITE = 16777215;
|
|
25432
26403
|
/**
|
|
26404
|
+
* Reads one font when `assets` can answer for it: the advance table first, then the install in the
|
|
26405
|
+
* renderer. A font that has both already is left alone.
|
|
26406
|
+
*
|
|
26407
|
+
* @param ctx - Domain context of the text plugin.
|
|
26408
|
+
* @param key - The asset key of the font.
|
|
26409
|
+
* @returns True when its advance table arrived, so what was measured without it is stale.
|
|
26410
|
+
* @throws {Error} When the `.fnt` is in neither BMFont format.
|
|
26411
|
+
*/
|
|
26412
|
+
function installFont(ctx, key) {
|
|
26413
|
+
const renderer = ctx.deps.renderer;
|
|
26414
|
+
const needsTable = !ctx.state.tables.has(key);
|
|
26415
|
+
const needsInstall = renderer.host.ready() && !ctx.state.installed.has(key);
|
|
26416
|
+
if (!needsTable && !needsInstall) return false;
|
|
26417
|
+
const font = ctx.deps.assets.font(key);
|
|
26418
|
+
if (font === void 0) return false;
|
|
26419
|
+
if (needsTable) ctx.state.tables.set(key, parseAdvances(font.fnt, key));
|
|
26420
|
+
if (needsInstall) {
|
|
26421
|
+
renderer.sync.fonts.install(key, font.fnt, font.texture);
|
|
26422
|
+
ctx.state.installed.add(key);
|
|
26423
|
+
}
|
|
26424
|
+
return needsTable;
|
|
26425
|
+
}
|
|
26426
|
+
/**
|
|
26427
|
+
* Forgets one font: its advance table and the mark that the renderer holds it, so the next
|
|
26428
|
+
* `installFont` reads both again.
|
|
26429
|
+
*
|
|
26430
|
+
* @param ctx - Domain context of the text plugin.
|
|
26431
|
+
* @param key - The asset key of the font.
|
|
26432
|
+
*/
|
|
26433
|
+
function dropFont(ctx, key) {
|
|
26434
|
+
ctx.state.tables.delete(key);
|
|
26435
|
+
ctx.state.installed.delete(key);
|
|
26436
|
+
}
|
|
26437
|
+
/**
|
|
25433
26438
|
* Reads the fonts of `fontKeys` that `assets` can answer for: the advance table first, then the
|
|
25434
26439
|
* install in the renderer. Runs in `onStart` and on every bundle that lands.
|
|
25435
26440
|
*
|
|
@@ -25437,24 +26442,8 @@ const WHITE = 16777215;
|
|
|
25437
26442
|
* @throws {Error} When a `.fnt` is in neither BMFont format.
|
|
25438
26443
|
*/
|
|
25439
26444
|
function installFonts(ctx) {
|
|
25440
|
-
const renderer = ctx.deps.renderer;
|
|
25441
|
-
const ready = renderer.host.ready();
|
|
25442
26445
|
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
|
-
}
|
|
26446
|
+
for (const key of ctx.state.fontKeys) if (installFont(ctx, key)) arrived = true;
|
|
25458
26447
|
if (arrived) refresh(ctx);
|
|
25459
26448
|
}
|
|
25460
26449
|
/**
|
|
@@ -25467,8 +26456,7 @@ function releaseFonts(ctx, keys) {
|
|
|
25467
26456
|
let left = false;
|
|
25468
26457
|
for (const key of keys) {
|
|
25469
26458
|
if (!ctx.state.fontKeys.has(key)) continue;
|
|
25470
|
-
ctx
|
|
25471
|
-
ctx.state.installed.delete(key);
|
|
26459
|
+
dropFont(ctx, key);
|
|
25472
26460
|
left = true;
|
|
25473
26461
|
}
|
|
25474
26462
|
if (left) refresh(ctx);
|
|
@@ -25483,6 +26471,73 @@ function refresh(ctx) {
|
|
|
25483
26471
|
markDirty(ctx);
|
|
25484
26472
|
}
|
|
25485
26473
|
/**
|
|
26474
|
+
* Reloads the page. This is how a font replace that failed ends, so the game is not left drawing
|
|
26475
|
+
* with a font whose table was released. Headless and in a test there is no page, so nothing
|
|
26476
|
+
* happens.
|
|
26477
|
+
*/
|
|
26478
|
+
function reloadPage() {
|
|
26479
|
+
globalThis.location?.reload();
|
|
26480
|
+
}
|
|
26481
|
+
/**
|
|
26482
|
+
* Says why a font could not be installed again.
|
|
26483
|
+
*
|
|
26484
|
+
* @param error - What the install threw.
|
|
26485
|
+
* @returns The message of an error, or the value as a string.
|
|
26486
|
+
* @example
|
|
26487
|
+
* ```ts
|
|
26488
|
+
* reasonOf(new Error("The page is gone.")); // "The page is gone."
|
|
26489
|
+
* ```
|
|
26490
|
+
*/
|
|
26491
|
+
function reasonOf(error) {
|
|
26492
|
+
return error instanceof Error ? error.message : String(error);
|
|
26493
|
+
}
|
|
26494
|
+
/**
|
|
26495
|
+
* Installs every replaced font again, one by one: its table and its mark go, then the new file
|
|
26496
|
+
* and the new page are read. No other font is touched and no label is marked here. The stamp of
|
|
26497
|
+
* the keys watch is written after a failed scan too, so a `.fnt` the scanner refused still gets
|
|
26498
|
+
* here, and reading it throws after its table was released. That font is logged and the page
|
|
26499
|
+
* reloads.
|
|
26500
|
+
*
|
|
26501
|
+
* @param ctx - Domain context of the text plugin.
|
|
26502
|
+
* @param fonts - The font keys among what a dev hot swap replaced.
|
|
26503
|
+
* @param reload - What a font that cannot be installed calls.
|
|
26504
|
+
* @returns False when a font failed, so the page is on its way out.
|
|
26505
|
+
*/
|
|
26506
|
+
function reinstallFonts(ctx, fonts, reload) {
|
|
26507
|
+
for (const key of fonts) try {
|
|
26508
|
+
dropFont(ctx, key);
|
|
26509
|
+
installFont(ctx, key);
|
|
26510
|
+
} catch (error) {
|
|
26511
|
+
ctx.log.error("text:font-replace-failed", {
|
|
26512
|
+
key,
|
|
26513
|
+
reason: reasonOf(error)
|
|
26514
|
+
});
|
|
26515
|
+
reload();
|
|
26516
|
+
return false;
|
|
26517
|
+
}
|
|
26518
|
+
return true;
|
|
26519
|
+
}
|
|
26520
|
+
/**
|
|
26521
|
+
* Applies what a dev hot swap replaced. `assets` answers the new font and the new texture of
|
|
26522
|
+
* every key already, and the old pages and textures are destroyed. A font among the keys is
|
|
26523
|
+
* installed again here, so the font registry of the renderer holds the new page at once. Only
|
|
26524
|
+
* the labels drawn with a replaced font or inline icon are marked, and the generation moves: the
|
|
26525
|
+
* next frame writes each of them, and the adapter builds it again from the new textures even when
|
|
26526
|
+
* its string stands still. A label of another font is not written and keeps its objects.
|
|
26527
|
+
*
|
|
26528
|
+
* @param ctx - Domain context of the text plugin.
|
|
26529
|
+
* @param keys - The asset keys with new bytes.
|
|
26530
|
+
* @param reload - What a font that cannot be installed calls. The page reload by default; a test
|
|
26531
|
+
* passes a spy.
|
|
26532
|
+
*/
|
|
26533
|
+
function applyReplaced(ctx, keys, reload = reloadPage) {
|
|
26534
|
+
const fonts = keys.filter((key) => ctx.state.fontKeys.has(key));
|
|
26535
|
+
if (fonts.length > 0) ctx.state.cache.clear();
|
|
26536
|
+
if (!reinstallFonts(ctx, fonts, reload)) return;
|
|
26537
|
+
if (markDirty(ctx, (text) => drawsWith(ctx, text, keys)) === 0) return;
|
|
26538
|
+
ctx.state.generation += 1;
|
|
26539
|
+
}
|
|
26540
|
+
/**
|
|
25486
26541
|
* Where a line starts inside the widest one.
|
|
25487
26542
|
*
|
|
25488
26543
|
* @param style - The style of the label.
|
|
@@ -25698,7 +26753,7 @@ function buildRun(ctx, pixi, run, style, at) {
|
|
|
25698
26753
|
return objects;
|
|
25699
26754
|
}
|
|
25700
26755
|
/**
|
|
25701
|
-
* The style and the laid-out block of one component value.
|
|
26756
|
+
* The style and the laid-out block of one component value, under the generation of this moment.
|
|
25702
26757
|
*
|
|
25703
26758
|
* @param ctx - Domain context of the text plugin.
|
|
25704
26759
|
* @param value - The component value.
|
|
@@ -25707,7 +26762,8 @@ function buildRun(ctx, pixi, run, style, at) {
|
|
|
25707
26762
|
function labelOf(ctx, value) {
|
|
25708
26763
|
return {
|
|
25709
26764
|
style: styleOf$1(ctx, value.style),
|
|
25710
|
-
layout: layoutFor(ctx, value.resolved, value.style)
|
|
26765
|
+
layout: layoutFor(ctx, value.resolved, value.style),
|
|
26766
|
+
generation: ctx.state.generation
|
|
25711
26767
|
};
|
|
25712
26768
|
}
|
|
25713
26769
|
/**
|
|
@@ -25821,10 +26877,27 @@ function samePoint(first, second) {
|
|
|
25821
26877
|
return first.x === second.x && first.y === second.y;
|
|
25822
26878
|
}
|
|
25823
26879
|
/**
|
|
26880
|
+
* Tells whether a container already shows a value: the string, the style name and the anchor it
|
|
26881
|
+
* was last updated with, drawn with the style object that is current and under the generation
|
|
26882
|
+
* that is current. A replaced style is a new object under the same name. A font or an icon a dev
|
|
26883
|
+
* hot swap replaced moves the generation, because the objects hold textures that are destroyed.
|
|
26884
|
+
*
|
|
26885
|
+
* @param ctx - Domain context of the text plugin.
|
|
26886
|
+
* @param container - The container of this label.
|
|
26887
|
+
* @param previous - The value the container was drawn from.
|
|
26888
|
+
* @param next - The value to draw now.
|
|
26889
|
+
* @returns True when nothing has to be drawn.
|
|
26890
|
+
*/
|
|
26891
|
+
function isCurrent(ctx, container, previous, next) {
|
|
26892
|
+
const drawn = ctx.state.drawn.get(container);
|
|
26893
|
+
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;
|
|
26894
|
+
}
|
|
26895
|
+
/**
|
|
25824
26896
|
* 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.
|
|
26897
|
+
* objects: with the same style object, the same generation, the same anchor and the same lines
|
|
26898
|
+
* and runs, the new text and spots are written into them. Anything else frees the children and
|
|
26899
|
+
* fills the container again. A label of an older generation is always filled again: writing the
|
|
26900
|
+
* string it shows already into a `BitmapText` rebuilds no glyph.
|
|
25828
26901
|
*
|
|
25829
26902
|
* @param ctx - Domain context of the text plugin.
|
|
25830
26903
|
* @param pixi - The Pixi module the renderer loaded.
|
|
@@ -25835,7 +26908,7 @@ function samePoint(first, second) {
|
|
|
25835
26908
|
function redraw(ctx, pixi, container, previous, next) {
|
|
25836
26909
|
const drawn = ctx.state.drawn.get(container);
|
|
25837
26910
|
const label = labelOf(ctx, next);
|
|
25838
|
-
if (drawn?.style === label.style && samePoint(previous.anchor, next.anchor) && sameShape(drawn.layout, label.layout)) {
|
|
26911
|
+
if (drawn?.style === label.style && drawn.generation === label.generation && samePoint(previous.anchor, next.anchor) && sameShape(drawn.layout, label.layout)) {
|
|
25839
26912
|
retext(ctx, pixi, container, next, label);
|
|
25840
26913
|
return;
|
|
25841
26914
|
}
|
|
@@ -25869,7 +26942,7 @@ function createTextAdapter(ctx) {
|
|
|
25869
26942
|
if (pixi === void 0 || object === void 0) return;
|
|
25870
26943
|
const container = asContainer(object);
|
|
25871
26944
|
container.alpha = next.alpha;
|
|
25872
|
-
if (
|
|
26945
|
+
if (isCurrent(ctx, container, previous, next)) return;
|
|
25873
26946
|
redraw(ctx, pixi, container, previous, next);
|
|
25874
26947
|
},
|
|
25875
26948
|
destroy: (object) => {
|
|
@@ -26080,19 +27153,22 @@ function createTextApi(ctx) {
|
|
|
26080
27153
|
//#endregion
|
|
26081
27154
|
//#region src/plugins/text/handlers.ts
|
|
26082
27155
|
/**
|
|
26083
|
-
* @file text plugin — the
|
|
26084
|
-
* bundle takes them away, a locale change marks the messages
|
|
26085
|
-
* is where the strings are built
|
|
27156
|
+
* @file text plugin — the four hooks. None of them draws in place: a bundle brings fonts, a
|
|
27157
|
+
* bundle takes them away, a locale change marks the messages, a dev hot swap marks the labels of
|
|
27158
|
+
* a replaced font or icon. The layout phase of the next frame is where the strings are built
|
|
27159
|
+
* again.
|
|
26086
27160
|
*/
|
|
26087
27161
|
/**
|
|
26088
|
-
* Creates the
|
|
27162
|
+
* Creates the four hook handlers. Each one builds the domain context when it first fires, not
|
|
26089
27163
|
* while this factory runs: the kernel registers hooks before it builds the plugin APIs, so
|
|
26090
27164
|
* nothing is resolvable yet.
|
|
26091
27165
|
*
|
|
26092
27166
|
* @param ctx - Kernel context of the text plugin.
|
|
26093
|
-
* @
|
|
27167
|
+
* @param reload - What a font that a dev hot swap replaced and that cannot be installed calls.
|
|
27168
|
+
* The kernel passes nothing, which means the page reload; a test passes a spy.
|
|
27169
|
+
* @returns The four hooks of the plugin.
|
|
26094
27170
|
*/
|
|
26095
|
-
function createHandlers(ctx) {
|
|
27171
|
+
function createHandlers(ctx, reload) {
|
|
26096
27172
|
let text;
|
|
26097
27173
|
/**
|
|
26098
27174
|
* The domain context, built on the first hook that fires.
|
|
@@ -26122,6 +27198,15 @@ function createHandlers(ctx) {
|
|
|
26122
27198
|
releaseFonts(domain(), payload.keys);
|
|
26123
27199
|
},
|
|
26124
27200
|
/**
|
|
27201
|
+
* Installs the fonts a dev hot swap replaced again and marks the labels drawn with a replaced
|
|
27202
|
+
* font or inline icon. The next frame builds each of them again from the new textures.
|
|
27203
|
+
*
|
|
27204
|
+
* @param payload - The bundle and the asset keys that have new bytes.
|
|
27205
|
+
*/
|
|
27206
|
+
"assets:replaced": (payload) => {
|
|
27207
|
+
applyReplaced(domain(), payload.keys, reload);
|
|
27208
|
+
},
|
|
27209
|
+
/**
|
|
26125
27210
|
* Marks every label whose content is a message. The next layout phase resolves them again,
|
|
26126
27211
|
* and `ui` solves once over the new sizes; no projection is re-run.
|
|
26127
27212
|
*
|
|
@@ -26129,7 +27214,7 @@ function createHandlers(ctx) {
|
|
|
26129
27214
|
*/
|
|
26130
27215
|
"i18n:locale-changed": (_payload) => {
|
|
26131
27216
|
const tctx = domain();
|
|
26132
|
-
markDirty(tctx,
|
|
27217
|
+
markDirty(tctx, (label) => typeof label.content !== "string");
|
|
26133
27218
|
tctx.state.cache.clear();
|
|
26134
27219
|
tctx.deps.time.wake();
|
|
26135
27220
|
}
|
|
@@ -26180,6 +27265,7 @@ function createTextState$1() {
|
|
|
26180
27265
|
cache: emptyMap$2(),
|
|
26181
27266
|
drawn: emptyWeakMap(),
|
|
26182
27267
|
dirty: emptySet$1(),
|
|
27268
|
+
generation: 0,
|
|
26183
27269
|
warned: emptySet$1(),
|
|
26184
27270
|
removers: []
|
|
26185
27271
|
};
|
|
@@ -31844,6 +32930,8 @@ function createLayoutApi(ctx) {
|
|
|
31844
32930
|
const HOT_GLOBAL = "__moku_hot";
|
|
31845
32931
|
/** A strings module `assets:keys` generates; the group is its locale. */
|
|
31846
32932
|
const STRINGS_FILE = /\/generated\/strings\.([\w-]+)\.ts$/;
|
|
32933
|
+
/** The stamp module the keys watch of `moku-game dev` writes: it names asset files, no view. */
|
|
32934
|
+
const ASSETS_STAMP = "/.moku/assets-stamp.ts";
|
|
31847
32935
|
/**
|
|
31848
32936
|
* The `kind` of every value a game registers by value at start: ECS component and tag types
|
|
31849
32937
|
* (filters too), nodes, slots, flows, asset bundles and plugin owners. Any other `kind` marks
|
|
@@ -32002,6 +33090,41 @@ function localeOf(file) {
|
|
|
32002
33090
|
return STRINGS_FILE.exec(file.replaceAll("\\", "/"))?.[1];
|
|
32003
33091
|
}
|
|
32004
33092
|
/**
|
|
33093
|
+
* Tells whether the saved module is the stamp of the keys watch, read from its path with `/`
|
|
33094
|
+
* separators.
|
|
33095
|
+
*
|
|
33096
|
+
* @param file - The path of the saved module.
|
|
33097
|
+
* @returns True for `.moku/assets-stamp.ts` of a game.
|
|
33098
|
+
* @example
|
|
33099
|
+
* ```ts
|
|
33100
|
+
* isAssetsStamp("/game/.moku/assets-stamp.ts"); // true
|
|
33101
|
+
* ```
|
|
33102
|
+
*/
|
|
33103
|
+
function isAssetsStamp(file) {
|
|
33104
|
+
return file.replaceAll("\\", "/").endsWith(ASSETS_STAMP);
|
|
33105
|
+
}
|
|
33106
|
+
/**
|
|
33107
|
+
* Starts the swaps of one module: its exports for the event, and nothing swapped yet.
|
|
33108
|
+
*
|
|
33109
|
+
* @param module - The exports of the saved module.
|
|
33110
|
+
* @returns The swaps, every list empty.
|
|
33111
|
+
* @example
|
|
33112
|
+
* ```ts
|
|
33113
|
+
* noSwaps({ GAP: 8 }).components; // []
|
|
33114
|
+
* ```
|
|
33115
|
+
*/
|
|
33116
|
+
function noSwaps(module) {
|
|
33117
|
+
return {
|
|
33118
|
+
module,
|
|
33119
|
+
components: [],
|
|
33120
|
+
projections: [],
|
|
33121
|
+
animations: [],
|
|
33122
|
+
emitters: [],
|
|
33123
|
+
strings: [],
|
|
33124
|
+
textStyles: []
|
|
33125
|
+
};
|
|
33126
|
+
}
|
|
33127
|
+
/**
|
|
32005
33128
|
* Files one export under the kind it is, in the order component, projection, animation, emitter,
|
|
32006
33129
|
* strings, text styles. The `default` export of a strings file counts as strings.
|
|
32007
33130
|
*
|
|
@@ -32041,15 +33164,7 @@ function sortExports(next, file) {
|
|
|
32041
33164
|
const exports = Object.entries(next);
|
|
32042
33165
|
if (exports.length === 0) return { reason: "no exports" };
|
|
32043
33166
|
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
|
-
};
|
|
33167
|
+
const swaps = noSwaps(Object.fromEntries(exports));
|
|
32053
33168
|
for (const entry of exports) {
|
|
32054
33169
|
const reason = fileExport(swaps, entry, locale);
|
|
32055
33170
|
if (reason !== void 0) return { reason };
|
|
@@ -32137,10 +33252,28 @@ function summaryOf(file, swaps) {
|
|
|
32137
33252
|
};
|
|
32138
33253
|
}
|
|
32139
33254
|
/**
|
|
33255
|
+
* Hands the stamp of the keys watch on: the event with the exports of the module, and the log
|
|
33256
|
+
* line the editor listens to, every list empty. Nothing is sorted, replaced or repainted: the
|
|
33257
|
+
* stamp names asset files, and `assets` hooks the event to replace them. A listener cannot refuse
|
|
33258
|
+
* by throwing, the bus catches what a hook throws, so `assets` reloads the page itself.
|
|
33259
|
+
*
|
|
33260
|
+
* @param ctx - Domain context of the ui plugin.
|
|
33261
|
+
* @param file - The path of the stamp module.
|
|
33262
|
+
* @param next - Its new module namespace.
|
|
33263
|
+
*/
|
|
33264
|
+
function forwardStamp(ctx, file, next) {
|
|
33265
|
+
const swaps = noSwaps(Object.fromEntries(Object.entries(next)));
|
|
33266
|
+
ctx.emit("ui:hot-swap", {
|
|
33267
|
+
file,
|
|
33268
|
+
module: swaps.module
|
|
33269
|
+
});
|
|
33270
|
+
ctx.log.info("ui:hot-swap", summaryOf(file, swaps));
|
|
33271
|
+
}
|
|
33272
|
+
/**
|
|
32140
33273
|
* Builds the handler of one app: sort the exports, run the replaces that may throw, write the
|
|
32141
33274
|
* components and the text styles, and repaint every view. The repaint runs for every module that
|
|
32142
33275
|
* is not refused, so a module of styles only, whose bindings Bun already patched, shows on the
|
|
32143
|
-
* next frame too.
|
|
33276
|
+
* next frame too. The stamp of the keys watch is the one module that is only forwarded.
|
|
32144
33277
|
*
|
|
32145
33278
|
* @param ctx - Domain context of the ui plugin.
|
|
32146
33279
|
* @param jsx - The jsx module, which owns the component registry and the roots.
|
|
@@ -32148,6 +33281,10 @@ function summaryOf(file, swaps) {
|
|
|
32148
33281
|
*/
|
|
32149
33282
|
function createSwap(ctx, jsx) {
|
|
32150
33283
|
return (next, file) => {
|
|
33284
|
+
if (isAssetsStamp(file) && hasMembers(next)) {
|
|
33285
|
+
forwardStamp(ctx, file, next);
|
|
33286
|
+
return;
|
|
33287
|
+
}
|
|
32151
33288
|
const sorted = sortExports(next, file);
|
|
32152
33289
|
if ("reason" in sorted) refuse(ctx, file, sorted.reason);
|
|
32153
33290
|
replaceRefusable(ctx, file, sorted);
|