narraleaf-react 0.45.1 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/game/nlcore/common/core.d.ts +1 -0
- package/dist/game/nlcore/common/player.d.ts +1 -0
- package/dist/game/nlcore/gameTypes.d.ts +22 -1
- package/dist/game/nlcore/preload/types.d.ts +213 -0
- package/dist/game/player/elements/preload/defaultStrategy.d.ts +16 -0
- package/dist/game/player/gameState.d.ts +51 -0
- package/dist/game/player/lib/ImageCacheManager.d.ts +53 -1
- package/dist/game/player/lib/VideoWarmQueue.d.ts +78 -0
- package/dist/main.js +44 -44
- package/package.json +1 -1
|
@@ -6,4 +6,5 @@ export * from "./types";
|
|
|
6
6
|
export * from "./position";
|
|
7
7
|
export * from "./transition";
|
|
8
8
|
export * from "./interface";
|
|
9
|
+
export type { PreloadAcquisition, PreloadBand, PreloadEntry, PreloadMoment, PreloadPlan, PreloadResource, PreloadResourceType, PreloadStrategy, } from "../preload/types";
|
|
9
10
|
export { i, c, b };
|
|
@@ -6,4 +6,5 @@ import { usePathname, useParams, useQueryParams } from "../../player/lib/PageRou
|
|
|
6
6
|
export * from "../../player/type";
|
|
7
7
|
export * from "../../player/libElements";
|
|
8
8
|
export type { ImageCacheManager, ImageCacheStats } from "../../player/lib/ImageCacheManager";
|
|
9
|
+
export { createDefaultPreloadStrategy } from "../../player/elements/preload/defaultStrategy";
|
|
9
10
|
export { GameProviders, Player, useGame, useRouter, usePathname, useParams, useQueryParams, };
|
|
@@ -11,6 +11,7 @@ import type { GameElementHistory } from "./action/gameHistory";
|
|
|
11
11
|
import { MenuComponent, NotificationComponent, NvlDialogComponent, SayComponent } from "./common/player";
|
|
12
12
|
import { LiveGameEventToken } from "./types";
|
|
13
13
|
import type { AudioBusDeclaration } from "./game/audioBus";
|
|
14
|
+
import type { PreloadStrategy } from "./preload/types";
|
|
14
15
|
/**
|
|
15
16
|
* Current save format version.
|
|
16
17
|
*
|
|
@@ -168,10 +169,30 @@ export type GameConfig = {
|
|
|
168
169
|
* @default 50
|
|
169
170
|
*/
|
|
170
171
|
ratioUpdateInterval: number;
|
|
172
|
+
/**
|
|
173
|
+
* Who decides what the player warms, when, and where the bytes come from.
|
|
174
|
+
*
|
|
175
|
+
* Unset, the player uses its built-in strategy, which is the behaviour it has always had: walk
|
|
176
|
+
* the action tree of the scene about to paint and of the scenes reachable from it, split the
|
|
177
|
+
* result into a first frame, the scene's registered set and a look-ahead, and fetch each one
|
|
178
|
+
* into an object url with an off-screen decode. Every `preload*` field below steers that
|
|
179
|
+
* strategy and nothing else.
|
|
180
|
+
*
|
|
181
|
+
* Set it and the player stops guessing. It asks this object what should be warm at each moment
|
|
182
|
+
* in the story and does exactly that, and if the object also supplies
|
|
183
|
+
* {@link PreloadStrategy.acquire} it stops fetching too - which is what lets a host whose assets
|
|
184
|
+
* are already on local disk hand back the url it was given and keep no second copy in memory.
|
|
185
|
+
* The cache, its budgets and the pins are still the player's; the plan is not.
|
|
186
|
+
*
|
|
187
|
+
* See {@link PreloadStrategy}.
|
|
188
|
+
*/
|
|
189
|
+
preload?: PreloadStrategy;
|
|
171
190
|
/**
|
|
172
191
|
* The game will preload the image with this delay between each preload task
|
|
173
192
|
*
|
|
174
|
-
* A single preload task may contain {@link GameConfig.preloadConcurrency} images
|
|
193
|
+
* A single preload task may contain {@link GameConfig.preloadConcurrency} images.
|
|
194
|
+
* Read by the built-in strategy's speculative band only; a game with its own
|
|
195
|
+
* {@link GameConfig.preload} paces that band with the same field.
|
|
175
196
|
* @default 100
|
|
176
197
|
*/
|
|
177
198
|
preloadDelay: number;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { Scene } from "../elements/scene";
|
|
2
|
+
import type { Sound } from "../elements/sound";
|
|
3
|
+
import type { Story } from "../elements/story";
|
|
4
|
+
import type { Video } from "../elements/video";
|
|
5
|
+
/**
|
|
6
|
+
* The preload seam: what the player warms, when it warms it, and where the bytes come from.
|
|
7
|
+
*
|
|
8
|
+
* ## Why this is a seam and not a policy
|
|
9
|
+
*
|
|
10
|
+
* The player used to answer three questions on its own. *What will be needed* it guessed, by
|
|
11
|
+
* walking the action tree of the scene about to paint and of every scene reachable from it. *When*
|
|
12
|
+
* followed from that walk: one pass per scene, split into a first frame, the scene's whole
|
|
13
|
+
* registered set, and a look-ahead. *How* was fixed: fetch the bytes, mint an object url, decode
|
|
14
|
+
* the image off-screen, hold the bitmap under a budget.
|
|
15
|
+
*
|
|
16
|
+
* All three answers are wrong for a host that knows more than the walk can see. A tool that
|
|
17
|
+
* compiled the story knows exactly which row shows which asset, in which order, and how big each
|
|
18
|
+
* one is; the walk only knows what a scene mentions anywhere in it, which on a real project is
|
|
19
|
+
* most of the library - so the pass fetched and decoded a chapter's artwork in order to paint one
|
|
20
|
+
* background. A host serving assets off local disk has no use for the object url either: the file
|
|
21
|
+
* is already there, and copying it into a blob costs that memory a second time.
|
|
22
|
+
*
|
|
23
|
+
* So the player now asks instead of deciding. A {@link PreloadStrategy} answers *what* and *when*
|
|
24
|
+
* as a {@link PreloadPlan}, and may answer *how* as well through
|
|
25
|
+
* {@link PreloadStrategy.acquire}. Everything the player still owns - the cache, its budgets, what
|
|
26
|
+
* a mounted element pins, and the url an element is finally pointed at - is unchanged, which is
|
|
27
|
+
* what lets a strategy replace the plan without replacing the player.
|
|
28
|
+
*
|
|
29
|
+
* A game that supplies no strategy gets the built-in one, which reproduces the walk and the tiers
|
|
30
|
+
* exactly as they were, driven by the same `preload*` fields of `GameConfig`.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* What kind of thing a preload resource names.
|
|
34
|
+
*
|
|
35
|
+
* Only images are named this way in a plan's {@link PreloadPlan.entries}: they are the resources
|
|
36
|
+
* the player fetches, caches and decodes, so a url is the whole of what it needs to know. Audio and
|
|
37
|
+
* video are named by element instead ({@link PreloadPlan.audio}, {@link PreloadPlan.video}),
|
|
38
|
+
* because what warming them means is a property of the element and not of the url. `video` survives
|
|
39
|
+
* here for {@link PreloadStrategy.onMissing} and {@link PreloadStrategy.acquire}, which speak about
|
|
40
|
+
* a resource rather than about a plan.
|
|
41
|
+
*/
|
|
42
|
+
export type PreloadResourceType = "image" | "video";
|
|
43
|
+
/** One thing a plan can ask for, named by the url the stage will show. */
|
|
44
|
+
export type PreloadResource = {
|
|
45
|
+
type: PreloadResourceType;
|
|
46
|
+
/**
|
|
47
|
+
* The url the stage points an element at, which is also the key the cache stores it under.
|
|
48
|
+
*
|
|
49
|
+
* A strategy that rewrites urls - a host serving through its own protocol, say - must name the
|
|
50
|
+
* resource by the url the *stage* will use and do the rewriting inside
|
|
51
|
+
* {@link PreloadStrategy.acquire}. The two are the same string for every ordinary game.
|
|
52
|
+
*/
|
|
53
|
+
src: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* How urgently a resource is wanted. One axis, three points, and only the first of them blocks.
|
|
57
|
+
*
|
|
58
|
+
* - `gate` - the frame is not allowed to paint until this has landed. That is the loading screen's
|
|
59
|
+
* whole meaning, so a plan that puts a chapter in this band is a plan that opens late.
|
|
60
|
+
* - `soon` - start now, at full speed, but nothing waits on it: what the player is a click away
|
|
61
|
+
* from needing.
|
|
62
|
+
* - `idle` - speculative. Paced by `GameConfig.preloadDelay`, run after the other two, and
|
|
63
|
+
* abandoned without ceremony when the moment is superseded.
|
|
64
|
+
*/
|
|
65
|
+
export type PreloadBand = "gate" | "soon" | "idle";
|
|
66
|
+
export type PreloadEntry = {
|
|
67
|
+
/**
|
|
68
|
+
* Always an image.
|
|
69
|
+
*
|
|
70
|
+
* Video used to be nameable here, and warming it meant reading the bytes once and dropping
|
|
71
|
+
* them, in the hope that whatever served them kept a copy. That hope is unfounded on the hosts
|
|
72
|
+
* that matter - a game serving assets through its own protocol has no cache to warm - so it
|
|
73
|
+
* reported warming that had not happened. {@link PreloadPlan.video} is the answer that works.
|
|
74
|
+
*/
|
|
75
|
+
type: "image";
|
|
76
|
+
src: string;
|
|
77
|
+
band: PreloadBand;
|
|
78
|
+
/**
|
|
79
|
+
* Whether to decode the image off-screen before anything shows it, and hold the bitmap.
|
|
80
|
+
*
|
|
81
|
+
* A decode is what lets an image paint on the frame it is revealed on rather than a frame or
|
|
82
|
+
* two later, and it is the expensive half of warming one: measured over a real library,
|
|
83
|
+
* fetching every image took 473 ms and fetching *and* decoding them took 2,140 ms, with each
|
|
84
|
+
* retained bitmap costing width x height x 4 bytes for as long as it is held. So it is worth
|
|
85
|
+
* paying for what is about to be revealed and wasteful for what merely might be.
|
|
86
|
+
*
|
|
87
|
+
* Defaults to true on the `gate` and `soon` bands and false on `idle`.
|
|
88
|
+
*/
|
|
89
|
+
decode?: boolean;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* What should be warm at one moment in the story, and what may be forgotten.
|
|
93
|
+
*
|
|
94
|
+
* A plan is complete: it replaces the previous one rather than adding to it. That is what makes
|
|
95
|
+
* {@link PreloadPlan.keep} meaningful - a scene the story has left keeps nothing, and its artwork
|
|
96
|
+
* is released as soon as no element is still showing it.
|
|
97
|
+
*/
|
|
98
|
+
export type PreloadPlan = {
|
|
99
|
+
/** Every image and video this moment wants, in the order each band should warm them. */
|
|
100
|
+
readonly entries: readonly PreloadEntry[];
|
|
101
|
+
/**
|
|
102
|
+
* Sounds the audio cache should hold for this moment, and only these.
|
|
103
|
+
*
|
|
104
|
+
* Separate from {@link PreloadPlan.entries}, and named by element rather than by url, because
|
|
105
|
+
* audio is warmed by a different cache with a different budget, and whether a clip is decoded
|
|
106
|
+
* into memory or streamed as it plays is a property of the sound rather than of its url. It is
|
|
107
|
+
* also never gated on: the audio context stays locked until the page has been interacted with,
|
|
108
|
+
* so a loading screen that waited for a clip could wait for ever.
|
|
109
|
+
*/
|
|
110
|
+
readonly audio?: readonly Sound[];
|
|
111
|
+
/**
|
|
112
|
+
* Clips this moment wants buffering, nearest first, and only these.
|
|
113
|
+
*
|
|
114
|
+
* Named by element for the same reason audio is, only more so: warming a video means putting
|
|
115
|
+
* its element in the document and letting the browser buffer into it, and the element that
|
|
116
|
+
* buffered has to be the one that plays or nothing was gained. There is no video cache in the
|
|
117
|
+
* player, and there is no url-shaped way to fill one.
|
|
118
|
+
*
|
|
119
|
+
* The order is a statement of what is coming first. The player admits the clips one at a time
|
|
120
|
+
* and starts the next when the current one reports it can play, so how many are really being
|
|
121
|
+
* fetched at once follows the connection rather than the length of this list - which is why
|
|
122
|
+
* there is no setting for it and no number for a host to choose. A clip the story has already
|
|
123
|
+
* declared is left alone: it is on the stage on the author's own instruction.
|
|
124
|
+
*
|
|
125
|
+
* Never gated on. A clip can take arbitrarily long to buffer, and a loading screen that waited
|
|
126
|
+
* for one would be a download bar wearing a story's clothes. Omit the field to leave the warm
|
|
127
|
+
* set as it is; an empty array releases it.
|
|
128
|
+
*/
|
|
129
|
+
readonly video?: readonly Video[];
|
|
130
|
+
/**
|
|
131
|
+
* Every url the image cache may keep, which is normally the plan's own entries.
|
|
132
|
+
*
|
|
133
|
+
* Anything outside this set is released at once if nothing is showing it, and the moment its
|
|
134
|
+
* last element unmounts otherwise. Omit it to leave what the cache holds alone - which is what
|
|
135
|
+
* a plan that only adds to a scene's warm set wants.
|
|
136
|
+
*/
|
|
137
|
+
readonly keep?: readonly string[];
|
|
138
|
+
/**
|
|
139
|
+
* Urls no budget may release, whatever else happens - normally the opening frame.
|
|
140
|
+
*
|
|
141
|
+
* Whatever a mounted element is showing is protected separately and does not need naming here.
|
|
142
|
+
*/
|
|
143
|
+
readonly pin?: readonly string[];
|
|
144
|
+
};
|
|
145
|
+
/** Why the player is asking. A strategy may answer only the moments it cares about. */
|
|
146
|
+
export type PreloadMoment = {
|
|
147
|
+
/** A scene is about to paint, or has just been entered. */
|
|
148
|
+
kind: "scene";
|
|
149
|
+
scene: Scene;
|
|
150
|
+
story: Story | null;
|
|
151
|
+
} | {
|
|
152
|
+
/**
|
|
153
|
+
* The story advanced. Sent for every action, so a strategy that plans row by row answers here
|
|
154
|
+
* and one that plans per scene returns null.
|
|
155
|
+
*/
|
|
156
|
+
kind: "advance";
|
|
157
|
+
actionId: string | null;
|
|
158
|
+
scene: Scene | null;
|
|
159
|
+
story: Story | null;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Where the bytes for one resource came from, and what keeping them costs.
|
|
163
|
+
*
|
|
164
|
+
* Returned by {@link PreloadStrategy.acquire}. A host that serves assets from local disk should
|
|
165
|
+
* hand back the url it was given with `bytes: 0` and no `release`: the browser then fetches and
|
|
166
|
+
* caches the file once, the way it would for any other url on the page, and the player holds no
|
|
167
|
+
* second copy of it in the renderer's heap.
|
|
168
|
+
*/
|
|
169
|
+
export type PreloadAcquisition = {
|
|
170
|
+
/** The url an element should be pointed at. May be the resource's own url. */
|
|
171
|
+
url: string;
|
|
172
|
+
/**
|
|
173
|
+
* What holding this costs the player's fetched-bytes budget. Zero when the host owns the
|
|
174
|
+
* memory, which is the honest answer for a url the player copied nothing for.
|
|
175
|
+
*/
|
|
176
|
+
bytes?: number;
|
|
177
|
+
/** Called once when the player lets the entry go, for a url the host has to clean up. */
|
|
178
|
+
release?: () => void;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* The host's answer to what the player should warm, and optionally to how.
|
|
182
|
+
*
|
|
183
|
+
* Supplied as `GameConfig.preload`. Every method is asked on the player's own schedule; none may
|
|
184
|
+
* assume it is called in order, and all may be called again for the same moment after a reload.
|
|
185
|
+
*/
|
|
186
|
+
export interface PreloadStrategy {
|
|
187
|
+
/**
|
|
188
|
+
* What should be warm at this moment, or null to leave the previous plan in force.
|
|
189
|
+
*
|
|
190
|
+
* May be asynchronous: a host that has to ask another process what a scene uses answers when it
|
|
191
|
+
* knows. The player will not paint a gated frame until the plan has arrived and its `gate` band
|
|
192
|
+
* has landed, so a strategy that takes its time is a strategy that opens late - the same trade
|
|
193
|
+
* the built-in one makes, only visible.
|
|
194
|
+
*/
|
|
195
|
+
plan(moment: PreloadMoment): PreloadPlan | null | Promise<PreloadPlan | null>;
|
|
196
|
+
/**
|
|
197
|
+
* Obtain the url for one resource, replacing the player's own fetch.
|
|
198
|
+
*
|
|
199
|
+
* Omit it and the player fetches the resource itself and mints an object url, which is what it
|
|
200
|
+
* has always done. Return null to say "nothing to warm, show it directly": the player then
|
|
201
|
+
* points the element at the resource's own url and caches nothing for it.
|
|
202
|
+
*/
|
|
203
|
+
acquire?(resource: PreloadResource, signal: AbortSignal): Promise<PreloadAcquisition | null>;
|
|
204
|
+
/**
|
|
205
|
+
* Told when the stage shows something no plan named.
|
|
206
|
+
*
|
|
207
|
+
* This is the case the player used to report by asking the author, in a console warning, to
|
|
208
|
+
* register the image by hand. A host that plans from a compiled story can say something far
|
|
209
|
+
* more useful - which row shows it - so the player hands the fact over rather than guessing at
|
|
210
|
+
* the remedy. Called at most once per url.
|
|
211
|
+
*/
|
|
212
|
+
onMissing?(resource: PreloadResource): void;
|
|
213
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Game } from "../../../nlcore/game";
|
|
2
|
+
import type { PreloadStrategy } from "../../../nlcore/preload/types";
|
|
3
|
+
/**
|
|
4
|
+
* The strategy a game gets when it supplies none: the walk and the tiers exactly as they were.
|
|
5
|
+
*
|
|
6
|
+
* It exists for two reasons beyond compatibility. It is the only proof that the seam is wide
|
|
7
|
+
* enough - if the behaviour the player shipped for years cannot be expressed as a
|
|
8
|
+
* {@link PreloadPlan}, the seam is the wrong shape. And it is what a host can fall back to for the
|
|
9
|
+
* parts of a story it does not know about, since a strategy is free to call this one and merge.
|
|
10
|
+
*
|
|
11
|
+
* Everything it does is read off `GameConfig`, so the fields that used to steer the player directly
|
|
12
|
+
* now steer this: `preloadAllImages` chooses between the two passes below, `preloadGate` decides
|
|
13
|
+
* whether the scene's whole registered set blocks the first frame or only its opening background,
|
|
14
|
+
* and `maxPreloadActions` sizes the prediction window.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createDefaultPreloadStrategy(game: Game): PreloadStrategy;
|
|
@@ -26,6 +26,7 @@ import { Vfx, VfxStateRaw } from "../nlcore/elements/vfx";
|
|
|
26
26
|
import { Timelines } from "./Tasks";
|
|
27
27
|
import { Notification, NotificationManager } from "./lib/notification";
|
|
28
28
|
import type { ImageCacheManager } from "./lib/ImageCacheManager";
|
|
29
|
+
import type { PreloadResource } from "../nlcore/preload/types";
|
|
29
30
|
import { ActionHistoryManager } from "../../game/nlcore/action/actionHistory";
|
|
30
31
|
import { GameHistoryManager } from "../../game/nlcore/action/gameHistory";
|
|
31
32
|
import type { Character } from "../nlcore/elements/character";
|
|
@@ -204,6 +205,14 @@ export declare class GameState {
|
|
|
204
205
|
preloadingScene: Scene | null;
|
|
205
206
|
flushDep: number;
|
|
206
207
|
rollLock: Lock;
|
|
208
|
+
/**
|
|
209
|
+
* Clips held on the stage, hidden, because a plan said they are coming - never part of a save.
|
|
210
|
+
* See {@link VideoWarmQueue}.
|
|
211
|
+
*/
|
|
212
|
+
private readonly videoWarmQueue;
|
|
213
|
+
private videoMissingReporter;
|
|
214
|
+
/** Sources already reported as unwarmed, so one clip in a loop does not report every pass. */
|
|
215
|
+
private readonly reportedUnwarmedVideos;
|
|
207
216
|
readonly notificationMgr: NotificationManager;
|
|
208
217
|
readonly events: EventDispatcher<GameStateEvents>;
|
|
209
218
|
readonly logger: Logger;
|
|
@@ -222,8 +231,50 @@ export declare class GameState {
|
|
|
222
231
|
get deps(): number;
|
|
223
232
|
addVideo(video: Video): this;
|
|
224
233
|
removeVideo(video: Video): this;
|
|
234
|
+
/**
|
|
235
|
+
* Whether the STORY has put this clip on the stage - a declaration row, a show, a play.
|
|
236
|
+
*
|
|
237
|
+
* Deliberately not "is it in the document": a clip the warm queue is holding is mounted and
|
|
238
|
+
* buffering but nothing has shown it, and the two answers are wanted in different places. What
|
|
239
|
+
* a save records, what an undo takes back and what a second declaration skips are all this one;
|
|
240
|
+
* whether an action may talk to the element is {@link GameState.isVideoOnStage}.
|
|
241
|
+
*/
|
|
225
242
|
isVideoAdded(video: Video): boolean;
|
|
243
|
+
/** Whether the clip is in the document at all, warmed or shown - so whether it has a state to talk to. */
|
|
244
|
+
isVideoOnStage(video: Video): boolean;
|
|
245
|
+
/**
|
|
246
|
+
* Every clip the stage renders: the story's own, then whatever the warm queue is holding.
|
|
247
|
+
*
|
|
248
|
+
* The two lists are kept apart rather than merged on insert because only the first is state.
|
|
249
|
+
* A save is a list of what the story put on the stage; a warmed clip is a performance detail of
|
|
250
|
+
* this run of it, and writing it into a save would make a story edit able to invalidate a save
|
|
251
|
+
* that has nothing to do with the change (`fromData` throws on a video id it cannot find).
|
|
252
|
+
*/
|
|
226
253
|
getVideos(): Video[];
|
|
254
|
+
/**
|
|
255
|
+
* Hold exactly these clips buffering ahead of the story, in this order of preference.
|
|
256
|
+
*
|
|
257
|
+
* What a {@link PreloadPlan.video} asks for. The queue starts them one at a time and waits for
|
|
258
|
+
* each to become playable before starting the next, so how many are really being fetched at
|
|
259
|
+
* once follows the connection rather than the plan; see {@link VideoWarmQueue}.
|
|
260
|
+
*/
|
|
261
|
+
retainWarmVideos(videos: readonly Video[]): this;
|
|
262
|
+
/**
|
|
263
|
+
* Where to report a clip the story played that no plan named, or null for nowhere.
|
|
264
|
+
*
|
|
265
|
+
* The image half of this lives on the cache manager, which is the thing that would have fetched
|
|
266
|
+
* the image; a video is never fetched by the player, so the report has to come from the action
|
|
267
|
+
* that starts it.
|
|
268
|
+
*/
|
|
269
|
+
useVideoMissingReporter(reporter: ((resource: PreloadResource) => void) | null): this;
|
|
270
|
+
/**
|
|
271
|
+
* Note that a clip is about to be shown or played with nothing having warmed it.
|
|
272
|
+
*
|
|
273
|
+
* Once per source, and only when no plan named it and nothing has it on the stage: a clip the
|
|
274
|
+
* author declared a few rows earlier is warm by authorship, and reporting that would be
|
|
275
|
+
* reporting the feature working.
|
|
276
|
+
*/
|
|
277
|
+
reportUnwarmedVideo(video: Video): this;
|
|
227
278
|
addVfx(vfx: Vfx): this;
|
|
228
279
|
removeVfx(vfx: Vfx): this;
|
|
229
280
|
isVfxAdded(vfx: Vfx): boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Game } from "../../../game/nlcore/game";
|
|
2
2
|
import type { GameState } from "../gameState";
|
|
3
|
+
import type { PreloadStrategy } from "../../nlcore/preload/types";
|
|
3
4
|
export type PreloadedToken = {
|
|
4
5
|
abort: () => void;
|
|
5
6
|
onFinished: (callback: () => void) => PreloadedToken;
|
|
@@ -92,7 +93,37 @@ export declare class ImageCacheManager {
|
|
|
92
93
|
private seen;
|
|
93
94
|
private blobBytes;
|
|
94
95
|
private decodedBytes;
|
|
96
|
+
/**
|
|
97
|
+
* How the cache gets bytes for a source, when the host would rather it did not fetch them.
|
|
98
|
+
*
|
|
99
|
+
* Installed by the preloader from `GameConfig.preload`. Unset, the cache fetches the url and
|
|
100
|
+
* mints an object url for it, which is what it has always done - and which costs the renderer a
|
|
101
|
+
* second copy of every image on a host whose assets are already on local disk.
|
|
102
|
+
*/
|
|
103
|
+
private acquisition;
|
|
104
|
+
/** Where a source nothing warmed is reported, when the host wants to hear about it. */
|
|
105
|
+
private missingReporter;
|
|
106
|
+
/** Sources already reported missing, so one unpredicted image is one report and not one a frame. */
|
|
107
|
+
private reportedMissing;
|
|
95
108
|
constructor(game: Game);
|
|
109
|
+
/**
|
|
110
|
+
* Install the host's way of obtaining bytes, or clear it.
|
|
111
|
+
*
|
|
112
|
+
* Set once, from the preloader, before anything is warmed. Entries already in the cache keep
|
|
113
|
+
* whatever url they were built with; the cache never re-acquires something it holds.
|
|
114
|
+
*/
|
|
115
|
+
useAcquisition(acquire: PreloadStrategy["acquire"] | null): this;
|
|
116
|
+
/** Install the host's ear for sources nothing warmed, or clear it. */
|
|
117
|
+
useMissingReporter(onMissing: PreloadStrategy["onMissing"] | null): this;
|
|
118
|
+
/**
|
|
119
|
+
* Say that the stage is showing `src` and no plan named it. Answers whether the host took it.
|
|
120
|
+
*
|
|
121
|
+
* The player's own answer to this was a console warning telling the author to register the
|
|
122
|
+
* image by hand, which is only useful to someone who writes the story in TypeScript. A host that
|
|
123
|
+
* planned from a compiled story can name the row instead, so it gets first refusal and the
|
|
124
|
+
* warning stays for the games that have no host to ask.
|
|
125
|
+
*/
|
|
126
|
+
reportMissing(src: string): boolean;
|
|
96
127
|
has(name: string): boolean;
|
|
97
128
|
/**
|
|
98
129
|
* Whether this source has ever been through the cache, whether or not it still is.
|
|
@@ -135,9 +166,14 @@ export declare class ImageCacheManager {
|
|
|
135
166
|
* {@link GameConfig.decodedImageBudgetBytes}, until this source leaves the cache. Use it for the
|
|
136
167
|
* assets that are about to be revealed; leave it off for speculative look-ahead preloading,
|
|
137
168
|
* whose bitmaps would otherwise pile up in memory.
|
|
169
|
+
* @param options.decode run the off-screen decode at all. Defaults to true. Off, the bytes are
|
|
170
|
+
* obtained and nothing else: measured over a real library, that is the difference between 473
|
|
171
|
+
* and 2,140 milliseconds, and it is the right trade for anything the plan does not expect to be
|
|
172
|
+
* revealed soon. `retainDecoded` implies a decode and overrides this.
|
|
138
173
|
*/
|
|
139
174
|
preload(gameState: GameState, url: string, options?: {
|
|
140
175
|
retainDecoded?: boolean;
|
|
176
|
+
decode?: boolean;
|
|
141
177
|
}): PreloadedToken;
|
|
142
178
|
abortAll(): void;
|
|
143
179
|
abort(src: string): void;
|
|
@@ -191,7 +227,23 @@ export declare class ImageCacheManager {
|
|
|
191
227
|
* the budget is a limit, not a preference, and what is on stage is protected by the pins.
|
|
192
228
|
*/
|
|
193
229
|
private enforce;
|
|
194
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Get the bytes for one source and, unless told not to, decode them off-screen.
|
|
232
|
+
*
|
|
233
|
+
* The acquisition step is the host's when one is installed: it may hand back the url unchanged
|
|
234
|
+
* and own the memory itself, which is what a host serving local files should do. Otherwise the
|
|
235
|
+
* cache fetches and mints an object url, and is the thing that has to revoke it.
|
|
236
|
+
*/
|
|
237
|
+
private acquireAndDecode;
|
|
238
|
+
/** The bytes for one source, from the host when it has an opinion and by fetching otherwise. */
|
|
239
|
+
private acquire;
|
|
240
|
+
/**
|
|
241
|
+
* Give a url back to whoever owns it: the host that supplied it, or the browser that minted it.
|
|
242
|
+
*
|
|
243
|
+
* One place, because the two are indistinguishable to every caller and getting it wrong leaks a
|
|
244
|
+
* whole image - an object url pins its blob for the lifetime of the document.
|
|
245
|
+
*/
|
|
246
|
+
private handBack;
|
|
195
247
|
/** A cached entry's bitmap, wanted again after the budget let it go or a look-ahead skipped it. */
|
|
196
248
|
private decodeAgain;
|
|
197
249
|
private runTask;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Video } from "../../nlcore/elements/video";
|
|
2
|
+
export type VideoWarmQueueOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Whether the story itself has put this clip on the stage.
|
|
5
|
+
*
|
|
6
|
+
* Declared clips are the author's own instruction (`/video`, or a `Video.preload()` call) and
|
|
7
|
+
* are never queued: they are already buffering, they are not subject to the ceiling, and
|
|
8
|
+
* releasing one would take away something the story asked for.
|
|
9
|
+
*/
|
|
10
|
+
isDeclared(video: Video): boolean;
|
|
11
|
+
/** Ask the player to re-render the stage, because what is mounted has changed. */
|
|
12
|
+
onChange(): void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Which clips are held on the stage buffering, and in what order they are started.
|
|
16
|
+
*
|
|
17
|
+
* ## Why a queue and not a number
|
|
18
|
+
*
|
|
19
|
+
* Warming a video is not warming an image. An image is bytes plus a decode, and the player can hold
|
|
20
|
+
* both in a cache under a budget it can measure. A video is a `<video>` element that is in the
|
|
21
|
+
* document: the browser buffers into it, the buffer belongs to that element, and the element that
|
|
22
|
+
* buffered has to be the element that plays or the buffering bought nothing. So "warm this clip"
|
|
23
|
+
* means "mount it hidden, early", and the only questions left are how many and in what order.
|
|
24
|
+
*
|
|
25
|
+
* Both are answered here rather than by whoever wrote the story or configured the game. The host's
|
|
26
|
+
* plan says which clips are coming and in what order (nearest first); this admits them one at a
|
|
27
|
+
* time and starts the next when the current one reports it can play. On a fast local disk that
|
|
28
|
+
* empties the queue almost at once; on a slow connection it keeps a single read running instead of
|
|
29
|
+
* five that finish together and too late. Nobody has to know what number to write, which is the
|
|
30
|
+
* point - it is not a number an author could be expected to get right, and the browser is telling
|
|
31
|
+
* us the answer anyway.
|
|
32
|
+
*/
|
|
33
|
+
export declare class VideoWarmQueue {
|
|
34
|
+
private readonly options;
|
|
35
|
+
/** Everything the current plan named, declared clips included - what {@link isPlanned} answers. */
|
|
36
|
+
private planned;
|
|
37
|
+
/** What this queue may mount, in plan order, minus anything the story declared itself. */
|
|
38
|
+
private desired;
|
|
39
|
+
/** Mounted by this queue, hidden and buffering. A subset of {@link desired}, in admission order. */
|
|
40
|
+
private admitted;
|
|
41
|
+
/** The clip whose "can play" the queue is waiting for before it starts another. */
|
|
42
|
+
private pending;
|
|
43
|
+
private pendingTimer;
|
|
44
|
+
constructor(options: VideoWarmQueueOptions);
|
|
45
|
+
/**
|
|
46
|
+
* Take a plan's clips as the complete set this moment wants warm.
|
|
47
|
+
*
|
|
48
|
+
* Total replacement, like every other part of a plan: a clip the new plan does not name is
|
|
49
|
+
* released as soon as this returns, because the scene that wanted it is behind the reader. What
|
|
50
|
+
* the story declared is untouched - releasing that is the story's business, not the plan's.
|
|
51
|
+
*/
|
|
52
|
+
retain(next: readonly Video[]): void;
|
|
53
|
+
/**
|
|
54
|
+
* A clip is playable, or has failed trying - either way the queue stops waiting for it.
|
|
55
|
+
*
|
|
56
|
+
* Fed from the player's exposed-state mount, which the video component performs on `canplay`
|
|
57
|
+
* and, for a source that will not load, on `error`. Anything that is not the clip being waited
|
|
58
|
+
* for is ignored, so this can be wired to every state mount there is.
|
|
59
|
+
*/
|
|
60
|
+
noteReady(video: unknown): void;
|
|
61
|
+
/**
|
|
62
|
+
* Stop holding a clip the story has taken over.
|
|
63
|
+
*
|
|
64
|
+
* Called when a row declares, shows or plays something this queue had mounted. The element does
|
|
65
|
+
* not move and does not reload - it is the same element, now on the stage for a better reason -
|
|
66
|
+
* so this only stops counting it against the ceiling and frees the queue to start another.
|
|
67
|
+
*/
|
|
68
|
+
forget(video: Video): void;
|
|
69
|
+
/** Clips this queue is holding on the stage, hidden. */
|
|
70
|
+
getAdmitted(): readonly Video[];
|
|
71
|
+
/** Whether the current plan named this clip at all, declared or not. */
|
|
72
|
+
isPlanned(video: Video): boolean;
|
|
73
|
+
/** Drop everything, for a player being torn down or reset. */
|
|
74
|
+
clear(): void;
|
|
75
|
+
/** Start the next clip if there is room and nothing is being waited on. Reports whether it did. */
|
|
76
|
+
private admit;
|
|
77
|
+
private clearPending;
|
|
78
|
+
}
|