@kubohiroya/turbowarp-asset-cache 0.1.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/LICENSE +373 -0
- package/README.md +118 -0
- package/dist/asset-cache.js +3388 -0
- package/dist/composition.js +8333 -0
- package/dist/extension-manifest.json +420 -0
- package/dist/types/animation.d.ts +41 -0
- package/dist/types/asset-cache-error.d.ts +28 -0
- package/dist/types/audio-voice.d.ts +9 -0
- package/dist/types/binary-bundle-store.d.ts +93 -0
- package/dist/types/binary-object-store.d.ts +56 -0
- package/dist/types/composition.d.ts +111 -0
- package/dist/types/dom-image-resource.d.ts +18 -0
- package/dist/types/extension.d.ts +257 -0
- package/dist/types/feature-flags.d.ts +10 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/named-body-provider.d.ts +12 -0
- package/dist/types/opfs-binary-bundle-store.d.ts +19 -0
- package/dist/types/server-manifest.d.ts +16 -0
- package/dist/types/session-binary-backing.d.ts +59 -0
- package/dist/types/text-style.d.ts +24 -0
- package/dist/types/verified-cache-catalog.d.ts +57 -0
- package/dist/types/verified-remote-cache.d.ts +106 -0
- package/package.json +85 -0
|
@@ -0,0 +1,3388 @@
|
|
|
1
|
+
// Name: Asset Cache
|
|
2
|
+
// ID: kubohiroyaassetcache
|
|
3
|
+
// Description: Register, cache, display, and play image, audio, and runtime text assets in TurboWarp.
|
|
4
|
+
// By: Hiroya Kubo
|
|
5
|
+
// License: MPL-2.0
|
|
6
|
+
|
|
7
|
+
(function (Scratch) {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
var block_definitions_default = {
|
|
11
|
+
extensionName: "Asset Cache",
|
|
12
|
+
blocks: [
|
|
13
|
+
{
|
|
14
|
+
"opcode": "registerAsset",
|
|
15
|
+
"blockType": "COMMAND",
|
|
16
|
+
"text": "register resource [RESOURCE_ID] as asset [NAME]",
|
|
17
|
+
"description": "Registers an external URL, cached asset, sprite costume, stage backdrop, project sound, or runtime text variable under one asset name.",
|
|
18
|
+
"arguments": {
|
|
19
|
+
"RESOURCE_ID": {
|
|
20
|
+
"type": "STRING",
|
|
21
|
+
"defaultValue": "https://example.com/asset.png"
|
|
22
|
+
},
|
|
23
|
+
"NAME": {
|
|
24
|
+
"type": "STRING",
|
|
25
|
+
"defaultValue": "asset1"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"opcode": "assetErrorType",
|
|
31
|
+
"blockType": "REPORTER",
|
|
32
|
+
"text": "asset registration error type",
|
|
33
|
+
"description": "Returns the stable error code for the most recent asset registration failure, or an empty string when the latest registration succeeded.",
|
|
34
|
+
"arguments": {}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"opcode": "assetErrorLabel",
|
|
38
|
+
"blockType": "REPORTER",
|
|
39
|
+
"text": "asset registration error label",
|
|
40
|
+
"description": "Returns the relevant asset, resource, or actor name for the most recent registration failure, or an empty string when the latest registration succeeded.",
|
|
41
|
+
"arguments": {}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"opcode": "loadAsset",
|
|
45
|
+
"blockType": "COMMAND",
|
|
46
|
+
"text": "load asset from URL [URL] or cache as [NAME]",
|
|
47
|
+
"description": "Legacy compatibility block. Loads an external image or audio asset from the supplied URL, or from IndexedDB when the URL is empty.",
|
|
48
|
+
"arguments": {
|
|
49
|
+
"URL": {
|
|
50
|
+
"type": "STRING",
|
|
51
|
+
"defaultValue": "https://example.com/asset.png"
|
|
52
|
+
},
|
|
53
|
+
"NAME": {
|
|
54
|
+
"type": "STRING",
|
|
55
|
+
"defaultValue": "asset1"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"hideFromPalette": true
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"opcode": "deleteMemoryAsset",
|
|
62
|
+
"blockType": "COMMAND",
|
|
63
|
+
"text": "delete asset [NAME] from memory",
|
|
64
|
+
"description": "Unregisters one asset. Owned external renderer skins are released; project costumes, sounds, and runtime variables are left unchanged.",
|
|
65
|
+
"arguments": { "NAME": {
|
|
66
|
+
"type": "STRING",
|
|
67
|
+
"defaultValue": "asset1"
|
|
68
|
+
} }
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"opcode": "deleteAllMemoryAssets",
|
|
72
|
+
"blockType": "COMMAND",
|
|
73
|
+
"text": "delete all assets from memory",
|
|
74
|
+
"description": "Unregisters all assets, releases owned external renderer skins, stops actor animations, and stops tracked external audio playback.",
|
|
75
|
+
"arguments": {}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"opcode": "deleteCachedAsset",
|
|
79
|
+
"blockType": "COMMAND",
|
|
80
|
+
"text": "delete asset [NAME] from cache",
|
|
81
|
+
"description": "Deletes one named external asset from the IndexedDB cache.",
|
|
82
|
+
"arguments": { "NAME": {
|
|
83
|
+
"type": "STRING",
|
|
84
|
+
"defaultValue": "asset1"
|
|
85
|
+
} }
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"opcode": "deleteAllCachedAssets",
|
|
89
|
+
"blockType": "COMMAND",
|
|
90
|
+
"text": "delete all assets from cache",
|
|
91
|
+
"description": "Clears all external assets from the IndexedDB cache.",
|
|
92
|
+
"arguments": {}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"opcode": "isLoaded",
|
|
96
|
+
"blockType": "BOOLEAN",
|
|
97
|
+
"text": "asset [NAME] is loaded",
|
|
98
|
+
"description": "Returns whether the named external, project-local, or runtime text asset is currently registered.",
|
|
99
|
+
"arguments": { "NAME": {
|
|
100
|
+
"type": "STRING",
|
|
101
|
+
"defaultValue": "asset1"
|
|
102
|
+
} }
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"opcode": "setTextValue",
|
|
106
|
+
"blockType": "COMMAND",
|
|
107
|
+
"text": "set text asset [NAME] to [VALUE]",
|
|
108
|
+
"description": "Sets the runtime text value for a text asset using Asset Cache's internal namespace.",
|
|
109
|
+
"arguments": {
|
|
110
|
+
"NAME": {
|
|
111
|
+
"type": "STRING",
|
|
112
|
+
"defaultValue": "Narration"
|
|
113
|
+
},
|
|
114
|
+
"VALUE": {
|
|
115
|
+
"type": "STRING",
|
|
116
|
+
"defaultValue": "Once upon a time..."
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"opcode": "setTextStyle",
|
|
122
|
+
"blockType": "COMMAND",
|
|
123
|
+
"text": "set text asset [NAME] style [PROPERTY] to [VALUE]",
|
|
124
|
+
"description": "Sets one runtime style property for a text asset. Supported properties are animation, font, color, width, and align. An empty value restores the default.",
|
|
125
|
+
"arguments": {
|
|
126
|
+
"NAME": {
|
|
127
|
+
"type": "STRING",
|
|
128
|
+
"defaultValue": "Narration"
|
|
129
|
+
},
|
|
130
|
+
"PROPERTY": {
|
|
131
|
+
"type": "STRING",
|
|
132
|
+
"defaultValue": "font"
|
|
133
|
+
},
|
|
134
|
+
"VALUE": {
|
|
135
|
+
"type": "STRING",
|
|
136
|
+
"defaultValue": "Sans Serif"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"opcode": "setThisSpriteSkin",
|
|
142
|
+
"blockType": "COMMAND",
|
|
143
|
+
"text": "show asset [NAME] on this sprite",
|
|
144
|
+
"description": "Applies a registered image asset or displays a registered runtime text asset on the current sprite or clone.",
|
|
145
|
+
"arguments": { "NAME": {
|
|
146
|
+
"type": "STRING",
|
|
147
|
+
"defaultValue": "asset1"
|
|
148
|
+
} }
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"opcode": "setSpriteSkin",
|
|
152
|
+
"blockType": "COMMAND",
|
|
153
|
+
"text": "show asset [NAME] on [SPRITE] (compatibility)",
|
|
154
|
+
"description": "Stops any actor animation and applies a registered image asset or displays a registered runtime text asset on a named sprite. This block is retained for compatibility.",
|
|
155
|
+
"arguments": {
|
|
156
|
+
"SPRITE": {
|
|
157
|
+
"type": "STRING",
|
|
158
|
+
"defaultValue": "Sprite1"
|
|
159
|
+
},
|
|
160
|
+
"NAME": {
|
|
161
|
+
"type": "STRING",
|
|
162
|
+
"defaultValue": "asset1"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"opcode": "startActorLoop",
|
|
168
|
+
"blockType": "COMMAND",
|
|
169
|
+
"text": "loop actor [ACTOR] through assets [ASSETS] for seconds [DURATIONS]",
|
|
170
|
+
"description": "Starts or replaces a background loop. ASSETS contains registered image or audio asset names. DURATIONS must have the same number of items; each item is the interval before the next asset, including the last-to-first interval. A zero makes the next asset start together with the preceding asset. If a simultaneous group has multiple image assets, only its last image is applied. Empty ASSETS and DURATIONS stop the actor animation.",
|
|
171
|
+
"arguments": {
|
|
172
|
+
"ACTOR": {
|
|
173
|
+
"type": "STRING",
|
|
174
|
+
"defaultValue": "Sprite1"
|
|
175
|
+
},
|
|
176
|
+
"ASSETS": {
|
|
177
|
+
"type": "STRING",
|
|
178
|
+
"defaultValue": "asset1,asset2"
|
|
179
|
+
},
|
|
180
|
+
"DURATIONS": {
|
|
181
|
+
"type": "STRING",
|
|
182
|
+
"defaultValue": "0.5,0.5"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"opcode": "startActorSequence",
|
|
188
|
+
"blockType": "COMMAND",
|
|
189
|
+
"text": "play actor [ACTOR] through assets [ASSETS] for seconds [DURATIONS] once in background",
|
|
190
|
+
"description": "Starts or replaces a one-shot background sequence and returns immediately. ASSETS contains registered image or audio asset names. DURATIONS must have exactly one fewer item; each item is the interval before the next asset. A zero makes the next asset start together with the preceding asset. If a simultaneous group has multiple image assets, only its last image is applied.",
|
|
191
|
+
"arguments": {
|
|
192
|
+
"ACTOR": {
|
|
193
|
+
"type": "STRING",
|
|
194
|
+
"defaultValue": "Sprite1"
|
|
195
|
+
},
|
|
196
|
+
"ASSETS": {
|
|
197
|
+
"type": "STRING",
|
|
198
|
+
"defaultValue": "asset1,asset2"
|
|
199
|
+
},
|
|
200
|
+
"DURATIONS": {
|
|
201
|
+
"type": "STRING",
|
|
202
|
+
"defaultValue": "0.5"
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"opcode": "stopActorAnimation",
|
|
208
|
+
"blockType": "COMMAND",
|
|
209
|
+
"text": "stop animation of actor [ACTOR]",
|
|
210
|
+
"description": "Stops the actor's current loop or sequence and leaves the currently displayed skin unchanged.",
|
|
211
|
+
"arguments": { "ACTOR": {
|
|
212
|
+
"type": "STRING",
|
|
213
|
+
"defaultValue": "Sprite1"
|
|
214
|
+
} }
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"opcode": "finishAllActorSequences",
|
|
218
|
+
"blockType": "COMMAND",
|
|
219
|
+
"text": "finish all actor sequences",
|
|
220
|
+
"description": "Finishes every one-shot actor sequence on its final image without stopping loops.",
|
|
221
|
+
"arguments": {}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"opcode": "setStageSkin",
|
|
225
|
+
"blockType": "COMMAND",
|
|
226
|
+
"text": "set stage backdrop to asset [NAME]",
|
|
227
|
+
"description": "Applies a registered external image, sprite costume, or stage backdrop to the stage drawable.",
|
|
228
|
+
"arguments": { "NAME": {
|
|
229
|
+
"type": "STRING",
|
|
230
|
+
"defaultValue": "backdrop1"
|
|
231
|
+
} }
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"opcode": "playSound",
|
|
235
|
+
"blockType": "COMMAND",
|
|
236
|
+
"text": "play asset [NAME] as sound",
|
|
237
|
+
"description": "Starts playback of a registered external audio asset or project sound without waiting for completion.",
|
|
238
|
+
"arguments": { "NAME": {
|
|
239
|
+
"type": "STRING",
|
|
240
|
+
"defaultValue": "sound1"
|
|
241
|
+
} }
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"opcode": "playSoundUntilDone",
|
|
245
|
+
"blockType": "COMMAND",
|
|
246
|
+
"text": "play asset [NAME] as sound until done",
|
|
247
|
+
"description": "Plays a registered external audio asset or project sound and waits until playback ends or fails.",
|
|
248
|
+
"arguments": { "NAME": {
|
|
249
|
+
"type": "STRING",
|
|
250
|
+
"defaultValue": "sound1"
|
|
251
|
+
} }
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"opcode": "stopSound",
|
|
255
|
+
"blockType": "COMMAND",
|
|
256
|
+
"text": "stop asset sound [NAME]",
|
|
257
|
+
"description": "Stops every active playback of one registered external or project sound asset without stopping other sounds.",
|
|
258
|
+
"arguments": { "NAME": {
|
|
259
|
+
"type": "STRING",
|
|
260
|
+
"defaultValue": "sound1"
|
|
261
|
+
} }
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"opcode": "stopAllSounds",
|
|
265
|
+
"blockType": "COMMAND",
|
|
266
|
+
"text": "stop all asset sounds",
|
|
267
|
+
"description": "Stops all external and project sounds currently tracked by Asset Cache.",
|
|
268
|
+
"arguments": {}
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"opcode": "getAssetMimeType",
|
|
272
|
+
"blockType": "REPORTER",
|
|
273
|
+
"text": "MIME type of asset [NAME]",
|
|
274
|
+
"description": "Returns the normalized MIME type of a registered external, project-local, or runtime text asset.",
|
|
275
|
+
"arguments": { "NAME": {
|
|
276
|
+
"type": "STRING",
|
|
277
|
+
"defaultValue": "asset1"
|
|
278
|
+
} }
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"opcode": "getVersion",
|
|
282
|
+
"blockType": "REPORTER",
|
|
283
|
+
"text": "Asset Cache version",
|
|
284
|
+
"description": "Returns the Asset Cache implementation version.",
|
|
285
|
+
"arguments": {}
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
};
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region node_modules/.pnpm/@kubohiroya+turbowarp-named-data@0.1.0/node_modules/@kubohiroya/turbowarp-named-data/dist/composition.js
|
|
291
|
+
var NAMED_DATA_REGISTRY_SYMBOL_KEY = "@kubohiroya/turbowarp-named-data/registry";
|
|
292
|
+
var NAMED_DATA_REGISTRY_SYMBOL = Symbol.for(NAMED_DATA_REGISTRY_SYMBOL_KEY);
|
|
293
|
+
var NAMED_DATA_NAMESPACE_PATTERN = /^[a-z][a-z0-9.-]{0,63}$/u;
|
|
294
|
+
function isNamedDataNamespace(value) {
|
|
295
|
+
return typeof value === "string" && NAMED_DATA_NAMESPACE_PATTERN.test(value);
|
|
296
|
+
}
|
|
297
|
+
var NAMED_DATA_KINDS = [
|
|
298
|
+
"structured",
|
|
299
|
+
"document",
|
|
300
|
+
"binary",
|
|
301
|
+
"asset"
|
|
302
|
+
];
|
|
303
|
+
var NAMED_DATA_SCOPES = ["target", "project"];
|
|
304
|
+
var NAMED_DATA_REPRESENTATIONS = [
|
|
305
|
+
"json",
|
|
306
|
+
"yaml",
|
|
307
|
+
"html",
|
|
308
|
+
"markdown",
|
|
309
|
+
"raw"
|
|
310
|
+
];
|
|
311
|
+
var NAMED_DATA_ERROR_CODES = [
|
|
312
|
+
"NAMED_DATA_INVALID_REF",
|
|
313
|
+
"NAMED_DATA_INVALID_REGISTRY",
|
|
314
|
+
"NAMED_DATA_PROVIDER_CONFLICT",
|
|
315
|
+
"NAMED_DATA_PROVIDER_NOT_FOUND",
|
|
316
|
+
"NAMED_DATA_NOT_FOUND",
|
|
317
|
+
"NAMED_DATA_KIND_MISMATCH",
|
|
318
|
+
"NAMED_DATA_SCOPE_MISMATCH",
|
|
319
|
+
"NAMED_DATA_REPRESENTATION_UNSUPPORTED",
|
|
320
|
+
"NAMED_DATA_INVALID_METADATA",
|
|
321
|
+
"NAMED_DATA_BODY_TOO_LARGE",
|
|
322
|
+
"NAMED_DATA_ABORTED",
|
|
323
|
+
"NAMED_DATA_PROVIDER_RELEASED"
|
|
324
|
+
];
|
|
325
|
+
var NamedDataError = class extends Error {
|
|
326
|
+
constructor(code, message, options) {
|
|
327
|
+
super(`${code}: ${message}`, options);
|
|
328
|
+
this.code = code;
|
|
329
|
+
this.name = "NamedDataError";
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
var LIFECYCLE_SYMBOL = Symbol.for("@kubohiroya/turbowarp-named-data/lifecycle");
|
|
333
|
+
var errorCodes = new Set(NAMED_DATA_ERROR_CODES);
|
|
334
|
+
var NamedDataRegistry = class {
|
|
335
|
+
constructor() {
|
|
336
|
+
this.providers = /* @__PURE__ */ new Map();
|
|
337
|
+
this.handles = /* @__PURE__ */ new Set();
|
|
338
|
+
}
|
|
339
|
+
registerProvider(provider, options = {}) {
|
|
340
|
+
requireNamespace(provider.namespace);
|
|
341
|
+
if (!NAMED_DATA_KINDS.includes(provider.kind)) throw new NamedDataError("NAMED_DATA_INVALID_REF", `Unknown provider kind: ${provider.kind}`);
|
|
342
|
+
const key = providerKey(provider.namespace, provider.kind);
|
|
343
|
+
if (this.providers.has(key)) throw new NamedDataError("NAMED_DATA_PROVIDER_CONFLICT", `Provider is already registered: ${provider.namespace}/${provider.kind}`);
|
|
344
|
+
const entry = {
|
|
345
|
+
provider,
|
|
346
|
+
lifetime: options.lifetime ?? "session"
|
|
347
|
+
};
|
|
348
|
+
this.providers.set(key, entry);
|
|
349
|
+
let active = true;
|
|
350
|
+
return {
|
|
351
|
+
namespace: provider.namespace,
|
|
352
|
+
kind: provider.kind,
|
|
353
|
+
unregister: async () => {
|
|
354
|
+
if (!active) return;
|
|
355
|
+
active = false;
|
|
356
|
+
if (this.providers.get(key) === entry) {
|
|
357
|
+
this.providers.delete(key);
|
|
358
|
+
await this.releaseHandlesForProvider(provider.namespace, provider.kind);
|
|
359
|
+
await provider.release("shutdown");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
canResolve(reference, representation) {
|
|
365
|
+
try {
|
|
366
|
+
validateReferenceShape(reference, representation);
|
|
367
|
+
return (this.providers.get(providerKey(reference.namespace, reference.kind))?.provider)?.canResolve(reference, representation) ?? false;
|
|
368
|
+
} catch {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async stat(reference, representation, context = {}) {
|
|
373
|
+
const provider = this.resolveProvider(reference, representation, context);
|
|
374
|
+
throwIfAborted(context.signal);
|
|
375
|
+
try {
|
|
376
|
+
const metadata = await provider.stat(reference, representation, context);
|
|
377
|
+
throwIfAborted(context.signal);
|
|
378
|
+
validateMetadata(metadata, reference, representation);
|
|
379
|
+
return metadata;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
throw normalizeProviderError(error);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async openBody(reference, representation, context = {}) {
|
|
385
|
+
const provider = this.resolveProvider(reference, representation, context);
|
|
386
|
+
throwIfAborted(context.signal);
|
|
387
|
+
let opened;
|
|
388
|
+
try {
|
|
389
|
+
opened = await provider.openBody(reference, representation, context);
|
|
390
|
+
validateMetadata(opened, reference, representation);
|
|
391
|
+
if (!(opened.body instanceof Uint8Array) && !(opened.body instanceof ReadableStream)) throw new NamedDataError("NAMED_DATA_INVALID_METADATA", "Provider returned an invalid body.");
|
|
392
|
+
} catch (error) {
|
|
393
|
+
throw normalizeProviderError(error);
|
|
394
|
+
}
|
|
395
|
+
if (context.signal?.aborted) {
|
|
396
|
+
await opened.release("abort");
|
|
397
|
+
throw abortedError();
|
|
398
|
+
}
|
|
399
|
+
let released = false;
|
|
400
|
+
let abortListener;
|
|
401
|
+
const tracked = {
|
|
402
|
+
namespace: reference.namespace,
|
|
403
|
+
kind: reference.kind,
|
|
404
|
+
release: async (reason = "complete") => {
|
|
405
|
+
if (released) return;
|
|
406
|
+
released = true;
|
|
407
|
+
if (abortListener && context.signal) context.signal.removeEventListener("abort", abortListener);
|
|
408
|
+
this.handles.delete(tracked);
|
|
409
|
+
await opened.release(reason);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
this.handles.add(tracked);
|
|
413
|
+
if (context.signal) {
|
|
414
|
+
abortListener = () => {
|
|
415
|
+
Promise.resolve(tracked.release("abort")).catch(() => void 0);
|
|
416
|
+
};
|
|
417
|
+
context.signal.addEventListener("abort", abortListener, { once: true });
|
|
418
|
+
if (context.signal.aborted) {
|
|
419
|
+
await tracked.release("abort");
|
|
420
|
+
throw abortedError();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return Object.freeze({
|
|
424
|
+
reference: Object.freeze({ ...opened.reference }),
|
|
425
|
+
nativeRepresentation: opened.nativeRepresentation,
|
|
426
|
+
representation: opened.representation,
|
|
427
|
+
mediaType: opened.mediaType,
|
|
428
|
+
...opened.byteLength === void 0 ? {} : { byteLength: opened.byteLength },
|
|
429
|
+
...opened.digest === void 0 ? {} : { digest: opened.digest },
|
|
430
|
+
revision: opened.revision,
|
|
431
|
+
replayable: opened.replayable,
|
|
432
|
+
body: opened.body,
|
|
433
|
+
release: tracked.release
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
async clearSession() {
|
|
437
|
+
const handles = [...this.handles];
|
|
438
|
+
this.handles.clear();
|
|
439
|
+
await Promise.allSettled(handles.map((handle) => handle.release("shutdown")));
|
|
440
|
+
const sessionEntries = [...this.providers.entries()].filter(([, entry]) => entry.lifetime === "session");
|
|
441
|
+
for (const [namespace] of sessionEntries) this.providers.delete(namespace);
|
|
442
|
+
await Promise.allSettled(sessionEntries.map(([, entry]) => entry.provider.release("shutdown")));
|
|
443
|
+
await Promise.allSettled([...this.providers.values()].map((entry) => entry.provider.clearSession?.()));
|
|
444
|
+
}
|
|
445
|
+
resolveProvider(reference, representation, context) {
|
|
446
|
+
validateReference(reference, representation, context);
|
|
447
|
+
const provider = this.providers.get(providerKey(reference.namespace, reference.kind))?.provider;
|
|
448
|
+
if (!provider) {
|
|
449
|
+
const hasNamespace = [...this.providers.values()].some((entry) => entry.provider.namespace === reference.namespace);
|
|
450
|
+
throw new NamedDataError(hasNamespace ? "NAMED_DATA_KIND_MISMATCH" : "NAMED_DATA_PROVIDER_NOT_FOUND", hasNamespace ? `No provider for kind ${reference.kind} is registered in namespace ${reference.namespace}.` : `No provider can resolve namespace: ${reference.namespace}`);
|
|
451
|
+
}
|
|
452
|
+
if (!provider.canResolve(reference, representation)) throw new NamedDataError("NAMED_DATA_REPRESENTATION_UNSUPPORTED", `Provider ${reference.namespace} does not support representation: ${representation}`);
|
|
453
|
+
return provider;
|
|
454
|
+
}
|
|
455
|
+
async releaseHandlesForProvider(namespace, kind) {
|
|
456
|
+
const handles = [...this.handles].filter((handle) => handle.namespace === namespace && handle.kind === kind);
|
|
457
|
+
await Promise.allSettled(handles.map((handle) => handle.release("shutdown")));
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
function installNamedDataRegistry(runtime) {
|
|
461
|
+
const host = runtime;
|
|
462
|
+
const existing = host[NAMED_DATA_REGISTRY_SYMBOL];
|
|
463
|
+
if (existing !== void 0) return requireCompatibleRegistry(existing);
|
|
464
|
+
const registry = new NamedDataRegistry();
|
|
465
|
+
Object.defineProperty(host, NAMED_DATA_REGISTRY_SYMBOL, {
|
|
466
|
+
configurable: true,
|
|
467
|
+
enumerable: false,
|
|
468
|
+
writable: false,
|
|
469
|
+
value: registry
|
|
470
|
+
});
|
|
471
|
+
return registry;
|
|
472
|
+
}
|
|
473
|
+
function bindNamedDataRegistryLifecycle(runtime, registry) {
|
|
474
|
+
const host = runtime;
|
|
475
|
+
const existing = host[LIFECYCLE_SYMBOL];
|
|
476
|
+
if (existing !== void 0) {
|
|
477
|
+
if (!isLifecycleBinding(existing) || existing.registry !== registry) throw new NamedDataError("NAMED_DATA_INVALID_REGISTRY", "Runtime already has a lifecycle binding for a different registry.");
|
|
478
|
+
existing.references += 1;
|
|
479
|
+
return lifecycleUnbind(runtime, host, existing);
|
|
480
|
+
}
|
|
481
|
+
const listener = () => {
|
|
482
|
+
registry.clearSession();
|
|
483
|
+
};
|
|
484
|
+
runtime.on("PROJECT_STOP_ALL", listener);
|
|
485
|
+
const binding = {
|
|
486
|
+
registry,
|
|
487
|
+
listener,
|
|
488
|
+
references: 1
|
|
489
|
+
};
|
|
490
|
+
Object.defineProperty(host, LIFECYCLE_SYMBOL, {
|
|
491
|
+
configurable: true,
|
|
492
|
+
enumerable: false,
|
|
493
|
+
writable: false,
|
|
494
|
+
value: binding
|
|
495
|
+
});
|
|
496
|
+
return lifecycleUnbind(runtime, host, binding);
|
|
497
|
+
}
|
|
498
|
+
function isLifecycleBinding(value) {
|
|
499
|
+
if (typeof value !== "object" || value === null) return false;
|
|
500
|
+
const candidate = value;
|
|
501
|
+
return isCompatibleRegistry(candidate.registry) && typeof candidate.listener === "function" && Number.isSafeInteger(candidate.references) && (candidate.references ?? -1) >= 0;
|
|
502
|
+
}
|
|
503
|
+
function lifecycleUnbind(runtime, host, binding) {
|
|
504
|
+
let active = true;
|
|
505
|
+
return () => {
|
|
506
|
+
if (!active || host[LIFECYCLE_SYMBOL] !== binding) return;
|
|
507
|
+
active = false;
|
|
508
|
+
binding.references -= 1;
|
|
509
|
+
if (binding.references > 0) return;
|
|
510
|
+
if (runtime.off) {
|
|
511
|
+
runtime.off("PROJECT_STOP_ALL", binding.listener);
|
|
512
|
+
delete host[LIFECYCLE_SYMBOL];
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function requireCompatibleRegistry(value) {
|
|
517
|
+
if (isCompatibleRegistry(value)) return value;
|
|
518
|
+
throw new NamedDataError("NAMED_DATA_INVALID_REGISTRY", `Runtime slot ${NAMED_DATA_REGISTRY_SYMBOL_KEY} contains an invalid registry.`);
|
|
519
|
+
}
|
|
520
|
+
function isCompatibleRegistry(value) {
|
|
521
|
+
if (typeof value !== "object" || value === null) return false;
|
|
522
|
+
try {
|
|
523
|
+
const candidate = value;
|
|
524
|
+
return typeof candidate.registerProvider === "function" && typeof candidate.canResolve === "function" && typeof candidate.stat === "function" && typeof candidate.openBody === "function" && typeof candidate.clearSession === "function";
|
|
525
|
+
} catch {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function validateReference(reference, representation, context) {
|
|
530
|
+
validateReferenceShape(reference, representation);
|
|
531
|
+
if (reference.scope === "target" && context.target === void 0) throw new NamedDataError("NAMED_DATA_SCOPE_MISMATCH", "Target scope requires target context.");
|
|
532
|
+
if (reference.scope === "project" && context.project === void 0) throw new NamedDataError("NAMED_DATA_SCOPE_MISMATCH", "Project scope requires project context.");
|
|
533
|
+
throwIfAborted(context.signal);
|
|
534
|
+
}
|
|
535
|
+
function validateReferenceShape(reference, representation) {
|
|
536
|
+
if (!reference || typeof reference !== "object") throw invalidReference$1();
|
|
537
|
+
requireNamespace(reference.namespace);
|
|
538
|
+
if (typeof reference.name !== "string" || reference.name.length === 0 || reference.name.length > 256 || containsControlCharacter(reference.name) || !NAMED_DATA_KINDS.includes(reference.kind) || !NAMED_DATA_SCOPES.includes(reference.scope) || !NAMED_DATA_REPRESENTATIONS.includes(representation)) throw invalidReference$1();
|
|
539
|
+
}
|
|
540
|
+
function validateMetadata(metadata, reference, representation) {
|
|
541
|
+
if (metadata.representation !== representation || !isNativeRepresentation(metadata.reference.kind, metadata.nativeRepresentation) || metadata.reference.namespace !== reference.namespace || metadata.reference.name !== reference.name || metadata.reference.kind !== reference.kind || metadata.reference.scope !== reference.scope || typeof metadata.mediaType !== "string" || metadata.mediaType.length === 0 || typeof metadata.revision !== "string" || metadata.revision.length === 0 || typeof metadata.replayable !== "boolean" || metadata.byteLength !== void 0 && (!Number.isSafeInteger(metadata.byteLength) || metadata.byteLength < 0) || metadata.digest !== void 0 && !/^sha256-[A-Za-z0-9_-]+$/u.test(metadata.digest)) throw new NamedDataError("NAMED_DATA_INVALID_METADATA", "Provider returned invalid metadata.");
|
|
542
|
+
}
|
|
543
|
+
function isNativeRepresentation(kind, representation) {
|
|
544
|
+
if (kind === "structured") return representation === "json" || representation === "yaml";
|
|
545
|
+
if (kind === "document") return representation === "html" || representation === "markdown";
|
|
546
|
+
return representation === "raw";
|
|
547
|
+
}
|
|
548
|
+
function requireNamespace(namespace) {
|
|
549
|
+
if (!isNamedDataNamespace(namespace)) throw invalidReference$1("Invalid namespace.");
|
|
550
|
+
}
|
|
551
|
+
function providerKey(namespace, kind) {
|
|
552
|
+
return `${namespace}\0${kind}`;
|
|
553
|
+
}
|
|
554
|
+
function containsControlCharacter(value) {
|
|
555
|
+
return [...value].some((character) => {
|
|
556
|
+
const code = character.codePointAt(0) ?? 0;
|
|
557
|
+
return code <= 31 || code === 127;
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
function invalidReference$1(message = "Invalid named-data reference.") {
|
|
561
|
+
return new NamedDataError("NAMED_DATA_INVALID_REF", message);
|
|
562
|
+
}
|
|
563
|
+
function throwIfAborted(signal) {
|
|
564
|
+
if (signal?.aborted) throw abortedError();
|
|
565
|
+
}
|
|
566
|
+
function abortedError() {
|
|
567
|
+
return new NamedDataError("NAMED_DATA_ABORTED", "Named-data operation was aborted.");
|
|
568
|
+
}
|
|
569
|
+
function normalizeProviderError(error) {
|
|
570
|
+
if (error instanceof NamedDataError) return error;
|
|
571
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string" && errorCodes.has(error.code)) return new NamedDataError(error.code, error.message, { cause: error });
|
|
572
|
+
return new NamedDataError("NAMED_DATA_PROVIDER_RELEASED", "Provider operation failed.", { cause: error });
|
|
573
|
+
}
|
|
574
|
+
function configuredFlag$1(name) {
|
|
575
|
+
const value = globalThis.__TW_NAMED_DATA_FEATURE_FLAGS__?.[name];
|
|
576
|
+
return value === true || value === "true";
|
|
577
|
+
}
|
|
578
|
+
Object.freeze({ NAMED_DATA_REGISTRY_MVP: configuredFlag$1("NAMED_DATA_REGISTRY_MVP") });
|
|
579
|
+
//#endregion
|
|
580
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/typeof.js
|
|
581
|
+
function _typeof(o) {
|
|
582
|
+
"@babel/helpers - typeof";
|
|
583
|
+
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
584
|
+
return typeof o;
|
|
585
|
+
} : function(o) {
|
|
586
|
+
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
587
|
+
}, _typeof(o);
|
|
588
|
+
}
|
|
589
|
+
//#endregion
|
|
590
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/toPrimitive.js
|
|
591
|
+
function toPrimitive(t, r) {
|
|
592
|
+
if ("object" != _typeof(t) || !t) return t;
|
|
593
|
+
var e = t[Symbol.toPrimitive];
|
|
594
|
+
if (void 0 !== e) {
|
|
595
|
+
var i = e.call(t, r || "default");
|
|
596
|
+
if ("object" != _typeof(i)) return i;
|
|
597
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
598
|
+
}
|
|
599
|
+
return ("string" === r ? String : Number)(t);
|
|
600
|
+
}
|
|
601
|
+
//#endregion
|
|
602
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/toPropertyKey.js
|
|
603
|
+
function toPropertyKey(t) {
|
|
604
|
+
var i = toPrimitive(t, "string");
|
|
605
|
+
return "symbol" == _typeof(i) ? i : i + "";
|
|
606
|
+
}
|
|
607
|
+
//#endregion
|
|
608
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/defineProperty.js
|
|
609
|
+
function _defineProperty(e, r, t) {
|
|
610
|
+
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
611
|
+
value: t,
|
|
612
|
+
enumerable: !0,
|
|
613
|
+
configurable: !0,
|
|
614
|
+
writable: !0
|
|
615
|
+
}) : e[r] = t, e;
|
|
616
|
+
}
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region src/asset-cache-error.ts
|
|
619
|
+
var AssetManagerError = class extends Error {
|
|
620
|
+
constructor(code, message, context) {
|
|
621
|
+
const hintText = context.hint ? ` ${context.hint}` : "";
|
|
622
|
+
super(`[Asset Cache][${code}] ${message}${hintText}`, { cause: context.cause });
|
|
623
|
+
_defineProperty(this, "code", void 0);
|
|
624
|
+
_defineProperty(this, "operation", void 0);
|
|
625
|
+
_defineProperty(this, "assetName", void 0);
|
|
626
|
+
_defineProperty(this, "resourceId", void 0);
|
|
627
|
+
_defineProperty(this, "actorName", void 0);
|
|
628
|
+
_defineProperty(this, "expectedKind", void 0);
|
|
629
|
+
_defineProperty(this, "actualKind", void 0);
|
|
630
|
+
_defineProperty(this, "hint", void 0);
|
|
631
|
+
_defineProperty(this, "candidates", void 0);
|
|
632
|
+
this.name = "AssetManagerError";
|
|
633
|
+
this.code = code;
|
|
634
|
+
this.operation = context.operation;
|
|
635
|
+
this.assetName = context.assetName;
|
|
636
|
+
this.resourceId = context.resourceId;
|
|
637
|
+
this.actorName = context.actorName;
|
|
638
|
+
this.expectedKind = context.expectedKind;
|
|
639
|
+
this.actualKind = context.actualKind;
|
|
640
|
+
this.hint = context.hint;
|
|
641
|
+
this.candidates = context.candidates ?? [];
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
function editDistance(left, right) {
|
|
645
|
+
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
646
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
647
|
+
const current = [leftIndex];
|
|
648
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (previous[rightIndex] ?? 0) + 1, (previous[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1));
|
|
649
|
+
previous.splice(0, previous.length, ...current);
|
|
650
|
+
}
|
|
651
|
+
return previous[right.length] ?? 0;
|
|
652
|
+
}
|
|
653
|
+
function suggestNames(value, choices, limit = 3) {
|
|
654
|
+
const input = String(value ?? "").trim();
|
|
655
|
+
if (!input || limit <= 0) return [];
|
|
656
|
+
const normalizedInput = input.toLocaleLowerCase();
|
|
657
|
+
return [...new Set([...choices].map((choice) => choice.trim()).filter(Boolean))].map((choice, index) => ({
|
|
658
|
+
choice,
|
|
659
|
+
index,
|
|
660
|
+
exactCaseInsensitive: choice.toLocaleLowerCase() === normalizedInput,
|
|
661
|
+
distance: editDistance(normalizedInput, choice.toLocaleLowerCase())
|
|
662
|
+
})).sort((left, right) => Number(right.exactCaseInsensitive) - Number(left.exactCaseInsensitive) || left.distance - right.distance || left.index - right.index).slice(0, limit).map(({ choice }) => choice);
|
|
663
|
+
}
|
|
664
|
+
function suggestionHint(candidates) {
|
|
665
|
+
if (candidates.length === 0) return void 0;
|
|
666
|
+
if (candidates.length === 1) return `Did you mean "${candidates[0]}"?`;
|
|
667
|
+
return `Did you mean one of: ${candidates.map((candidate) => `"${candidate}"`).join(", ")}?`;
|
|
668
|
+
}
|
|
669
|
+
function errorMessage(error) {
|
|
670
|
+
return error instanceof Error ? error.message : String(error);
|
|
671
|
+
}
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region src/feature-flags.ts
|
|
674
|
+
function configuredFlag(name) {
|
|
675
|
+
const configured = globalThis.__TW_ASSET_MANAGER_FEATURE_FLAGS__?.[name];
|
|
676
|
+
return configured === true || configured === "true";
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Startup-fixed rollout flags. Optional features remain disabled unless a host
|
|
680
|
+
* explicitly configures them before loading the extension bundle.
|
|
681
|
+
*/
|
|
682
|
+
var FEATURE_FLAGS = Object.freeze({
|
|
683
|
+
ENABLE_LIVE_ASSET_REPLACEMENT: configuredFlag("ENABLE_LIVE_ASSET_REPLACEMENT"),
|
|
684
|
+
ENABLE_STRICT_ASSET_KIND_REPLACEMENT: configuredFlag("ENABLE_STRICT_ASSET_KIND_REPLACEMENT"),
|
|
685
|
+
NAMED_ASSET_BODY_PROVIDER: configuredFlag("NAMED_ASSET_BODY_PROVIDER")
|
|
686
|
+
});
|
|
687
|
+
//#endregion
|
|
688
|
+
//#region src/dom-image-resource.ts
|
|
689
|
+
var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
690
|
+
var XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
|
|
691
|
+
var XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
|
|
692
|
+
var XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
|
|
693
|
+
var FORBIDDEN_SVG_ELEMENTS = /* @__PURE__ */ new Set([
|
|
694
|
+
"animate",
|
|
695
|
+
"animatemotion",
|
|
696
|
+
"animatetransform",
|
|
697
|
+
"discard",
|
|
698
|
+
"embed",
|
|
699
|
+
"foreignobject",
|
|
700
|
+
"iframe",
|
|
701
|
+
"object",
|
|
702
|
+
"script",
|
|
703
|
+
"set"
|
|
704
|
+
]);
|
|
705
|
+
var SAFE_DATA_IMAGE_REFERENCE = /^data:image\/(?:avif|bmp|gif|jpeg|png|webp);base64,[a-z0-9+/=\s]+$/i;
|
|
706
|
+
async function createDOMImageResourceBacking(input, onIdle) {
|
|
707
|
+
const normalizedInput = {
|
|
708
|
+
...input,
|
|
709
|
+
mimeType: normalizedImageMimeType(input.mimeType)
|
|
710
|
+
};
|
|
711
|
+
const verified = normalizedInput.mimeType === "image/svg+xml" ? verifySVGImage(normalizedInput) : await verifyRasterImage(normalizedInput);
|
|
712
|
+
const objectURL = URL.createObjectURL(new Blob([copyArrayBuffer(verified.bytes)], { type: verified.mimeType }));
|
|
713
|
+
let leaseCount = 0;
|
|
714
|
+
let revoked = false;
|
|
715
|
+
let idleListener = onIdle;
|
|
716
|
+
const backing = Object.freeze({ acquire(onRelease) {
|
|
717
|
+
if (revoked) throw new Error("DOM image resource backing has been released.");
|
|
718
|
+
leaseCount += 1;
|
|
719
|
+
let released = false;
|
|
720
|
+
let releaseListener = onRelease;
|
|
721
|
+
let backingReference = backing;
|
|
722
|
+
const resource = Object.freeze({
|
|
723
|
+
url: objectURL,
|
|
724
|
+
mimeType: verified.mimeType,
|
|
725
|
+
width: verified.width,
|
|
726
|
+
height: verified.height,
|
|
727
|
+
get released() {
|
|
728
|
+
return released;
|
|
729
|
+
},
|
|
730
|
+
release() {
|
|
731
|
+
if (released) return;
|
|
732
|
+
released = true;
|
|
733
|
+
leaseCount -= 1;
|
|
734
|
+
const releasedBacking = backingReference;
|
|
735
|
+
backingReference = void 0;
|
|
736
|
+
try {
|
|
737
|
+
if (leaseCount === 0) {
|
|
738
|
+
revoked = true;
|
|
739
|
+
URL.revokeObjectURL(objectURL);
|
|
740
|
+
}
|
|
741
|
+
} finally {
|
|
742
|
+
const listener = releaseListener;
|
|
743
|
+
releaseListener = void 0;
|
|
744
|
+
try {
|
|
745
|
+
listener?.(resource);
|
|
746
|
+
} finally {
|
|
747
|
+
if (revoked) {
|
|
748
|
+
const listener = idleListener;
|
|
749
|
+
idleListener = void 0;
|
|
750
|
+
if (releasedBacking) listener?.(releasedBacking);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
return resource;
|
|
757
|
+
} });
|
|
758
|
+
return backing;
|
|
759
|
+
}
|
|
760
|
+
function verifySVGImage(input) {
|
|
761
|
+
let source;
|
|
762
|
+
try {
|
|
763
|
+
source = new TextDecoder("utf-8", { fatal: true }).decode(input.bytes);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
throw invalidResource(input.name, "SVG bytes are not valid UTF-8.", error);
|
|
766
|
+
}
|
|
767
|
+
if (/<!DOCTYPE|<!ENTITY|<\?xml-stylesheet\b/i.test(source)) throw unsafeSVG(input.name, "DOCTYPE, entity, and stylesheet processing instructions are forbidden.");
|
|
768
|
+
if (typeof DOMParser !== "function" || typeof XMLSerializer !== "function") throw new AssetManagerError("DEPENDENCY_MISSING", "DOMParser and XMLSerializer are required to validate SVG image resources.", {
|
|
769
|
+
operation: "resolveDOMImageResource",
|
|
770
|
+
assetName: input.name,
|
|
771
|
+
hint: "Resolve SVG resources in a browser DOM environment."
|
|
772
|
+
});
|
|
773
|
+
let document;
|
|
774
|
+
try {
|
|
775
|
+
document = new DOMParser().parseFromString(source, "image/svg+xml");
|
|
776
|
+
} catch (error) {
|
|
777
|
+
throw invalidResource(input.name, "SVG markup is not well-formed XML.", error);
|
|
778
|
+
}
|
|
779
|
+
const root = document.documentElement;
|
|
780
|
+
if (root.localName.toLowerCase() === "parsererror" || document.getElementsByTagName("parsererror").length > 0) throw invalidResource(input.name, "SVG markup is not well-formed XML.");
|
|
781
|
+
if (root.namespaceURI !== SVG_NAMESPACE || root.localName.toLowerCase() !== "svg") throw invalidResource(input.name, "SVG resource must have an SVG root element.");
|
|
782
|
+
validateSVGElement(input.name, root);
|
|
783
|
+
for (const element of root.getElementsByTagName("*")) validateSVGElement(input.name, element);
|
|
784
|
+
const { width, height } = svgIntrinsicSize(input.name, root);
|
|
785
|
+
const serialized = new XMLSerializer().serializeToString(document);
|
|
786
|
+
return {
|
|
787
|
+
bytes: new TextEncoder().encode(serialized),
|
|
788
|
+
mimeType: "image/svg+xml",
|
|
789
|
+
width,
|
|
790
|
+
height
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function validateSVGElement(name, element) {
|
|
794
|
+
const localName = element.localName.toLowerCase();
|
|
795
|
+
if (element.namespaceURI !== SVG_NAMESPACE) throw unsafeSVG(name, `Element namespace is not allowed: ${element.namespaceURI ?? "(none)"}.`);
|
|
796
|
+
if (FORBIDDEN_SVG_ELEMENTS.has(localName)) throw unsafeSVG(name, `SVG <${localName}> elements are forbidden.`);
|
|
797
|
+
if (localName === "style") validateCSSReferences(name, element.textContent ?? "");
|
|
798
|
+
for (const attribute of [...element.attributes]) {
|
|
799
|
+
const attributeName = attribute.localName.toLowerCase();
|
|
800
|
+
if (attributeName.startsWith("on")) throw unsafeSVG(name, `SVG event handler attribute ${attribute.name} is forbidden.`);
|
|
801
|
+
if (attribute.namespaceURI && ![
|
|
802
|
+
XLINK_NAMESPACE,
|
|
803
|
+
XML_NAMESPACE,
|
|
804
|
+
XMLNS_NAMESPACE
|
|
805
|
+
].includes(attribute.namespaceURI)) throw unsafeSVG(name, `Attribute namespace is not allowed: ${attribute.namespaceURI}.`);
|
|
806
|
+
if (attributeName === "base" && attribute.namespaceURI === XML_NAMESPACE) throw unsafeSVG(name, "SVG xml:base is forbidden.");
|
|
807
|
+
if (attributeName === "href" || attributeName === "src") validateImageReference(name, attribute.value);
|
|
808
|
+
if (attributeName === "style" || /url\s*\(/i.test(attribute.value)) validateCSSReferences(name, attribute.value);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function validateImageReference(name, rawReference) {
|
|
812
|
+
const reference = rawReference.trim();
|
|
813
|
+
if (!reference || reference.startsWith("#") || SAFE_DATA_IMAGE_REFERENCE.test(reference)) return;
|
|
814
|
+
throw unsafeSVG(name, `External SVG reference is forbidden: ${safeLabel(reference)}.`);
|
|
815
|
+
}
|
|
816
|
+
function validateCSSReferences(name, css) {
|
|
817
|
+
if (/[\\@]|\/\*|expression\s*\(|-moz-binding\s*:|(?:-webkit-)?image(?:-set)?\s*\(|cross-fade\s*\(|(?:https?|file|ftp|javascript):|\/\//i.test(css)) throw unsafeSVG(name, "Imported, obfuscated, external, or executable SVG CSS is forbidden.");
|
|
818
|
+
for (const match of css.matchAll(/url\(\s*(['"]?)(.*?)\1\s*\)/gi)) validateImageReference(name, match[2] ?? "");
|
|
819
|
+
}
|
|
820
|
+
function svgIntrinsicSize(name, root) {
|
|
821
|
+
const rawWidth = root.getAttribute("width");
|
|
822
|
+
const rawHeight = root.getAttribute("height");
|
|
823
|
+
const rawViewBox = root.getAttribute("viewBox");
|
|
824
|
+
const width = absoluteSVGLength(rawWidth);
|
|
825
|
+
const height = absoluteSVGLength(rawHeight);
|
|
826
|
+
const viewBox = svgViewBox(rawViewBox);
|
|
827
|
+
if (rawWidth !== null && width === null) throw invalidResource(name, "SVG width must be a positive absolute length.");
|
|
828
|
+
if (rawHeight !== null && height === null) throw invalidResource(name, "SVG height must be a positive absolute length.");
|
|
829
|
+
if (rawViewBox !== null && viewBox === null) throw invalidResource(name, "SVG viewBox must contain four finite values and positive dimensions.");
|
|
830
|
+
if (width !== null && height !== null) return {
|
|
831
|
+
width,
|
|
832
|
+
height
|
|
833
|
+
};
|
|
834
|
+
if (width !== null && viewBox) return {
|
|
835
|
+
width,
|
|
836
|
+
height: width * viewBox.height / viewBox.width
|
|
837
|
+
};
|
|
838
|
+
if (height !== null && viewBox) return {
|
|
839
|
+
width: height * viewBox.width / viewBox.height,
|
|
840
|
+
height
|
|
841
|
+
};
|
|
842
|
+
if (viewBox) return {
|
|
843
|
+
width: viewBox.width,
|
|
844
|
+
height: viewBox.height
|
|
845
|
+
};
|
|
846
|
+
throw invalidResource(name, "SVG resource must declare positive absolute width/height or a positive viewBox.");
|
|
847
|
+
}
|
|
848
|
+
function absoluteSVGLength(raw) {
|
|
849
|
+
if (raw === null || !raw.trim()) return null;
|
|
850
|
+
const match = /^([+]?(?:\d+\.?\d*|\.\d+))(px|in|cm|mm|q|pt|pc)?$/i.exec(raw.trim());
|
|
851
|
+
if (!match) return null;
|
|
852
|
+
const pixels = Number(match[1]) * ((/* @__PURE__ */ new Map([
|
|
853
|
+
["", 1],
|
|
854
|
+
["px", 1],
|
|
855
|
+
["in", 96],
|
|
856
|
+
["cm", 96 / 2.54],
|
|
857
|
+
["mm", 96 / 25.4],
|
|
858
|
+
["q", 96 / 101.6],
|
|
859
|
+
["pt", 96 / 72],
|
|
860
|
+
["pc", 16]
|
|
861
|
+
])).get((match[2] ?? "").toLowerCase()) ?? NaN);
|
|
862
|
+
return Number.isFinite(pixels) && pixels > 0 ? pixels : null;
|
|
863
|
+
}
|
|
864
|
+
function svgViewBox(raw) {
|
|
865
|
+
if (raw === null) return null;
|
|
866
|
+
const values = raw.trim().split(/[\s,]+/).map(Number);
|
|
867
|
+
if (values.length !== 4 || values.some((value) => !Number.isFinite(value)) || !(values[2] > 0) || !(values[3] > 0)) return null;
|
|
868
|
+
return {
|
|
869
|
+
width: values[2],
|
|
870
|
+
height: values[3]
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
async function verifyRasterImage(input) {
|
|
874
|
+
const detectedMimeType = sniffRasterMimeType(input.bytes);
|
|
875
|
+
if (!detectedMimeType || detectedMimeType !== input.mimeType) throw new AssetManagerError("ASSET_TYPE_MISMATCH", `Image asset "${input.name}" bytes do not match MIME type ${input.mimeType}.`, {
|
|
876
|
+
operation: "resolveDOMImageResource",
|
|
877
|
+
assetName: input.name,
|
|
878
|
+
expectedKind: input.mimeType,
|
|
879
|
+
actualKind: detectedMimeType ?? "unrecognized image bytes",
|
|
880
|
+
hint: "Use PNG, JPEG, GIF, WebP, BMP, or AVIF bytes with the matching MIME type."
|
|
881
|
+
});
|
|
882
|
+
if (typeof createImageBitmap !== "function") throw new AssetManagerError("DEPENDENCY_MISSING", "createImageBitmap is required to validate raster image dimensions.", {
|
|
883
|
+
operation: "resolveDOMImageResource",
|
|
884
|
+
assetName: input.name,
|
|
885
|
+
hint: "Resolve raster resources in a browser that supports createImageBitmap."
|
|
886
|
+
});
|
|
887
|
+
let bitmap;
|
|
888
|
+
try {
|
|
889
|
+
bitmap = await createImageBitmap(new Blob([copyArrayBuffer(input.bytes)], { type: input.mimeType }));
|
|
890
|
+
} catch (error) {
|
|
891
|
+
throw invalidResource(input.name, "Raster image bytes could not be decoded.", error);
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
if (!Number.isFinite(bitmap.width) || bitmap.width <= 0 || !Number.isFinite(bitmap.height) || bitmap.height <= 0) throw invalidResource(input.name, "Raster image has invalid intrinsic dimensions.");
|
|
895
|
+
return {
|
|
896
|
+
bytes: input.bytes,
|
|
897
|
+
mimeType: input.mimeType,
|
|
898
|
+
width: bitmap.width,
|
|
899
|
+
height: bitmap.height
|
|
900
|
+
};
|
|
901
|
+
} finally {
|
|
902
|
+
bitmap.close();
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function sniffRasterMimeType(bytes) {
|
|
906
|
+
if (startsWith(bytes, [
|
|
907
|
+
137,
|
|
908
|
+
80,
|
|
909
|
+
78,
|
|
910
|
+
71,
|
|
911
|
+
13,
|
|
912
|
+
10,
|
|
913
|
+
26,
|
|
914
|
+
10
|
|
915
|
+
])) return "image/png";
|
|
916
|
+
if (startsWith(bytes, [
|
|
917
|
+
255,
|
|
918
|
+
216,
|
|
919
|
+
255
|
|
920
|
+
])) return "image/jpeg";
|
|
921
|
+
if (ascii(bytes, 0, 6) === "GIF87a" || ascii(bytes, 0, 6) === "GIF89a") return "image/gif";
|
|
922
|
+
if (ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WEBP") return "image/webp";
|
|
923
|
+
if (ascii(bytes, 0, 2) === "BM") return "image/bmp";
|
|
924
|
+
if (ascii(bytes, 4, 4) === "ftyp") {
|
|
925
|
+
const brand = ascii(bytes, 8, 4);
|
|
926
|
+
if (brand === "avif" || brand === "avis") return "image/avif";
|
|
927
|
+
}
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
function normalizedImageMimeType(value) {
|
|
931
|
+
const mimeType = value.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
932
|
+
if (mimeType === "image/jpg" || mimeType === "image/pjpeg") return "image/jpeg";
|
|
933
|
+
if (mimeType === "image/x-png") return "image/png";
|
|
934
|
+
if (mimeType === "image/x-ms-bmp") return "image/bmp";
|
|
935
|
+
return mimeType;
|
|
936
|
+
}
|
|
937
|
+
function startsWith(bytes, prefix) {
|
|
938
|
+
return prefix.every((value, index) => bytes[index] === value);
|
|
939
|
+
}
|
|
940
|
+
function ascii(bytes, offset, length) {
|
|
941
|
+
if (bytes.byteLength < offset + length) return "";
|
|
942
|
+
return String.fromCharCode(...bytes.subarray(offset, offset + length));
|
|
943
|
+
}
|
|
944
|
+
function copyArrayBuffer(bytes) {
|
|
945
|
+
return Uint8Array.from(bytes).buffer;
|
|
946
|
+
}
|
|
947
|
+
function invalidResource(name, message, cause) {
|
|
948
|
+
return new AssetManagerError("RESOURCE_ID_INVALID", message, {
|
|
949
|
+
operation: "resolveDOMImageResource",
|
|
950
|
+
assetName: name,
|
|
951
|
+
hint: "Provide a well-formed image with verifiable intrinsic dimensions.",
|
|
952
|
+
cause
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
function unsafeSVG(name, message) {
|
|
956
|
+
return new AssetManagerError("RESOURCE_ID_INVALID", message, {
|
|
957
|
+
operation: "resolveDOMImageResource",
|
|
958
|
+
assetName: name,
|
|
959
|
+
hint: "Remove scripts, event handlers, embedded HTML, and external references from the SVG."
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
function safeLabel(value) {
|
|
963
|
+
const normalized = value.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 80);
|
|
964
|
+
return JSON.stringify(normalized || "(empty)");
|
|
965
|
+
}
|
|
966
|
+
//#endregion
|
|
967
|
+
//#region src/audio-voice.ts
|
|
968
|
+
function normalizeAudioVoiceGain(value, label = "Audio voice gain") {
|
|
969
|
+
const gain = value === void 0 ? 1 : value;
|
|
970
|
+
if (typeof gain !== "number" || !Number.isFinite(gain) || gain < 0 || gain > 1) throw new TypeError(`${label} must be a finite number from 0 to 1.`);
|
|
971
|
+
return gain;
|
|
972
|
+
}
|
|
973
|
+
//#endregion
|
|
974
|
+
//#region src/named-body-provider.ts
|
|
975
|
+
var NAMED_ASSET_BODY_NAMESPACE = "asset";
|
|
976
|
+
function requireNamedAssetBodyReference(value) {
|
|
977
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalidReference();
|
|
978
|
+
const reference = value;
|
|
979
|
+
if (reference.namespace !== "asset" || typeof reference.name !== "string" || reference.name.length === 0 || reference.kind !== "asset" || reference.scope !== "project") throw invalidReference();
|
|
980
|
+
return Object.freeze({
|
|
981
|
+
namespace: NAMED_ASSET_BODY_NAMESPACE,
|
|
982
|
+
name: reference.name,
|
|
983
|
+
kind: "asset",
|
|
984
|
+
scope: "project"
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
function invalidReference() {
|
|
988
|
+
return new NamedDataError("NAMED_DATA_INVALID_REF", "Expected an asset/project reference with a non-empty string name.");
|
|
989
|
+
}
|
|
990
|
+
function namedBodyAbortError() {
|
|
991
|
+
return new NamedDataError("NAMED_DATA_ABORTED", "Named asset body resolution was aborted.");
|
|
992
|
+
}
|
|
993
|
+
function throwIfNamedBodyAborted(signal) {
|
|
994
|
+
if (signal?.aborted) throw namedBodyAbortError();
|
|
995
|
+
}
|
|
996
|
+
//#endregion
|
|
997
|
+
//#region src/text-style.ts
|
|
998
|
+
var TEXT_RUNTIME_NAMESPACE = "text";
|
|
999
|
+
var TEXT_STYLE_RUNTIME_NAMESPACE = "textStyle";
|
|
1000
|
+
var TEXT_STYLE_PROPERTIES = [
|
|
1001
|
+
"animation",
|
|
1002
|
+
"font",
|
|
1003
|
+
"color",
|
|
1004
|
+
"width",
|
|
1005
|
+
"align"
|
|
1006
|
+
];
|
|
1007
|
+
var DEFAULT_STAGE_WIDTH = 480;
|
|
1008
|
+
var DEFAULT_FONT = "Handwriting";
|
|
1009
|
+
var DEFAULT_COLOR = "#ffffff";
|
|
1010
|
+
var DEFAULT_ALIGNMENT = "center";
|
|
1011
|
+
var DEFAULT_OUTLINE_COLOR = "#000000";
|
|
1012
|
+
function textRuntimeVariableName(name) {
|
|
1013
|
+
return `${TEXT_RUNTIME_NAMESPACE}:${name}`;
|
|
1014
|
+
}
|
|
1015
|
+
function textStyleRuntimeVariableName(name, property) {
|
|
1016
|
+
return `${TEXT_STYLE_RUNTIME_NAMESPACE}:${name}:${property}`;
|
|
1017
|
+
}
|
|
1018
|
+
function normalizeTextStyleProperty(value) {
|
|
1019
|
+
const property = String(value ?? "").trim().toLowerCase();
|
|
1020
|
+
if (TEXT_STYLE_PROPERTIES.includes(property)) return property;
|
|
1021
|
+
throw new Error(`Unknown text style property: ${property || "(empty)"}`);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Normalize a DSL-facing style value for runtime-variable storage.
|
|
1025
|
+
* An empty value intentionally resets the property to its default.
|
|
1026
|
+
*/
|
|
1027
|
+
function normalizeTextStyleValue(property, value) {
|
|
1028
|
+
const raw = String(value ?? "").trim();
|
|
1029
|
+
if (!raw) return "";
|
|
1030
|
+
switch (property) {
|
|
1031
|
+
case "animation": {
|
|
1032
|
+
const animation = raw.toLowerCase() === "typing" ? "type" : raw.toLowerCase();
|
|
1033
|
+
if (animation === "none" || animation === "type" || animation === "rainbow" || animation === "zoom" || animation === "shake") return animation;
|
|
1034
|
+
throw new Error(`Invalid text animation: ${raw}`);
|
|
1035
|
+
}
|
|
1036
|
+
case "font": return raw;
|
|
1037
|
+
case "color": {
|
|
1038
|
+
if (/^#[0-9a-f]{6}$/i.test(raw)) return raw.toLowerCase();
|
|
1039
|
+
const shortColor = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(raw);
|
|
1040
|
+
if (shortColor) return `#${shortColor[1]}${shortColor[1]}${shortColor[2]}${shortColor[2]}${shortColor[3]}${shortColor[3]}`.toLowerCase();
|
|
1041
|
+
throw new Error(`Invalid text color: ${raw}`);
|
|
1042
|
+
}
|
|
1043
|
+
case "width": {
|
|
1044
|
+
const width = Number(raw);
|
|
1045
|
+
if (!Number.isFinite(width) || width <= 0) throw new Error(`Text width must be a positive number: ${raw}`);
|
|
1046
|
+
return String(width);
|
|
1047
|
+
}
|
|
1048
|
+
case "align": {
|
|
1049
|
+
const align = raw.toLowerCase();
|
|
1050
|
+
if (align === "left" || align === "center" || align === "right") return align;
|
|
1051
|
+
throw new Error(`Invalid text alignment: ${raw}`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function resolveTextStyle(name, stageWidth, getRuntimeVariable) {
|
|
1056
|
+
const width = Number(stageWidth);
|
|
1057
|
+
const defaults = {
|
|
1058
|
+
animation: "none",
|
|
1059
|
+
font: DEFAULT_FONT,
|
|
1060
|
+
color: DEFAULT_COLOR,
|
|
1061
|
+
width: Number.isFinite(width) && width > 0 ? width : DEFAULT_STAGE_WIDTH,
|
|
1062
|
+
align: DEFAULT_ALIGNMENT
|
|
1063
|
+
};
|
|
1064
|
+
const read = (property) => normalizeTextStyleValue(property, getRuntimeVariable(textStyleRuntimeVariableName(name, property)));
|
|
1065
|
+
const animation = read("animation");
|
|
1066
|
+
const font = read("font");
|
|
1067
|
+
const color = read("color");
|
|
1068
|
+
const configuredWidth = read("width");
|
|
1069
|
+
const align = read("align");
|
|
1070
|
+
return {
|
|
1071
|
+
animation: animation ? animation : defaults.animation,
|
|
1072
|
+
font: font || defaults.font,
|
|
1073
|
+
color: color || defaults.color,
|
|
1074
|
+
width: configuredWidth ? Number(configuredWidth) : defaults.width,
|
|
1075
|
+
align: align ? align : defaults.align
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
//#endregion
|
|
1079
|
+
//#region src/extension.ts
|
|
1080
|
+
var EXTENSION_ID = "kubohiroyaassetcache";
|
|
1081
|
+
var EXTENSION_VERSION = "0.14.0";
|
|
1082
|
+
var EXTENSION_DOCS_URI = "https://kubohiroya.github.io/turbowarp-asset-cache/";
|
|
1083
|
+
var BLOCK_ICON_URI = `data:image/svg+xml,${encodeURIComponent("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\"><path fill=\"#fff\" d=\"M19 47 29 17h7l10 30h-7l-2-7H27l-2 7h-6Zm10-13h6l-3-10-3 10Z\"/></svg>")}`;
|
|
1084
|
+
var DB_NAME = "tw-asset-cache";
|
|
1085
|
+
var DB_VERSION = 1;
|
|
1086
|
+
var STORE_NAME = "assets";
|
|
1087
|
+
var STAGE_RESOURCE_NAME = "@stage";
|
|
1088
|
+
async function sha256Digest(bytes) {
|
|
1089
|
+
const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes).buffer);
|
|
1090
|
+
return `sha256-${Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("")}`;
|
|
1091
|
+
}
|
|
1092
|
+
var blockDefinitions = block_definitions_default.blocks;
|
|
1093
|
+
blockDefinitions.unshift({
|
|
1094
|
+
opcode: "validateProjectAssetAddress",
|
|
1095
|
+
blockType: "REPORTER",
|
|
1096
|
+
text: "validate project asset address [RESOURCE_ID] for [NAME]",
|
|
1097
|
+
description: "Returns a JSON validation result without fetching, caching, registering, or rendering the asset.",
|
|
1098
|
+
arguments: {
|
|
1099
|
+
RESOURCE_ID: {
|
|
1100
|
+
type: "STRING",
|
|
1101
|
+
defaultValue: "costume:Sprite1:costume1"
|
|
1102
|
+
},
|
|
1103
|
+
NAME: {
|
|
1104
|
+
type: "STRING",
|
|
1105
|
+
defaultValue: "asset1"
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
1108
|
+
hideFromPalette: true
|
|
1109
|
+
}, {
|
|
1110
|
+
opcode: "setLoadingBackdrop",
|
|
1111
|
+
blockType: "COMMAND",
|
|
1112
|
+
text: "set loading backdrop asset to [NAME]",
|
|
1113
|
+
description: "Configures the image asset shown behind the loading indicator.",
|
|
1114
|
+
arguments: { NAME: {
|
|
1115
|
+
type: "STRING",
|
|
1116
|
+
defaultValue: "loadingBackdrop"
|
|
1117
|
+
} },
|
|
1118
|
+
hideFromPalette: true
|
|
1119
|
+
}, {
|
|
1120
|
+
opcode: "setLoadingCostumes",
|
|
1121
|
+
blockType: "COMMAND",
|
|
1122
|
+
text: "set loading costume assets to [NAMES]",
|
|
1123
|
+
description: "Configures the comma-separated image assets used by the loading indicator.",
|
|
1124
|
+
arguments: { NAMES: {
|
|
1125
|
+
type: "STRING",
|
|
1126
|
+
defaultValue: "loading1,loading2"
|
|
1127
|
+
} },
|
|
1128
|
+
hideFromPalette: true
|
|
1129
|
+
}, {
|
|
1130
|
+
opcode: "prepareLoadingAssets",
|
|
1131
|
+
blockType: "COMMAND",
|
|
1132
|
+
text: "prioritize loading assets in list [LIST]",
|
|
1133
|
+
description: "Moves configured loading assets to the front of the named asset definition list.",
|
|
1134
|
+
arguments: { LIST: {
|
|
1135
|
+
type: "STRING",
|
|
1136
|
+
defaultValue: "assetList"
|
|
1137
|
+
} },
|
|
1138
|
+
hideFromPalette: true
|
|
1139
|
+
}, {
|
|
1140
|
+
opcode: "loadingAssetCount",
|
|
1141
|
+
blockType: "REPORTER",
|
|
1142
|
+
text: "loading asset count",
|
|
1143
|
+
description: "Returns the number of configured loading assets present in the prepared asset list.",
|
|
1144
|
+
arguments: {},
|
|
1145
|
+
hideFromPalette: true
|
|
1146
|
+
}, {
|
|
1147
|
+
opcode: "loadingBackdrop",
|
|
1148
|
+
blockType: "REPORTER",
|
|
1149
|
+
text: "loading backdrop asset",
|
|
1150
|
+
description: "Returns the configured loading backdrop asset name.",
|
|
1151
|
+
arguments: {},
|
|
1152
|
+
hideFromPalette: true
|
|
1153
|
+
}, {
|
|
1154
|
+
opcode: "loadingCostumeAt",
|
|
1155
|
+
blockType: "REPORTER",
|
|
1156
|
+
text: "loading costume for asset number [INDEX]",
|
|
1157
|
+
description: "Returns the configured loading costume asset for a one-based regular asset number.",
|
|
1158
|
+
arguments: { INDEX: {
|
|
1159
|
+
type: "NUMBER",
|
|
1160
|
+
defaultValue: "1"
|
|
1161
|
+
} },
|
|
1162
|
+
hideFromPalette: true
|
|
1163
|
+
});
|
|
1164
|
+
function normalizeName(value) {
|
|
1165
|
+
return String(value ?? "").trim();
|
|
1166
|
+
}
|
|
1167
|
+
function literalProjectName(value, label) {
|
|
1168
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`${label} must be a non-empty literal string.`);
|
|
1169
|
+
return value;
|
|
1170
|
+
}
|
|
1171
|
+
function parseProjectAssetLocator(value) {
|
|
1172
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Project asset locator must be an object.");
|
|
1173
|
+
const input = value;
|
|
1174
|
+
const kind = input.kind;
|
|
1175
|
+
const allowed = kind === "costume" ? /* @__PURE__ */ new Set([
|
|
1176
|
+
"kind",
|
|
1177
|
+
"name",
|
|
1178
|
+
"target"
|
|
1179
|
+
]) : kind === "sound" ? /* @__PURE__ */ new Set([
|
|
1180
|
+
"kind",
|
|
1181
|
+
"name",
|
|
1182
|
+
"target"
|
|
1183
|
+
]) : /* @__PURE__ */ new Set(["kind", "name"]);
|
|
1184
|
+
if (Object.keys(input).some((key) => !allowed.has(key))) throw new Error("Project asset locator contains an unknown field.");
|
|
1185
|
+
const name = literalProjectName(input.name, "Project asset source name");
|
|
1186
|
+
if (kind === "backdrop") {
|
|
1187
|
+
if (Object.hasOwn(input, "target")) throw new Error("Backdrop locator must not provide target.");
|
|
1188
|
+
return Object.freeze({
|
|
1189
|
+
kind,
|
|
1190
|
+
name
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
if (kind === "costume") return Object.freeze({
|
|
1194
|
+
kind,
|
|
1195
|
+
name,
|
|
1196
|
+
target: literalProjectName(input.target, "Costume target name")
|
|
1197
|
+
});
|
|
1198
|
+
if (kind === "sound") return Object.freeze({
|
|
1199
|
+
kind,
|
|
1200
|
+
name,
|
|
1201
|
+
...Object.hasOwn(input, "target") ? { target: literalProjectName(input.target, "Sound target name") } : {}
|
|
1202
|
+
});
|
|
1203
|
+
throw new Error("Project asset locator kind must be backdrop, costume, or sound.");
|
|
1204
|
+
}
|
|
1205
|
+
function guessMimeType(value) {
|
|
1206
|
+
const name = String(value ?? "").toLowerCase().split("?")[0]?.split("#")[0] ?? "";
|
|
1207
|
+
return [
|
|
1208
|
+
[[".svg"], "image/svg+xml"],
|
|
1209
|
+
[[".png"], "image/png"],
|
|
1210
|
+
[[".jpg", ".jpeg"], "image/jpeg"],
|
|
1211
|
+
[[".webp"], "image/webp"],
|
|
1212
|
+
[[".gif"], "image/gif"],
|
|
1213
|
+
[[".mp3"], "audio/mpeg"],
|
|
1214
|
+
[[".wav"], "audio/wav"],
|
|
1215
|
+
[[".ogg"], "audio/ogg"],
|
|
1216
|
+
[[".m4a"], "audio/mp4"],
|
|
1217
|
+
[[".aac"], "audio/aac"]
|
|
1218
|
+
].find(([extensions]) => extensions.some((extension) => name.endsWith(extension)))?.[1] ?? "application/octet-stream";
|
|
1219
|
+
}
|
|
1220
|
+
function normalizeMimeType(mimeType, urlOrName) {
|
|
1221
|
+
const raw = String(mimeType ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
|
|
1222
|
+
return !raw || raw === "application/octet-stream" || raw === "binary/octet-stream" ? guessMimeType(urlOrName) : raw;
|
|
1223
|
+
}
|
|
1224
|
+
function parseResourceIdentifier(value, fallbackAssetName) {
|
|
1225
|
+
const resourceId = normalizeName(value);
|
|
1226
|
+
if (!resourceId) return { kind: "cache" };
|
|
1227
|
+
if (/^https?:\/\//i.test(resourceId)) return {
|
|
1228
|
+
kind: "external",
|
|
1229
|
+
url: resourceId
|
|
1230
|
+
};
|
|
1231
|
+
const separatorIndex = resourceId.indexOf(":");
|
|
1232
|
+
if (separatorIndex < 0) {
|
|
1233
|
+
const fallbackName = normalizeName(fallbackAssetName);
|
|
1234
|
+
const bareScheme = resourceId.toLowerCase();
|
|
1235
|
+
if (bareScheme === "costume" && fallbackName) return {
|
|
1236
|
+
kind: "costume",
|
|
1237
|
+
spriteName: fallbackName,
|
|
1238
|
+
costumeName: null
|
|
1239
|
+
};
|
|
1240
|
+
if (bareScheme === "backdrop" && fallbackName) return {
|
|
1241
|
+
kind: "backdrop",
|
|
1242
|
+
backdropName: fallbackName
|
|
1243
|
+
};
|
|
1244
|
+
if (bareScheme === "sound" && fallbackName) return {
|
|
1245
|
+
kind: "sound",
|
|
1246
|
+
spriteName: STAGE_RESOURCE_NAME,
|
|
1247
|
+
soundName: fallbackName
|
|
1248
|
+
};
|
|
1249
|
+
if (bareScheme === "text" && fallbackName) return {
|
|
1250
|
+
kind: "text",
|
|
1251
|
+
runtimeVariableName: textRuntimeVariableName(parseLocalResourceName(fallbackName, "Text variable"))
|
|
1252
|
+
};
|
|
1253
|
+
throw new Error(`Unsupported resource identifier: ${resourceId}`);
|
|
1254
|
+
}
|
|
1255
|
+
const scheme = resourceId.slice(0, separatorIndex).trim().toLowerCase();
|
|
1256
|
+
const payload = resourceId.slice(separatorIndex + 1).trim();
|
|
1257
|
+
switch (scheme) {
|
|
1258
|
+
case "costume": {
|
|
1259
|
+
const [spriteName, costumeName] = splitLocalResourcePair(payload, "costume", fallbackAssetName);
|
|
1260
|
+
return {
|
|
1261
|
+
kind: "costume",
|
|
1262
|
+
spriteName,
|
|
1263
|
+
costumeName
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
case "backdrop": return {
|
|
1267
|
+
kind: "backdrop",
|
|
1268
|
+
backdropName: parseLocalResourceName(payload, "Backdrop")
|
|
1269
|
+
};
|
|
1270
|
+
case "sound": {
|
|
1271
|
+
const [spriteName, soundName] = splitLocalResourcePair(payload, "sound", fallbackAssetName);
|
|
1272
|
+
return {
|
|
1273
|
+
kind: "sound",
|
|
1274
|
+
spriteName,
|
|
1275
|
+
soundName
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
case "text": return {
|
|
1279
|
+
kind: "text",
|
|
1280
|
+
runtimeVariableName: textRuntimeVariableName(parseLocalResourceName(payload, "Text variable"))
|
|
1281
|
+
};
|
|
1282
|
+
default: throw new Error(`Unsupported resource scheme: ${scheme}`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
function splitLocalResourcePair(payload, scheme, fallbackAssetName) {
|
|
1286
|
+
if (!payload.includes(":") && fallbackAssetName !== void 0) {
|
|
1287
|
+
const spriteName = payload.trim();
|
|
1288
|
+
const assetName = normalizeName(fallbackAssetName);
|
|
1289
|
+
if (!spriteName) throw new Error(`${scheme} source name is empty.`);
|
|
1290
|
+
if (!assetName) throw new Error(`${scheme} asset name is empty.`);
|
|
1291
|
+
return [spriteName, assetName];
|
|
1292
|
+
}
|
|
1293
|
+
const parts = payload.split(":");
|
|
1294
|
+
if (parts.length !== 2) throw new Error(`${scheme} resource must specify a source and asset name separated by exactly one colon.`);
|
|
1295
|
+
const sourceName = parts[0]?.trim() ?? "";
|
|
1296
|
+
const assetName = parts[1]?.trim() ?? "";
|
|
1297
|
+
if (!sourceName) throw new Error(`${scheme} source name is empty.`);
|
|
1298
|
+
if (!assetName) throw new Error(`${scheme} asset name is empty.`);
|
|
1299
|
+
return [sourceName, assetName];
|
|
1300
|
+
}
|
|
1301
|
+
function parseLocalResourceName(payload, label) {
|
|
1302
|
+
const name = payload.trim();
|
|
1303
|
+
if (!name) throw new Error(`${label} name is empty.`);
|
|
1304
|
+
if (name.includes(":")) throw new Error(`${label} name must not contain a colon.`);
|
|
1305
|
+
return name;
|
|
1306
|
+
}
|
|
1307
|
+
function requireAssetNameValue(value, operation = "registerAsset") {
|
|
1308
|
+
const name = normalizeName(value);
|
|
1309
|
+
if (!name) throw new AssetManagerError("INVALID_ASSET_NAME", "Asset name is empty.", {
|
|
1310
|
+
operation,
|
|
1311
|
+
assetName: name,
|
|
1312
|
+
hint: "Provide a non-empty asset name."
|
|
1313
|
+
});
|
|
1314
|
+
return name;
|
|
1315
|
+
}
|
|
1316
|
+
function requireTextAssetNameValue(value, operation = "registerAsset") {
|
|
1317
|
+
const name = requireAssetNameValue(value, operation);
|
|
1318
|
+
if (name.includes(":")) throw new AssetManagerError("INVALID_ASSET_NAME", `Text asset name "${name}" must not contain a colon.`, {
|
|
1319
|
+
operation,
|
|
1320
|
+
assetName: name,
|
|
1321
|
+
hint: "Use a logical name without a colon."
|
|
1322
|
+
});
|
|
1323
|
+
return name;
|
|
1324
|
+
}
|
|
1325
|
+
function copyEmbeddedBytes(value, assetName) {
|
|
1326
|
+
let bytes;
|
|
1327
|
+
if (value instanceof ArrayBuffer) bytes = new Uint8Array(value);
|
|
1328
|
+
else if (value instanceof Uint8Array) bytes = value;
|
|
1329
|
+
else throw new AssetManagerError("RESOURCE_ID_INVALID", `Embedded asset "${assetName}" must provide an ArrayBuffer or Uint8Array.`, {
|
|
1330
|
+
operation: "registerEmbeddedAsset",
|
|
1331
|
+
assetName,
|
|
1332
|
+
hint: "Pass validated binary image or audio bytes."
|
|
1333
|
+
});
|
|
1334
|
+
if (bytes.byteLength === 0) throw new AssetManagerError("RESOURCE_ID_INVALID", `Embedded asset "${assetName}" is empty.`, {
|
|
1335
|
+
operation: "registerEmbeddedAsset",
|
|
1336
|
+
assetName,
|
|
1337
|
+
hint: "Pass at least one byte."
|
|
1338
|
+
});
|
|
1339
|
+
return Uint8Array.from(bytes).buffer;
|
|
1340
|
+
}
|
|
1341
|
+
function embeddedBitmapResolution(value, mimeType, assetName) {
|
|
1342
|
+
if (value === void 0) return 1;
|
|
1343
|
+
if (!mimeType.startsWith("image/") || mimeType === "image/svg+xml") throw new AssetManagerError("ASSET_TYPE_MISMATCH", `Embedded asset "${assetName}" can specify bitmapResolution only for bitmap images.`, {
|
|
1344
|
+
operation: "registerEmbeddedAsset",
|
|
1345
|
+
assetName,
|
|
1346
|
+
expectedKind: "bitmap image",
|
|
1347
|
+
actualKind: mimeType,
|
|
1348
|
+
hint: "Remove bitmapResolution or use a bitmap image MIME type."
|
|
1349
|
+
});
|
|
1350
|
+
if (value !== 1 && value !== 2) throw new AssetManagerError("RESOURCE_ID_INVALID", `Embedded bitmap asset "${assetName}" must use bitmapResolution 1 or 2.`, {
|
|
1351
|
+
operation: "registerEmbeddedAsset",
|
|
1352
|
+
assetName,
|
|
1353
|
+
hint: "Use Scratch bitmap resolution 1 or 2."
|
|
1354
|
+
});
|
|
1355
|
+
return value;
|
|
1356
|
+
}
|
|
1357
|
+
function findStageTarget(runtime) {
|
|
1358
|
+
const stage = runtime.targets.find((target) => target.isStage);
|
|
1359
|
+
if (!stage) throw new AssetManagerError("SPRITE_NOT_FOUND", "Stage target was not found.", {
|
|
1360
|
+
operation: "resolveStage",
|
|
1361
|
+
actorName: STAGE_RESOURCE_NAME,
|
|
1362
|
+
hint: "Load a project with a valid stage target."
|
|
1363
|
+
});
|
|
1364
|
+
return stage;
|
|
1365
|
+
}
|
|
1366
|
+
function findProjectTargetByName(runtime, name) {
|
|
1367
|
+
return runtime.targets.find((target) => !target.isStage && target.isOriginal && target.sprite?.name === name) ?? runtime.targets.find((target) => !target.isStage && target.sprite?.name === name) ?? null;
|
|
1368
|
+
}
|
|
1369
|
+
function findProjectCostume(target, costumeName, assetId) {
|
|
1370
|
+
const costumes = target.sprite?.costumes ?? [];
|
|
1371
|
+
return (assetId ? costumes.find((costume) => costume.assetId === assetId) : void 0) ?? costumes.find((costume) => costume.name === costumeName) ?? null;
|
|
1372
|
+
}
|
|
1373
|
+
function findProjectSound(target, soundName, assetId) {
|
|
1374
|
+
const sounds = target.sprite?.sounds ?? [];
|
|
1375
|
+
return (assetId ? sounds.find((sound) => sound.assetId === assetId) : void 0) ?? sounds.find((sound) => sound.name === soundName) ?? null;
|
|
1376
|
+
}
|
|
1377
|
+
function resolveCostumeAddress(runtime, name, spriteName, costumeName) {
|
|
1378
|
+
const target = findProjectTargetByName(runtime, spriteName);
|
|
1379
|
+
if (!target) {
|
|
1380
|
+
const candidates = suggestNames(spriteName, runtime.targets.flatMap((candidate) => !candidate.isStage && candidate.sprite?.name ? [candidate.sprite.name] : []));
|
|
1381
|
+
throw new AssetManagerError("SPRITE_NOT_FOUND", `Sprite not found: ${spriteName}.`, {
|
|
1382
|
+
operation: "registerAsset",
|
|
1383
|
+
assetName: name,
|
|
1384
|
+
actorName: spriteName,
|
|
1385
|
+
candidates,
|
|
1386
|
+
hint: suggestionHint(candidates)
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
const costumes = target.sprite?.costumes ?? [];
|
|
1390
|
+
const costume = costumeName === null ? costumes.find((candidate) => candidate.name === name) ?? (costumes.length === 1 ? costumes[0] : null) : findProjectCostume(target, costumeName, null);
|
|
1391
|
+
if (!costume && costumeName === null && costumes.length > 1) {
|
|
1392
|
+
const candidates = suggestNames(name, costumes.map((candidate) => candidate.name));
|
|
1393
|
+
throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", `Costume shorthand is ambiguous: ${spriteName} has multiple costumes and none is named ${name}.`, {
|
|
1394
|
+
operation: "registerAsset",
|
|
1395
|
+
assetName: name,
|
|
1396
|
+
actorName: spriteName,
|
|
1397
|
+
candidates,
|
|
1398
|
+
hint: suggestionHint(candidates) ?? "Specify the costume name explicitly."
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
const resolvedCostumeName = costume?.name ?? costumeName ?? name;
|
|
1402
|
+
if (!costume) {
|
|
1403
|
+
const candidates = suggestNames(resolvedCostumeName, costumes.map((candidate) => candidate.name));
|
|
1404
|
+
throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", `Costume not found: ${spriteName}/${resolvedCostumeName}.`, {
|
|
1405
|
+
operation: "registerAsset",
|
|
1406
|
+
assetName: name,
|
|
1407
|
+
actorName: spriteName,
|
|
1408
|
+
candidates,
|
|
1409
|
+
hint: suggestionHint(candidates)
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
return {
|
|
1413
|
+
target,
|
|
1414
|
+
costume,
|
|
1415
|
+
costumeName: resolvedCostumeName
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
function resolveBackdropAddress(runtime, backdropName) {
|
|
1419
|
+
const target = findStageTarget(runtime);
|
|
1420
|
+
const costume = findProjectCostume(target, backdropName, null);
|
|
1421
|
+
if (!costume) {
|
|
1422
|
+
const candidates = suggestNames(backdropName, (target.sprite?.costumes ?? []).map((candidate) => candidate.name));
|
|
1423
|
+
throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", `Backdrop not found: ${backdropName}.`, {
|
|
1424
|
+
operation: "registerAsset",
|
|
1425
|
+
assetName: backdropName,
|
|
1426
|
+
actorName: STAGE_RESOURCE_NAME,
|
|
1427
|
+
candidates,
|
|
1428
|
+
hint: suggestionHint(candidates)
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
return {
|
|
1432
|
+
target,
|
|
1433
|
+
costume
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
function resolveSoundAddress(runtime, spriteName, soundName, assetName = soundName) {
|
|
1437
|
+
const isStage = spriteName.toLowerCase() === STAGE_RESOURCE_NAME;
|
|
1438
|
+
const target = isStage ? findStageTarget(runtime) : findProjectTargetByName(runtime, spriteName);
|
|
1439
|
+
if (!target) {
|
|
1440
|
+
const candidates = suggestNames(spriteName, runtime.targets.flatMap((candidate) => candidate.sprite?.name ? [candidate.sprite.name] : []));
|
|
1441
|
+
throw new AssetManagerError("SPRITE_NOT_FOUND", `Sound source not found: ${spriteName}.`, {
|
|
1442
|
+
operation: "registerAsset",
|
|
1443
|
+
assetName,
|
|
1444
|
+
actorName: spriteName,
|
|
1445
|
+
candidates,
|
|
1446
|
+
hint: suggestionHint(candidates)
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
const sound = findProjectSound(target, soundName, null);
|
|
1450
|
+
if (!sound) {
|
|
1451
|
+
const candidates = suggestNames(soundName, (target.sprite?.sounds ?? []).map((candidate) => candidate.name));
|
|
1452
|
+
throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", `Sound not found: ${spriteName}/${soundName}.`, {
|
|
1453
|
+
operation: "registerAsset",
|
|
1454
|
+
assetName,
|
|
1455
|
+
actorName: spriteName,
|
|
1456
|
+
candidates,
|
|
1457
|
+
hint: suggestionHint(candidates)
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
return {
|
|
1461
|
+
target,
|
|
1462
|
+
sound,
|
|
1463
|
+
isStage
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function resolveLiteralSoundAddress(runtime, targetName, soundName, assetName) {
|
|
1467
|
+
const isStage = targetName === void 0;
|
|
1468
|
+
const target = isStage ? findStageTarget(runtime) : findProjectTargetByName(runtime, targetName);
|
|
1469
|
+
if (!target) {
|
|
1470
|
+
const candidates = suggestNames(targetName ?? STAGE_RESOURCE_NAME, runtime.targets.flatMap((candidate) => !candidate.isStage && candidate.sprite?.name ? [candidate.sprite.name] : []));
|
|
1471
|
+
throw new AssetManagerError("SPRITE_NOT_FOUND", "Structured sound target was not found.", {
|
|
1472
|
+
operation: "registerProjectAsset",
|
|
1473
|
+
assetName,
|
|
1474
|
+
actorName: targetName,
|
|
1475
|
+
candidates,
|
|
1476
|
+
hint: suggestionHint(candidates)
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
const sound = findProjectSound(target, soundName, null);
|
|
1480
|
+
if (!sound) {
|
|
1481
|
+
const candidates = suggestNames(soundName, (target.sprite?.sounds ?? []).map((candidate) => candidate.name));
|
|
1482
|
+
throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", "Structured project sound was not found.", {
|
|
1483
|
+
operation: "registerProjectAsset",
|
|
1484
|
+
assetName,
|
|
1485
|
+
actorName: targetName,
|
|
1486
|
+
candidates,
|
|
1487
|
+
hint: suggestionHint(candidates)
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
return {
|
|
1491
|
+
target,
|
|
1492
|
+
sound,
|
|
1493
|
+
isStage,
|
|
1494
|
+
targetName: targetName ?? STAGE_RESOURCE_NAME
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
function validateProjectAssetAddress(runtime, assetName, resourceIdentifier) {
|
|
1498
|
+
let fallbackType = "asset-name";
|
|
1499
|
+
let fallbackLabel = normalizeName(assetName);
|
|
1500
|
+
try {
|
|
1501
|
+
const name = requireAssetNameValue(assetName);
|
|
1502
|
+
const resourceId = normalizeName(resourceIdentifier);
|
|
1503
|
+
if (resourceId === "text" || resourceId.startsWith("text:")) requireTextAssetNameValue(name);
|
|
1504
|
+
fallbackType = "resource-id";
|
|
1505
|
+
fallbackLabel = resourceId;
|
|
1506
|
+
const resource = parseResourceIdentifier(resourceIdentifier, name);
|
|
1507
|
+
switch (resource.kind) {
|
|
1508
|
+
case "costume":
|
|
1509
|
+
resolveCostumeAddress(runtime, name, resource.spriteName, resource.costumeName);
|
|
1510
|
+
break;
|
|
1511
|
+
case "backdrop":
|
|
1512
|
+
resolveBackdropAddress(runtime, resource.backdropName);
|
|
1513
|
+
break;
|
|
1514
|
+
case "sound":
|
|
1515
|
+
resolveSoundAddress(runtime, resource.spriteName, resource.soundName, name);
|
|
1516
|
+
break;
|
|
1517
|
+
case "text": requireTextAssetNameValue(name);
|
|
1518
|
+
}
|
|
1519
|
+
return {
|
|
1520
|
+
ok: true,
|
|
1521
|
+
kind: resource.kind,
|
|
1522
|
+
projectLocal: resource.kind !== "cache" && resource.kind !== "external"
|
|
1523
|
+
};
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
return {
|
|
1526
|
+
ok: false,
|
|
1527
|
+
type: error instanceof AssetManagerError ? error.code : fallbackType === "asset-name" ? "INVALID_ASSET_NAME" : "RESOURCE_ID_INVALID",
|
|
1528
|
+
label: error instanceof AssetManagerError ? error.code === "SPRITE_NOT_FOUND" ? error.actorName ?? error.assetName ?? fallbackLabel : error.code === "RESOURCE_ID_INVALID" ? error.resourceId ?? error.assetName ?? fallbackLabel : error.assetName ?? error.actorName ?? error.resourceId ?? fallbackLabel : fallbackLabel,
|
|
1529
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
var AssetManagerExtension = class {
|
|
1534
|
+
constructor(featureFlags = FEATURE_FLAGS) {
|
|
1535
|
+
_defineProperty(this, "runtime", Scratch.vm.runtime);
|
|
1536
|
+
_defineProperty(this, "renderer", this.runtime.renderer);
|
|
1537
|
+
_defineProperty(this, "externalAssets", /* @__PURE__ */ new Map());
|
|
1538
|
+
_defineProperty(this, "costumeAssets", /* @__PURE__ */ new Map());
|
|
1539
|
+
_defineProperty(this, "soundAssets", /* @__PURE__ */ new Map());
|
|
1540
|
+
_defineProperty(this, "textAssets", /* @__PURE__ */ new Map());
|
|
1541
|
+
_defineProperty(this, "assetRegistry", /* @__PURE__ */ new Map());
|
|
1542
|
+
_defineProperty(this, "displayedAssets", /* @__PURE__ */ new Map());
|
|
1543
|
+
_defineProperty(this, "playingAudio", /* @__PURE__ */ new Map());
|
|
1544
|
+
_defineProperty(this, "audioVoiceStops", /* @__PURE__ */ new WeakMap());
|
|
1545
|
+
_defineProperty(this, "registrationVersions", /* @__PURE__ */ new Map());
|
|
1546
|
+
_defineProperty(this, "successfulRegistrationVersions", /* @__PURE__ */ new Map());
|
|
1547
|
+
_defineProperty(this, "registrationCancellationVersions", /* @__PURE__ */ new Map());
|
|
1548
|
+
_defineProperty(this, "registrationCommits", /* @__PURE__ */ new Map());
|
|
1549
|
+
_defineProperty(this, "committedCacheRecords", /* @__PURE__ */ new Map());
|
|
1550
|
+
_defineProperty(this, "featureFlags", void 0);
|
|
1551
|
+
_defineProperty(this, "loadingBackdropName", "");
|
|
1552
|
+
_defineProperty(this, "lastAssetErrorType", "");
|
|
1553
|
+
_defineProperty(this, "lastAssetErrorLabel", "");
|
|
1554
|
+
_defineProperty(this, "assetErrorVersion", 0);
|
|
1555
|
+
_defineProperty(this, "domImageResourceVersions", /* @__PURE__ */ new Map());
|
|
1556
|
+
_defineProperty(this, "domImageResourceBackings", /* @__PURE__ */ new Map());
|
|
1557
|
+
_defineProperty(this, "activeDOMImageResources", /* @__PURE__ */ new Set());
|
|
1558
|
+
_defineProperty(this, "activeDOMImageResourcesByName", /* @__PURE__ */ new Map());
|
|
1559
|
+
_defineProperty(this, "domImageCapabilityValue", void 0);
|
|
1560
|
+
_defineProperty(this, "namedBodyProviderValue", void 0);
|
|
1561
|
+
_defineProperty(this, "namedBodyProviderRegistration", void 0);
|
|
1562
|
+
_defineProperty(this, "unbindNamedDataRegistryLifecycle", void 0);
|
|
1563
|
+
_defineProperty(this, "openNamedBodySnapshots", /* @__PURE__ */ new Set());
|
|
1564
|
+
_defineProperty(this, "namedBodyLifecycleVersion", 0);
|
|
1565
|
+
_defineProperty(this, "listeningForNamedBodyLifecycle", false);
|
|
1566
|
+
_defineProperty(this, "listeningForDOMImageLifecycle", false);
|
|
1567
|
+
_defineProperty(this, "releaseAllDOMImageResourcesForLifecycle", () => {
|
|
1568
|
+
this.releaseAllDOMImageResources();
|
|
1569
|
+
});
|
|
1570
|
+
_defineProperty(this, "releaseDOMImageResourcesForRuntimeDispose", () => {
|
|
1571
|
+
this.releaseAllDOMImageResources();
|
|
1572
|
+
this.stopListeningForDOMImageLifecycle();
|
|
1573
|
+
});
|
|
1574
|
+
_defineProperty(this, "releaseNamedBodySnapshotsForLifecycle", () => {
|
|
1575
|
+
this.releaseOpenNamedBodySnapshots();
|
|
1576
|
+
});
|
|
1577
|
+
_defineProperty(this, "unregisterNamedBodyProviderForRuntimeDispose", () => {
|
|
1578
|
+
this.namedBodyProviderValue?.release("shutdown");
|
|
1579
|
+
this.namedBodyProviderRegistration?.unregister();
|
|
1580
|
+
this.namedBodyProviderRegistration = void 0;
|
|
1581
|
+
this.unbindNamedDataRegistryLifecycle?.();
|
|
1582
|
+
this.unbindNamedDataRegistryLifecycle = void 0;
|
|
1583
|
+
});
|
|
1584
|
+
this.featureFlags = Object.freeze({ ...featureFlags });
|
|
1585
|
+
if (this.featureFlags.NAMED_ASSET_BODY_PROVIDER) {
|
|
1586
|
+
this.namedBodyProviderValue = this.createNamedBodyProvider();
|
|
1587
|
+
const registry = installNamedDataRegistry(this.runtime);
|
|
1588
|
+
this.namedBodyProviderRegistration = registry.registerProvider(this.namedBodyProviderValue, { lifetime: "persistent" });
|
|
1589
|
+
this.unbindNamedDataRegistryLifecycle = bindNamedDataRegistryLifecycle(this.runtime, registry);
|
|
1590
|
+
this.startListeningForNamedBodyLifecycle();
|
|
1591
|
+
}
|
|
1592
|
+
this.runtime.on?.("STOP_FOR_TARGET", (target) => {
|
|
1593
|
+
if (target && !this.runtime.targets.includes(target)) this.displayedAssets.delete(target.id);
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
setLoadingBackdrop(args) {
|
|
1597
|
+
this.loadingBackdropName = normalizeName(args.NAME);
|
|
1598
|
+
this.loadingAssetCountValue = 0;
|
|
1599
|
+
}
|
|
1600
|
+
setLoadingCostumes(args) {
|
|
1601
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1602
|
+
this.loadingCostumes = String(args.NAMES ?? "").split(",").map((name) => normalizeName(name)).filter((name) => {
|
|
1603
|
+
if (!name || seen.has(name)) return false;
|
|
1604
|
+
seen.add(name);
|
|
1605
|
+
return true;
|
|
1606
|
+
});
|
|
1607
|
+
this.loadingAssetCountValue = 0;
|
|
1608
|
+
}
|
|
1609
|
+
prepareLoadingAssets(args, util) {
|
|
1610
|
+
const listName = normalizeName(args.LIST);
|
|
1611
|
+
const list = util.target?.lookupVariableByNameAndType?.(listName, "list");
|
|
1612
|
+
if (!list || !Array.isArray(list.value)) throw new Error(`Loading asset list not found: ${listName || "(empty)"}`);
|
|
1613
|
+
const loadingBackdrop = this.loadingBackdropName;
|
|
1614
|
+
const loadingCostumes = this.loadingCostumes ?? [];
|
|
1615
|
+
const loadingCostumeNames = new Set(loadingCostumes);
|
|
1616
|
+
const loadingNames = new Set([loadingBackdrop, ...loadingCostumes].filter((name) => name.length > 0));
|
|
1617
|
+
const entries = list.value.map((entry) => String(entry));
|
|
1618
|
+
const declaredNames = new Set(entries.map((entry) => {
|
|
1619
|
+
const separatorIndex = entry.indexOf(",");
|
|
1620
|
+
return normalizeName(separatorIndex < 0 ? entry : entry.slice(0, separatorIndex));
|
|
1621
|
+
}));
|
|
1622
|
+
const missingNames = [...loadingNames].filter((name) => !declaredNames.has(name));
|
|
1623
|
+
if (missingNames.length > 0) throw new Error(`Loading asset is not declared: ${missingNames.join(", ")}`);
|
|
1624
|
+
const backdropEntries = [];
|
|
1625
|
+
const prioritized = [];
|
|
1626
|
+
const regular = [];
|
|
1627
|
+
for (const entry of entries) {
|
|
1628
|
+
const separatorIndex = entry.indexOf(",");
|
|
1629
|
+
const assetName = normalizeName(separatorIndex < 0 ? entry : entry.slice(0, separatorIndex));
|
|
1630
|
+
if (loadingBackdrop && assetName === loadingBackdrop) backdropEntries.push(entry);
|
|
1631
|
+
else (loadingCostumeNames.has(assetName) ? prioritized : regular).push(entry);
|
|
1632
|
+
}
|
|
1633
|
+
list.value.splice(0, list.value.length, ...backdropEntries, ...prioritized, ...regular);
|
|
1634
|
+
this.loadingAssetCountValue = backdropEntries.length + prioritized.length;
|
|
1635
|
+
}
|
|
1636
|
+
loadingAssetCount() {
|
|
1637
|
+
return this.loadingAssetCountValue ?? 0;
|
|
1638
|
+
}
|
|
1639
|
+
loadingBackdrop() {
|
|
1640
|
+
return this.loadingBackdropName;
|
|
1641
|
+
}
|
|
1642
|
+
loadingCostumeAt(args) {
|
|
1643
|
+
const loadingCostumes = this.loadingCostumes ?? [];
|
|
1644
|
+
if (loadingCostumes.length === 0) return "";
|
|
1645
|
+
const numericIndex = Number(args.INDEX);
|
|
1646
|
+
return loadingCostumes[((Number.isFinite(numericIndex) ? Math.max(1, Math.trunc(numericIndex)) : 1) - 1) % loadingCostumes.length];
|
|
1647
|
+
}
|
|
1648
|
+
getInfo() {
|
|
1649
|
+
return {
|
|
1650
|
+
id: EXTENSION_ID,
|
|
1651
|
+
name: Scratch.translate(block_definitions_default.extensionName),
|
|
1652
|
+
docsURI: EXTENSION_DOCS_URI,
|
|
1653
|
+
blockIconURI: BLOCK_ICON_URI,
|
|
1654
|
+
color1: "#5b7cfa",
|
|
1655
|
+
color2: "#425ed8",
|
|
1656
|
+
color3: "#2f46aa",
|
|
1657
|
+
blocks: blockDefinitions.map((block) => this.toScratchBlock(block))
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Exposes the stock extension registry through a host-neutral DOM image
|
|
1662
|
+
* resource contract for other unsandboxed extensions.
|
|
1663
|
+
*/
|
|
1664
|
+
getDOMImageCapability() {
|
|
1665
|
+
this.startListeningForDOMImageLifecycle();
|
|
1666
|
+
this.domImageCapabilityValue ?? (this.domImageCapabilityValue = Object.freeze({
|
|
1667
|
+
isRegistered: (name) => this.assetRegistry.has(normalizeName(name)),
|
|
1668
|
+
getMimeType: (name) => this.getAssetMimeType({ NAME: name }),
|
|
1669
|
+
resolveDOMImageResource: (name) => this.resolveExtensionDOMImageResource(name)
|
|
1670
|
+
}));
|
|
1671
|
+
return this.domImageCapabilityValue;
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Returns the optional read-only `asset` body provider. The startup-fixed
|
|
1675
|
+
* feature flag keeps the adapter completely absent from the default path.
|
|
1676
|
+
*/
|
|
1677
|
+
getNamedBodyProvider() {
|
|
1678
|
+
if (this.featureFlags.NAMED_ASSET_BODY_PROVIDER !== true) return null;
|
|
1679
|
+
return this.namedBodyProviderValue ?? null;
|
|
1680
|
+
}
|
|
1681
|
+
/** Canonical typed entry point for the existing asset registry provider. */
|
|
1682
|
+
getNamedDataProvider() {
|
|
1683
|
+
return this.getNamedBodyProvider();
|
|
1684
|
+
}
|
|
1685
|
+
validateProjectAssetAddress(args) {
|
|
1686
|
+
return JSON.stringify(validateProjectAssetAddress(this.runtime, args.NAME, args.RESOURCE_ID));
|
|
1687
|
+
}
|
|
1688
|
+
async registerProjectAssetLiteral(assetName, locatorInput) {
|
|
1689
|
+
const name = requireAssetNameValue(assetName, "registerProjectAsset");
|
|
1690
|
+
let locator;
|
|
1691
|
+
try {
|
|
1692
|
+
locator = parseProjectAssetLocator(locatorInput);
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
throw new AssetManagerError("RESOURCE_ID_INVALID", errorMessage(error), {
|
|
1695
|
+
operation: "registerProjectAsset",
|
|
1696
|
+
assetName: name,
|
|
1697
|
+
hint: "Pass a structured backdrop, costume, or sound locator.",
|
|
1698
|
+
cause: error
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
switch (locator.kind) {
|
|
1702
|
+
case "backdrop":
|
|
1703
|
+
await this.registerBackdropReference(name, locator.name);
|
|
1704
|
+
return;
|
|
1705
|
+
case "costume":
|
|
1706
|
+
await this.registerCostumeReference(name, locator.target, locator.name);
|
|
1707
|
+
return;
|
|
1708
|
+
case "sound":
|
|
1709
|
+
await this.registerLiteralSoundReference(name, locator.target, locator.name);
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
async registerAsset(args) {
|
|
1714
|
+
const errorVersion = ++this.assetErrorVersion;
|
|
1715
|
+
this.clearAssetError();
|
|
1716
|
+
let fallbackType = "asset-name";
|
|
1717
|
+
let fallbackLabel = normalizeName(args.NAME);
|
|
1718
|
+
try {
|
|
1719
|
+
const name = this.requireAssetName(args.NAME);
|
|
1720
|
+
const resourceId = normalizeName(args.RESOURCE_ID);
|
|
1721
|
+
if (resourceId === "text" || resourceId.startsWith("text:")) this.requireTextAssetName(name);
|
|
1722
|
+
fallbackType = "resource-id";
|
|
1723
|
+
fallbackLabel = resourceId;
|
|
1724
|
+
let resource;
|
|
1725
|
+
try {
|
|
1726
|
+
resource = parseResourceIdentifier(args.RESOURCE_ID, name);
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
throw new AssetManagerError("RESOURCE_ID_INVALID", errorMessage(error), {
|
|
1729
|
+
operation: "registerAsset",
|
|
1730
|
+
assetName: name,
|
|
1731
|
+
resourceId,
|
|
1732
|
+
hint: "Use an HTTP(S) URL or a supported costume, backdrop, sound, or text resource ID.",
|
|
1733
|
+
cause: error
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
switch (resource.kind) {
|
|
1737
|
+
case "cache":
|
|
1738
|
+
fallbackType = "cache";
|
|
1739
|
+
fallbackLabel = name;
|
|
1740
|
+
await this.registerExternalAsset("", name);
|
|
1741
|
+
return;
|
|
1742
|
+
case "external":
|
|
1743
|
+
fallbackType = "external";
|
|
1744
|
+
fallbackLabel = resource.url;
|
|
1745
|
+
await this.registerExternalAsset(resource.url, name);
|
|
1746
|
+
return;
|
|
1747
|
+
case "costume":
|
|
1748
|
+
fallbackType = "costume";
|
|
1749
|
+
fallbackLabel = resource.costumeName ?? name;
|
|
1750
|
+
await this.registerCostumeReference(name, resource.spriteName, resource.costumeName);
|
|
1751
|
+
return;
|
|
1752
|
+
case "backdrop":
|
|
1753
|
+
fallbackType = "backdrop";
|
|
1754
|
+
fallbackLabel = resource.backdropName;
|
|
1755
|
+
await this.registerBackdropReference(name, resource.backdropName);
|
|
1756
|
+
return;
|
|
1757
|
+
case "sound":
|
|
1758
|
+
fallbackType = "sound";
|
|
1759
|
+
fallbackLabel = resource.soundName;
|
|
1760
|
+
await this.registerSoundReference(name, resource.spriteName, resource.soundName);
|
|
1761
|
+
return;
|
|
1762
|
+
case "text":
|
|
1763
|
+
fallbackType = "text";
|
|
1764
|
+
fallbackLabel = resource.runtimeVariableName;
|
|
1765
|
+
await this.registerTextReference(name, resource.runtimeVariableName);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
} catch (error) {
|
|
1769
|
+
const diagnostic = error instanceof AssetManagerError ? error : new AssetManagerError("REPLACEMENT_FAILED", errorMessage(error), {
|
|
1770
|
+
operation: "registerAsset",
|
|
1771
|
+
assetName: fallbackType === "asset-name" ? fallbackLabel : normalizeName(args.NAME),
|
|
1772
|
+
resourceId: normalizeName(args.RESOURCE_ID),
|
|
1773
|
+
hint: "The previous registration and display were kept. Check the resource and try again.",
|
|
1774
|
+
cause: error
|
|
1775
|
+
});
|
|
1776
|
+
if (this.assetErrorVersion === errorVersion) {
|
|
1777
|
+
if (diagnostic instanceof AssetManagerError) {
|
|
1778
|
+
this.lastAssetErrorType = diagnostic.code;
|
|
1779
|
+
this.lastAssetErrorLabel = diagnostic.code === "SPRITE_NOT_FOUND" ? diagnostic.actorName ?? diagnostic.assetName ?? fallbackLabel : diagnostic.code === "RESOURCE_ID_INVALID" ? diagnostic.resourceId ?? diagnostic.assetName ?? fallbackLabel : diagnostic.assetName ?? diagnostic.actorName ?? diagnostic.resourceId ?? fallbackLabel;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
throw diagnostic;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
async registerEmbeddedAsset(input) {
|
|
1786
|
+
const name = this.requireAssetName(input.name, "registerEmbeddedAsset");
|
|
1787
|
+
const sourceName = normalizeName(input.sourceName) || name;
|
|
1788
|
+
const mimeType = normalizeMimeType(input.mimeType, sourceName);
|
|
1789
|
+
const mediaKind = mimeType.startsWith("image/") ? "image" : mimeType.startsWith("audio/") ? "audio" : "unknown";
|
|
1790
|
+
if (mediaKind === "unknown") throw new AssetManagerError("ASSET_TYPE_MISMATCH", `Embedded asset "${name}" has unsupported MIME type ${mimeType}.`, {
|
|
1791
|
+
operation: "registerEmbeddedAsset",
|
|
1792
|
+
assetName: name,
|
|
1793
|
+
expectedKind: "image or audio",
|
|
1794
|
+
actualKind: mimeType,
|
|
1795
|
+
hint: "Use an image/* or audio/* MIME type."
|
|
1796
|
+
});
|
|
1797
|
+
const bitmapResolution = embeddedBitmapResolution(input.bitmapResolution, mimeType, name);
|
|
1798
|
+
const data = copyEmbeddedBytes(input.bytes, name);
|
|
1799
|
+
const token = this.beginRegistration(name);
|
|
1800
|
+
const prepared = {
|
|
1801
|
+
kind: "external",
|
|
1802
|
+
name,
|
|
1803
|
+
url: sourceName,
|
|
1804
|
+
mimeType,
|
|
1805
|
+
data,
|
|
1806
|
+
cachedAt: Date.now(),
|
|
1807
|
+
skinId: null,
|
|
1808
|
+
...mediaKind === "image" && mimeType !== "image/svg+xml" ? { bitmapResolution } : {}
|
|
1809
|
+
};
|
|
1810
|
+
await this.commitPreparedAsset(name, "external", prepared, token);
|
|
1811
|
+
return Object.freeze({
|
|
1812
|
+
name,
|
|
1813
|
+
mimeType
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
assetErrorType() {
|
|
1817
|
+
return this.lastAssetErrorType;
|
|
1818
|
+
}
|
|
1819
|
+
assetErrorLabel() {
|
|
1820
|
+
return this.lastAssetErrorLabel;
|
|
1821
|
+
}
|
|
1822
|
+
/** Legacy opcode retained for existing projects. */
|
|
1823
|
+
async loadAsset(args) {
|
|
1824
|
+
const name = this.requireAssetName(args.NAME, "loadAsset");
|
|
1825
|
+
const resourceId = normalizeName(args.URL);
|
|
1826
|
+
try {
|
|
1827
|
+
await this.registerExternalAsset(resourceId, name);
|
|
1828
|
+
} catch (error) {
|
|
1829
|
+
if (error instanceof AssetManagerError) throw error;
|
|
1830
|
+
throw new AssetManagerError("REPLACEMENT_FAILED", errorMessage(error), {
|
|
1831
|
+
operation: "loadAsset",
|
|
1832
|
+
assetName: name,
|
|
1833
|
+
resourceId,
|
|
1834
|
+
hint: "The previous registration and display were kept. Check the URL or cache.",
|
|
1835
|
+
cause: error
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
deleteMemoryAsset(args) {
|
|
1840
|
+
this.unregisterAsset(normalizeName(args.NAME));
|
|
1841
|
+
}
|
|
1842
|
+
deleteAllMemoryAssets() {
|
|
1843
|
+
this.releaseAllDOMImageResources();
|
|
1844
|
+
for (const name of this.registrationVersions.keys()) this.cancelRegistrations(name);
|
|
1845
|
+
for (const asset of this.externalAssets.values()) this.deleteOwnedSkinIfExists(asset);
|
|
1846
|
+
this.externalAssets.clear();
|
|
1847
|
+
this.costumeAssets.clear();
|
|
1848
|
+
this.soundAssets.clear();
|
|
1849
|
+
this.textAssets.clear();
|
|
1850
|
+
this.assetRegistry.clear();
|
|
1851
|
+
this.displayedAssets.clear();
|
|
1852
|
+
for (const audio of [...this.playingAudio.keys()]) this.stopExternalAudio(audio);
|
|
1853
|
+
this.playingAudio.clear();
|
|
1854
|
+
}
|
|
1855
|
+
async deleteCachedAsset(args) {
|
|
1856
|
+
const name = normalizeName(args.NAME);
|
|
1857
|
+
await this.cacheDelete(name);
|
|
1858
|
+
this.committedCacheRecords.set(name, null);
|
|
1859
|
+
}
|
|
1860
|
+
async deleteAllCachedAssets() {
|
|
1861
|
+
await this.cacheClear();
|
|
1862
|
+
this.committedCacheRecords.clear();
|
|
1863
|
+
}
|
|
1864
|
+
isLoaded(args) {
|
|
1865
|
+
return this.assetRegistry.has(normalizeName(args.NAME));
|
|
1866
|
+
}
|
|
1867
|
+
async setThisSpriteSkin(args, util) {
|
|
1868
|
+
if (!util.target || util.target.isStage) throw new AssetManagerError("SPRITE_NOT_FOUND", "This block must be used on a sprite or its clone.", {
|
|
1869
|
+
operation: "setThisSpriteSkin",
|
|
1870
|
+
hint: "Run this block from a sprite target."
|
|
1871
|
+
});
|
|
1872
|
+
await this.applyAssetToTarget(util.target, args.NAME, util);
|
|
1873
|
+
}
|
|
1874
|
+
async setSpriteSkin(args, util) {
|
|
1875
|
+
const name = normalizeName(args.SPRITE);
|
|
1876
|
+
const target = this.findTargetByName(name);
|
|
1877
|
+
if (!target) {
|
|
1878
|
+
const candidates = suggestNames(name, this.runtime.targets.flatMap((candidate) => !candidate.isStage && candidate.sprite?.name ? [candidate.sprite.name] : []));
|
|
1879
|
+
throw new AssetManagerError("SPRITE_NOT_FOUND", `Sprite not found: ${name}.`, {
|
|
1880
|
+
operation: "setSpriteSkin",
|
|
1881
|
+
actorName: name,
|
|
1882
|
+
candidates,
|
|
1883
|
+
hint: suggestionHint(candidates)
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
await this.applyAssetToTarget(target, args.NAME, util);
|
|
1887
|
+
}
|
|
1888
|
+
async setStageSkin(args) {
|
|
1889
|
+
await this.applyAssetToTarget(this.getStageTarget(), args.NAME);
|
|
1890
|
+
}
|
|
1891
|
+
async playSound(args) {
|
|
1892
|
+
await this.playResolvedSound(args.NAME, false);
|
|
1893
|
+
}
|
|
1894
|
+
async playSoundUntilDone(args) {
|
|
1895
|
+
await this.playResolvedSound(args.NAME, true);
|
|
1896
|
+
}
|
|
1897
|
+
async createAudioVoice(value, options = {}) {
|
|
1898
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) throw new TypeError("Audio voice options must be an object.");
|
|
1899
|
+
if (Object.keys(options).some((key) => key !== "gain")) throw new TypeError("Audio voice options contain an unknown property.");
|
|
1900
|
+
const gain = normalizeAudioVoiceGain(options.gain, "Audio voice initial gain");
|
|
1901
|
+
const name = normalizeName(value);
|
|
1902
|
+
const resource = await this.resolveAudioBytes(name);
|
|
1903
|
+
const playback = this.startBrowserAudioVoice(name, resource.bytes, resource.mimeType, gain, "createAudioVoice");
|
|
1904
|
+
await playback.started;
|
|
1905
|
+
return playback.voice;
|
|
1906
|
+
}
|
|
1907
|
+
stopSound(args) {
|
|
1908
|
+
const name = normalizeName(args.NAME);
|
|
1909
|
+
const kind = this.assetRegistry.get(name);
|
|
1910
|
+
if (!kind) throw this.assetNotRegistered("stopSound", name);
|
|
1911
|
+
if (kind === "external") {
|
|
1912
|
+
const asset = this.externalAssets.get(name);
|
|
1913
|
+
if (!asset) throw this.assetNotRegistered("stopSound", name);
|
|
1914
|
+
asset.mimeType = normalizeMimeType(asset.mimeType, asset.url || name);
|
|
1915
|
+
if (!asset.mimeType.startsWith("audio/")) throw this.assetTypeMismatch("stopSound", name, "audio", `external/${this.externalMediaKind(asset)}`);
|
|
1916
|
+
for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (kind === "sound") {
|
|
1920
|
+
const { target, sound, soundBank } = this.resolveSoundReference(name);
|
|
1921
|
+
for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
|
|
1922
|
+
soundBank.stop(target, sound.soundId);
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
throw this.assetTypeMismatch("stopSound", name, "audio", kind);
|
|
1926
|
+
}
|
|
1927
|
+
stopAllSounds() {
|
|
1928
|
+
for (const audio of [...this.playingAudio.keys()]) this.stopExternalAudio(audio);
|
|
1929
|
+
this.playingAudio.clear();
|
|
1930
|
+
for (const target of this.runtime.targets) target.sprite?.soundBank?.stopAllSounds(target);
|
|
1931
|
+
}
|
|
1932
|
+
getAssetMimeType(args) {
|
|
1933
|
+
const name = normalizeName(args.NAME);
|
|
1934
|
+
const kind = this.assetRegistry.get(name);
|
|
1935
|
+
if (!kind) return "";
|
|
1936
|
+
switch (kind) {
|
|
1937
|
+
case "external": {
|
|
1938
|
+
const asset = this.externalAssets.get(name);
|
|
1939
|
+
return asset ? normalizeMimeType(asset.mimeType, asset.url || name) : "";
|
|
1940
|
+
}
|
|
1941
|
+
case "costume":
|
|
1942
|
+
case "backdrop": {
|
|
1943
|
+
const { costume } = this.resolveCostumeReference(name);
|
|
1944
|
+
return this.projectAssetMimeType(costume.dataFormat, "image");
|
|
1945
|
+
}
|
|
1946
|
+
case "sound": {
|
|
1947
|
+
const { sound } = this.resolveSoundReference(name);
|
|
1948
|
+
return this.projectAssetMimeType(sound.dataFormat, "audio");
|
|
1949
|
+
}
|
|
1950
|
+
case "text": return "text/plain";
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
async resolveImageAssetBytes(args) {
|
|
1954
|
+
const name = normalizeName(args.NAME);
|
|
1955
|
+
const kind = this.assetRegistry.get(name);
|
|
1956
|
+
if (!kind) throw this.assetNotRegistered("resolveDOMImageResource", name);
|
|
1957
|
+
if (kind === "external") {
|
|
1958
|
+
const asset = this.externalAssets.get(name);
|
|
1959
|
+
if (!asset) throw this.assetNotRegistered("resolveDOMImageResource", name);
|
|
1960
|
+
const mimeType = normalizeMimeType(asset.mimeType, asset.url || name);
|
|
1961
|
+
if (!mimeType.startsWith("image/")) throw this.assetTypeMismatch("resolveDOMImageResource", name, "image", `external/${this.externalMediaKind(asset)}`);
|
|
1962
|
+
return Object.freeze({
|
|
1963
|
+
bytes: new Uint8Array(asset.data.slice(0)),
|
|
1964
|
+
mimeType
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
if (kind === "costume" || kind === "backdrop") {
|
|
1968
|
+
const { costume } = this.resolveCostumeReference(name);
|
|
1969
|
+
const mimeType = this.projectAssetMimeType(costume.dataFormat, "image");
|
|
1970
|
+
const asset = await this.resolveProjectImageStorageAsset(name, costume);
|
|
1971
|
+
const source = asset.data instanceof Uint8Array ? asset.data : new Uint8Array(asset.data);
|
|
1972
|
+
return Object.freeze({
|
|
1973
|
+
bytes: Uint8Array.from(source),
|
|
1974
|
+
mimeType
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
throw this.assetTypeMismatch("resolveDOMImageResource", name, "image", kind);
|
|
1978
|
+
}
|
|
1979
|
+
getVersion() {
|
|
1980
|
+
return EXTENSION_VERSION;
|
|
1981
|
+
}
|
|
1982
|
+
async setTextValue(args) {
|
|
1983
|
+
const name = this.requireTextAssetName(args.NAME, "setTextValue");
|
|
1984
|
+
const kind = this.assetRegistry.get(name);
|
|
1985
|
+
if (kind !== void 0 && kind !== "text") throw this.assetTypeMismatch("setTextValue", name, "text", kind);
|
|
1986
|
+
const reference = this.textAssets.get(name);
|
|
1987
|
+
this.setRuntimeVariable(reference?.runtimeVariableName ?? textRuntimeVariableName(name), String(args.VALUE ?? ""));
|
|
1988
|
+
const targets = this.runtime.targets.filter((target) => this.displayedAssets.get(target.id)?.assetName === name);
|
|
1989
|
+
await Promise.all(targets.map(async (target) => {
|
|
1990
|
+
await this.applyTextToTarget(target, name, {
|
|
1991
|
+
runtime: this.runtime,
|
|
1992
|
+
target
|
|
1993
|
+
});
|
|
1994
|
+
this.setDisplayBinding(target, name, "text");
|
|
1995
|
+
}));
|
|
1996
|
+
}
|
|
1997
|
+
setTextStyle(args) {
|
|
1998
|
+
const name = this.requireTextAssetName(args.NAME, "setTextStyle");
|
|
1999
|
+
const kind = this.assetRegistry.get(name);
|
|
2000
|
+
if (kind !== void 0 && kind !== "text") throw this.assetTypeMismatch("setTextStyle", name, "text", kind);
|
|
2001
|
+
let property;
|
|
2002
|
+
try {
|
|
2003
|
+
property = normalizeTextStyleProperty(args.PROPERTY);
|
|
2004
|
+
} catch (error) {
|
|
2005
|
+
throw new AssetManagerError("STYLE_PROPERTY_INVALID", errorMessage(error), {
|
|
2006
|
+
operation: "setTextStyle",
|
|
2007
|
+
assetName: name,
|
|
2008
|
+
hint: "Use animation, font, color, width, or align.",
|
|
2009
|
+
cause: error
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
let value;
|
|
2013
|
+
try {
|
|
2014
|
+
value = normalizeTextStyleValue(property, args.VALUE);
|
|
2015
|
+
} catch (error) {
|
|
2016
|
+
throw new AssetManagerError("STYLE_VALUE_INVALID", errorMessage(error), {
|
|
2017
|
+
operation: "setTextStyle",
|
|
2018
|
+
assetName: name,
|
|
2019
|
+
hint: `Provide a valid ${property} value.`,
|
|
2020
|
+
cause: error
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
this.setRuntimeVariable(textStyleRuntimeVariableName(name, property), value);
|
|
2024
|
+
}
|
|
2025
|
+
assetNotRegistered(operation, name) {
|
|
2026
|
+
const candidates = suggestNames(name, this.assetRegistry.keys());
|
|
2027
|
+
return new AssetManagerError("ASSET_NOT_REGISTERED", `Cannot ${operation} asset "${name}": no registered asset has that name.`, {
|
|
2028
|
+
operation,
|
|
2029
|
+
assetName: name,
|
|
2030
|
+
candidates,
|
|
2031
|
+
hint: suggestionHint(candidates) ?? "Register the asset before using it."
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
assetTypeMismatch(operation, name, expectedKind, actualKind) {
|
|
2035
|
+
return new AssetManagerError("ASSET_TYPE_MISMATCH", `Cannot ${operation} asset "${name}": expected ${expectedKind}, but it is ${actualKind}.`, {
|
|
2036
|
+
operation,
|
|
2037
|
+
assetName: name,
|
|
2038
|
+
expectedKind,
|
|
2039
|
+
actualKind,
|
|
2040
|
+
hint: `Use an asset registered as ${expectedKind}.`
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
toScratchBlock(block) {
|
|
2044
|
+
return {
|
|
2045
|
+
opcode: block.opcode,
|
|
2046
|
+
blockType: Scratch.BlockType[block.blockType],
|
|
2047
|
+
text: Scratch.translate(block.text),
|
|
2048
|
+
...block.hideFromPalette ? { hideFromPalette: true } : {},
|
|
2049
|
+
...Object.keys(block.arguments).length > 0 ? { arguments: Object.fromEntries(Object.entries(block.arguments).map(([name, argument]) => [name, {
|
|
2050
|
+
type: Scratch.ArgumentType[argument.type],
|
|
2051
|
+
defaultValue: argument.defaultValue
|
|
2052
|
+
}])) } : {}
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
requireAssetName(value, operation = "registerAsset") {
|
|
2056
|
+
return requireAssetNameValue(value, operation);
|
|
2057
|
+
}
|
|
2058
|
+
requireTextAssetName(value, operation = "registerAsset") {
|
|
2059
|
+
return requireTextAssetNameValue(value, operation);
|
|
2060
|
+
}
|
|
2061
|
+
clearAssetError() {
|
|
2062
|
+
this.lastAssetErrorType = "";
|
|
2063
|
+
this.lastAssetErrorLabel = "";
|
|
2064
|
+
}
|
|
2065
|
+
beginRegistration(name) {
|
|
2066
|
+
const version = (this.registrationVersions.get(name) ?? 0) + 1;
|
|
2067
|
+
this.registrationVersions.set(name, version);
|
|
2068
|
+
return {
|
|
2069
|
+
version,
|
|
2070
|
+
cancellationVersion: this.registrationCancellationVersions.get(name) ?? 0
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
cancelRegistrations(name) {
|
|
2074
|
+
const cancellationVersion = (this.registrationCancellationVersions.get(name) ?? 0) + 1;
|
|
2075
|
+
this.registrationCancellationVersions.set(name, cancellationVersion);
|
|
2076
|
+
}
|
|
2077
|
+
isRegistrationCancellationCurrent(name, token) {
|
|
2078
|
+
return (this.registrationCancellationVersions.get(name) ?? 0) === token.cancellationVersion;
|
|
2079
|
+
}
|
|
2080
|
+
async registerExternalAsset(url, name) {
|
|
2081
|
+
const token = this.beginRegistration(name);
|
|
2082
|
+
const record = url ? await this.fetchExternalAsset(url, name) : this.committedCacheRecords.has(name) ? this.committedCacheRecords.get(name) ?? null : await this.cacheGet(name);
|
|
2083
|
+
if (!this.isRegistrationCancellationCurrent(name, token)) return;
|
|
2084
|
+
if (!record) {
|
|
2085
|
+
const candidates = suggestNames(name, this.assetRegistry.keys());
|
|
2086
|
+
throw new AssetManagerError("ASSET_NOT_REGISTERED", `Asset "${name}" is not cached and no URL was provided.`, {
|
|
2087
|
+
operation: "registerAsset",
|
|
2088
|
+
assetName: name,
|
|
2089
|
+
candidates,
|
|
2090
|
+
hint: suggestionHint(candidates) ?? "Register an HTTP(S) URL before loading from cache."
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
const prepared = {
|
|
2094
|
+
...record,
|
|
2095
|
+
kind: "external",
|
|
2096
|
+
mimeType: normalizeMimeType(record.mimeType, record.url || name),
|
|
2097
|
+
skinId: null
|
|
2098
|
+
};
|
|
2099
|
+
await this.commitPreparedAsset(name, "external", prepared, token, url ? record : void 0);
|
|
2100
|
+
}
|
|
2101
|
+
async registerCostumeReference(name, spriteName, costumeName) {
|
|
2102
|
+
const token = this.beginRegistration(name);
|
|
2103
|
+
const { target, costume, costumeName: resolvedCostumeName } = resolveCostumeAddress(this.runtime, name, spriteName, costumeName);
|
|
2104
|
+
if (!this.isRegistrationCancellationCurrent(name, token)) return;
|
|
2105
|
+
await this.commitPreparedAsset(name, "costume", {
|
|
2106
|
+
kind: "costume",
|
|
2107
|
+
name,
|
|
2108
|
+
targetId: target.id,
|
|
2109
|
+
targetName: spriteName,
|
|
2110
|
+
isStage: false,
|
|
2111
|
+
costumeName: resolvedCostumeName,
|
|
2112
|
+
assetId: costume.assetId ?? null
|
|
2113
|
+
}, token);
|
|
2114
|
+
}
|
|
2115
|
+
async registerBackdropReference(name, backdropName) {
|
|
2116
|
+
const token = this.beginRegistration(name);
|
|
2117
|
+
const { target: stage, costume } = resolveBackdropAddress(this.runtime, backdropName);
|
|
2118
|
+
if (!this.isRegistrationCancellationCurrent(name, token)) return;
|
|
2119
|
+
await this.commitPreparedAsset(name, "backdrop", {
|
|
2120
|
+
kind: "backdrop",
|
|
2121
|
+
name,
|
|
2122
|
+
targetId: stage.id,
|
|
2123
|
+
targetName: STAGE_RESOURCE_NAME,
|
|
2124
|
+
isStage: true,
|
|
2125
|
+
costumeName: backdropName,
|
|
2126
|
+
assetId: costume.assetId ?? null
|
|
2127
|
+
}, token);
|
|
2128
|
+
}
|
|
2129
|
+
async registerSoundReference(name, spriteName, soundName) {
|
|
2130
|
+
const token = this.beginRegistration(name);
|
|
2131
|
+
const { target, sound, isStage } = resolveSoundAddress(this.runtime, spriteName, soundName, name);
|
|
2132
|
+
if (!this.isRegistrationCancellationCurrent(name, token)) return;
|
|
2133
|
+
await this.commitPreparedAsset(name, "sound", {
|
|
2134
|
+
kind: "sound",
|
|
2135
|
+
name,
|
|
2136
|
+
targetId: target.id,
|
|
2137
|
+
targetName: isStage ? STAGE_RESOURCE_NAME : spriteName,
|
|
2138
|
+
isStage,
|
|
2139
|
+
soundName,
|
|
2140
|
+
assetId: sound.assetId ?? null
|
|
2141
|
+
}, token);
|
|
2142
|
+
}
|
|
2143
|
+
async registerLiteralSoundReference(name, targetName, soundName) {
|
|
2144
|
+
const token = this.beginRegistration(name);
|
|
2145
|
+
const resolved = resolveLiteralSoundAddress(this.runtime, targetName, soundName, name);
|
|
2146
|
+
if (!this.isRegistrationCancellationCurrent(name, token)) return;
|
|
2147
|
+
await this.commitPreparedAsset(name, "sound", {
|
|
2148
|
+
kind: "sound",
|
|
2149
|
+
name,
|
|
2150
|
+
targetId: resolved.target.id,
|
|
2151
|
+
targetName: resolved.targetName,
|
|
2152
|
+
isStage: resolved.isStage,
|
|
2153
|
+
soundName,
|
|
2154
|
+
assetId: resolved.sound.assetId ?? null
|
|
2155
|
+
}, token);
|
|
2156
|
+
}
|
|
2157
|
+
async registerTextReference(name, runtimeVariableName) {
|
|
2158
|
+
const token = this.beginRegistration(name);
|
|
2159
|
+
this.requireTextAssetName(name);
|
|
2160
|
+
if (!this.isRegistrationCancellationCurrent(name, token)) return;
|
|
2161
|
+
await this.commitPreparedAsset(name, "text", {
|
|
2162
|
+
kind: "text",
|
|
2163
|
+
name,
|
|
2164
|
+
runtimeVariableName
|
|
2165
|
+
}, token);
|
|
2166
|
+
}
|
|
2167
|
+
unregisterAsset(name) {
|
|
2168
|
+
this.cancelRegistrations(name);
|
|
2169
|
+
this.invalidateDOMImageResources(name);
|
|
2170
|
+
const kind = this.assetRegistry.get(name);
|
|
2171
|
+
if (!kind) return;
|
|
2172
|
+
const asset = this.getRegisteredAsset(name, kind);
|
|
2173
|
+
this.removeRegisteredAsset(name, kind);
|
|
2174
|
+
this.disposeRegisteredAsset(asset);
|
|
2175
|
+
this.assetRegistry.delete(name);
|
|
2176
|
+
for (const [targetId, binding] of this.displayedAssets) if (binding.assetName === name) this.displayedAssets.delete(targetId);
|
|
2177
|
+
}
|
|
2178
|
+
async commitPreparedAsset(name, kind, prepared, token, cacheRecord) {
|
|
2179
|
+
const commit = (this.registrationCommits.get(name) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
2180
|
+
if (!this.isRegistrationCancellationCurrent(name, token) || token.version < (this.successfulRegistrationVersions.get(name) ?? 0)) {
|
|
2181
|
+
this.disposeRegisteredAsset(prepared);
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
const currentKind = this.assetRegistry.get(name);
|
|
2185
|
+
const current = currentKind ? this.getRegisteredAsset(name, currentKind) : void 0;
|
|
2186
|
+
this.assertReplacementKind(name, currentKind, current, kind, prepared);
|
|
2187
|
+
let previousCached;
|
|
2188
|
+
let nextCached;
|
|
2189
|
+
if (cacheRecord) {
|
|
2190
|
+
previousCached = this.committedCacheRecords.has(name) ? this.committedCacheRecords.get(name) ?? null : await this.cacheGet(name);
|
|
2191
|
+
if (!this.isRegistrationCancellationCurrent(name, token)) {
|
|
2192
|
+
this.disposeRegisteredAsset(prepared);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
nextCached = {
|
|
2196
|
+
...cacheRecord,
|
|
2197
|
+
generation: token.version
|
|
2198
|
+
};
|
|
2199
|
+
await this.cachePut(nextCached);
|
|
2200
|
+
if (!this.isRegistrationCancellationCurrent(name, token)) {
|
|
2201
|
+
await this.restoreCacheIfGeneration(name, token.version, previousCached);
|
|
2202
|
+
this.disposeRegisteredAsset(prepared);
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
try {
|
|
2207
|
+
await this.commitPreparedAssetNow(name, kind, prepared, token);
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
if (cacheRecord) await this.restoreCacheIfGeneration(name, token.version, previousCached ?? null);
|
|
2210
|
+
throw error;
|
|
2211
|
+
}
|
|
2212
|
+
this.invalidateDOMImageResources(name);
|
|
2213
|
+
if (!this.isRegistrationCancellationCurrent(name, token)) {
|
|
2214
|
+
if (cacheRecord) await this.restoreCacheIfGeneration(name, token.version, previousCached ?? null);
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
this.successfulRegistrationVersions.set(name, token.version);
|
|
2218
|
+
if (nextCached) this.committedCacheRecords.set(name, nextCached);
|
|
2219
|
+
});
|
|
2220
|
+
this.registrationCommits.set(name, commit);
|
|
2221
|
+
try {
|
|
2222
|
+
await commit;
|
|
2223
|
+
} finally {
|
|
2224
|
+
if (this.registrationCommits.get(name) === commit) this.registrationCommits.delete(name);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
async commitPreparedAssetNow(name, kind, prepared, token) {
|
|
2228
|
+
const currentKind = this.assetRegistry.get(name);
|
|
2229
|
+
const current = currentKind ? this.getRegisteredAsset(name, currentKind) : void 0;
|
|
2230
|
+
this.assertReplacementKind(name, currentKind, current, kind, prepared);
|
|
2231
|
+
if (currentKind === kind && current && this.featureFlags.ENABLE_LIVE_ASSET_REPLACEMENT) {
|
|
2232
|
+
await this.replaceRegisteredAsset(name, kind, current, prepared, token);
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
if (currentKind) this.removeRegisteredAsset(name, currentKind);
|
|
2236
|
+
this.installRegisteredAsset(name, kind, prepared);
|
|
2237
|
+
this.disposeRegisteredAsset(current);
|
|
2238
|
+
for (const [targetId, binding] of this.displayedAssets) if (binding.assetName === name) this.displayedAssets.delete(targetId);
|
|
2239
|
+
}
|
|
2240
|
+
assertReplacementKind(name, currentKind, current, nextKind, next) {
|
|
2241
|
+
if (!currentKind || !this.featureFlags.ENABLE_STRICT_ASSET_KIND_REPLACEMENT) return;
|
|
2242
|
+
let expectedKind = currentKind;
|
|
2243
|
+
let actualKind = nextKind;
|
|
2244
|
+
if (currentKind === "external" && nextKind === "external" && current?.kind === "external" && next.kind === "external") {
|
|
2245
|
+
const currentMedia = this.externalMediaKind(current);
|
|
2246
|
+
const nextMedia = this.externalMediaKind(next);
|
|
2247
|
+
if (currentMedia === nextMedia || currentMedia === "unknown" || nextMedia === "unknown") return;
|
|
2248
|
+
expectedKind = `external/${currentMedia}`;
|
|
2249
|
+
actualKind = `external/${nextMedia}`;
|
|
2250
|
+
} else if (currentKind === nextKind) return;
|
|
2251
|
+
throw new AssetManagerError("ASSET_TYPE_CHANGE", `Cannot replace asset "${name}": it is currently registered as ${expectedKind}, but the new resource is ${actualKind}.`, {
|
|
2252
|
+
operation: "registerAsset",
|
|
2253
|
+
assetName: name,
|
|
2254
|
+
expectedKind,
|
|
2255
|
+
actualKind,
|
|
2256
|
+
hint: "Delete the existing asset first if this type change is intentional."
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
async replaceRegisteredAsset(name, kind, current, prepared, token) {
|
|
2260
|
+
const managedDisplays = [...this.displayedAssets].filter(([, binding]) => binding.assetName === name && binding.assetKind === kind).flatMap(([targetId, binding]) => {
|
|
2261
|
+
const target = this.runtime.targets.find((candidate) => candidate.id === targetId);
|
|
2262
|
+
if (target && this.isDisplayBindingCurrent(target, binding)) return [{
|
|
2263
|
+
target,
|
|
2264
|
+
binding
|
|
2265
|
+
}];
|
|
2266
|
+
this.displayedAssets.delete(targetId);
|
|
2267
|
+
return [];
|
|
2268
|
+
});
|
|
2269
|
+
let preparedDisplays;
|
|
2270
|
+
try {
|
|
2271
|
+
const skin = kind === "text" || managedDisplays.length === 0 ? null : await this.resolveSkinFromAsset(name, kind, prepared);
|
|
2272
|
+
preparedDisplays = managedDisplays.map(({ target, binding }) => ({
|
|
2273
|
+
target,
|
|
2274
|
+
binding,
|
|
2275
|
+
skin
|
|
2276
|
+
}));
|
|
2277
|
+
} catch (error) {
|
|
2278
|
+
this.disposeRegisteredAsset(prepared);
|
|
2279
|
+
throw new AssetManagerError("REPLACEMENT_FAILED", `Cannot prepare replacement asset "${name}": ${errorMessage(error)}`, {
|
|
2280
|
+
operation: "registerAsset",
|
|
2281
|
+
assetName: name,
|
|
2282
|
+
hint: "The old registration and display were kept.",
|
|
2283
|
+
cause: error
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
if (!this.isRegistrationCancellationCurrent(name, token)) {
|
|
2287
|
+
this.disposeRegisteredAsset(prepared);
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
this.installRegisteredAsset(name, kind, prepared);
|
|
2291
|
+
const attemptedTargets = [];
|
|
2292
|
+
try {
|
|
2293
|
+
for (const display of preparedDisplays) {
|
|
2294
|
+
if (!this.isRegistrationCancellationCurrent(name, token)) break;
|
|
2295
|
+
if (!this.runtime.targets.includes(display.target)) {
|
|
2296
|
+
this.displayedAssets.delete(display.target.id);
|
|
2297
|
+
continue;
|
|
2298
|
+
}
|
|
2299
|
+
if (this.displayedAssets.get(display.target.id) !== display.binding) continue;
|
|
2300
|
+
if (!this.isDisplayBindingCurrent(display.target, display.binding)) {
|
|
2301
|
+
this.displayedAssets.delete(display.target.id);
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
attemptedTargets.push(display.target);
|
|
2305
|
+
if (kind === "text") await this.applyTextReferenceToTarget(display.target, name, prepared, {
|
|
2306
|
+
runtime: this.runtime,
|
|
2307
|
+
target: display.target
|
|
2308
|
+
});
|
|
2309
|
+
else if (display.skin) this.applySkinToTarget(display.target, display.skin);
|
|
2310
|
+
if (this.isRegistrationCancellationCurrent(name, token)) this.setDisplayBinding(display.target, name, kind, display.skin?.skinId);
|
|
2311
|
+
}
|
|
2312
|
+
} catch (error) {
|
|
2313
|
+
if (!this.isRegistrationCancellationCurrent(name, token)) {
|
|
2314
|
+
this.disposeRegisteredAsset(current);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
this.installRegisteredAsset(name, kind, current);
|
|
2318
|
+
let rollbackError;
|
|
2319
|
+
for (const target of attemptedTargets) {
|
|
2320
|
+
if (!this.runtime.targets.includes(target)) continue;
|
|
2321
|
+
try {
|
|
2322
|
+
if (kind === "text") await this.applyTextReferenceToTarget(target, name, current, {
|
|
2323
|
+
runtime: this.runtime,
|
|
2324
|
+
target
|
|
2325
|
+
});
|
|
2326
|
+
else {
|
|
2327
|
+
const currentSkin = await this.resolveSkinFromAsset(name, kind, current);
|
|
2328
|
+
this.applySkinToTarget(target, currentSkin);
|
|
2329
|
+
this.setDisplayBinding(target, name, kind, currentSkin.skinId);
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
this.setDisplayBinding(target, name, kind);
|
|
2333
|
+
} catch (rollbackFailure) {
|
|
2334
|
+
rollbackError ?? (rollbackError = rollbackFailure);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
this.disposeRegisteredAsset(prepared);
|
|
2338
|
+
throw new AssetManagerError("REPLACEMENT_FAILED", `Cannot replace asset "${name}": ${errorMessage(error)}`, {
|
|
2339
|
+
operation: "registerAsset",
|
|
2340
|
+
assetName: name,
|
|
2341
|
+
hint: rollbackError ? `The old registration was restored, but its display could not be reapplied: ${errorMessage(rollbackError)}` : "The old registration and display were restored.",
|
|
2342
|
+
cause: error
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
this.disposeRegisteredAsset(current);
|
|
2346
|
+
}
|
|
2347
|
+
getRegisteredAsset(name, kind) {
|
|
2348
|
+
if (kind === "external") return this.externalAssets.get(name);
|
|
2349
|
+
if (kind === "costume" || kind === "backdrop") return this.costumeAssets.get(name);
|
|
2350
|
+
if (kind === "sound") return this.soundAssets.get(name);
|
|
2351
|
+
return this.textAssets.get(name);
|
|
2352
|
+
}
|
|
2353
|
+
installRegisteredAsset(name, kind, asset) {
|
|
2354
|
+
if (kind === "external") this.externalAssets.set(name, asset);
|
|
2355
|
+
else if (kind === "costume" || kind === "backdrop") this.costumeAssets.set(name, asset);
|
|
2356
|
+
else if (kind === "sound") this.soundAssets.set(name, asset);
|
|
2357
|
+
else this.textAssets.set(name, asset);
|
|
2358
|
+
this.assetRegistry.set(name, kind);
|
|
2359
|
+
}
|
|
2360
|
+
removeRegisteredAsset(name, kind) {
|
|
2361
|
+
if (kind === "external") this.externalAssets.delete(name);
|
|
2362
|
+
else if (kind === "costume" || kind === "backdrop") this.costumeAssets.delete(name);
|
|
2363
|
+
else if (kind === "sound") this.soundAssets.delete(name);
|
|
2364
|
+
else this.textAssets.delete(name);
|
|
2365
|
+
}
|
|
2366
|
+
disposeRegisteredAsset(asset) {
|
|
2367
|
+
if (asset?.kind === "external") this.deleteOwnedSkinIfExists(asset);
|
|
2368
|
+
}
|
|
2369
|
+
externalMediaKind(asset) {
|
|
2370
|
+
const mimeType = normalizeMimeType(asset.mimeType, asset.url || asset.name);
|
|
2371
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
2372
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
2373
|
+
return "unknown";
|
|
2374
|
+
}
|
|
2375
|
+
openDatabase() {
|
|
2376
|
+
return new Promise((resolve, reject) => {
|
|
2377
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
2378
|
+
request.onupgradeneeded = () => {
|
|
2379
|
+
if (!request.result.objectStoreNames.contains(STORE_NAME)) request.result.createObjectStore(STORE_NAME, { keyPath: "name" });
|
|
2380
|
+
};
|
|
2381
|
+
request.onsuccess = () => resolve(request.result);
|
|
2382
|
+
request.onerror = () => reject(request.error);
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
async transaction(mode, action) {
|
|
2386
|
+
const database = await this.openDatabase();
|
|
2387
|
+
return new Promise((resolve, reject) => {
|
|
2388
|
+
const request = action(database.transaction(STORE_NAME, mode).objectStore(STORE_NAME));
|
|
2389
|
+
request.onsuccess = () => resolve(request.result);
|
|
2390
|
+
request.onerror = () => reject(request.error);
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
async cacheGet(name) {
|
|
2394
|
+
return await this.transaction("readonly", (store) => store.get(name)) ?? null;
|
|
2395
|
+
}
|
|
2396
|
+
async cachePut(record) {
|
|
2397
|
+
await this.transaction("readwrite", (store) => store.put(record));
|
|
2398
|
+
}
|
|
2399
|
+
async cacheDelete(name) {
|
|
2400
|
+
await this.transaction("readwrite", (store) => store.delete(name));
|
|
2401
|
+
}
|
|
2402
|
+
async cacheClear() {
|
|
2403
|
+
await this.transaction("readwrite", (store) => store.clear());
|
|
2404
|
+
}
|
|
2405
|
+
async restoreCacheIfGeneration(name, generation, previous) {
|
|
2406
|
+
const database = await this.openDatabase();
|
|
2407
|
+
await new Promise((resolve, reject) => {
|
|
2408
|
+
const transaction = database.transaction(STORE_NAME, "readwrite");
|
|
2409
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
2410
|
+
const request = store.get(name);
|
|
2411
|
+
request.onerror = () => reject(request.error);
|
|
2412
|
+
request.onsuccess = () => {
|
|
2413
|
+
if (request.result?.generation !== generation) return;
|
|
2414
|
+
if (previous) store.put(previous);
|
|
2415
|
+
else store.delete(name);
|
|
2416
|
+
};
|
|
2417
|
+
transaction.oncomplete = () => resolve();
|
|
2418
|
+
transaction.onerror = () => reject(transaction.error);
|
|
2419
|
+
transaction.onabort = () => reject(transaction.error);
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
async fetchExternalAsset(url, name) {
|
|
2423
|
+
const response = await fetch(url);
|
|
2424
|
+
if (!response.ok) throw new Error(`Failed to fetch asset "${name}": ${response.status} ${response.statusText}`);
|
|
2425
|
+
const blob = await response.blob();
|
|
2426
|
+
return {
|
|
2427
|
+
name,
|
|
2428
|
+
url,
|
|
2429
|
+
mimeType: normalizeMimeType(blob.type || response.headers.get("Content-Type"), url),
|
|
2430
|
+
data: await blob.arrayBuffer(),
|
|
2431
|
+
cachedAt: Date.now()
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
getStageTarget() {
|
|
2435
|
+
return findStageTarget(this.runtime);
|
|
2436
|
+
}
|
|
2437
|
+
findTargetByName(name) {
|
|
2438
|
+
return findProjectTargetByName(this.runtime, name);
|
|
2439
|
+
}
|
|
2440
|
+
resolveReferencedTarget(targetId, targetName, isStage) {
|
|
2441
|
+
const byId = this.runtime.targets.find((target) => target.id === targetId);
|
|
2442
|
+
if (byId) return byId;
|
|
2443
|
+
if (isStage) return this.getStageTarget();
|
|
2444
|
+
const byName = this.findTargetByName(targetName);
|
|
2445
|
+
if (!byName) {
|
|
2446
|
+
const candidates = suggestNames(targetName, this.runtime.targets.flatMap((target) => target.sprite?.name ? [target.sprite.name] : []));
|
|
2447
|
+
throw new AssetManagerError("SPRITE_NOT_FOUND", `Asset source target no longer exists: ${targetName}.`, {
|
|
2448
|
+
operation: "resolveAsset",
|
|
2449
|
+
actorName: targetName,
|
|
2450
|
+
candidates,
|
|
2451
|
+
hint: suggestionHint(candidates)
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
return byName;
|
|
2455
|
+
}
|
|
2456
|
+
findCostume(target, costumeName, assetId) {
|
|
2457
|
+
return findProjectCostume(target, costumeName, assetId);
|
|
2458
|
+
}
|
|
2459
|
+
findSound(target, soundName, assetId) {
|
|
2460
|
+
return findProjectSound(target, soundName, assetId);
|
|
2461
|
+
}
|
|
2462
|
+
async resolveSkin(value) {
|
|
2463
|
+
const name = normalizeName(value);
|
|
2464
|
+
const kind = this.assetRegistry.get(name);
|
|
2465
|
+
if (!kind) throw this.assetNotRegistered("show", name);
|
|
2466
|
+
const asset = this.getRegisteredAsset(name, kind);
|
|
2467
|
+
if (!asset) throw this.assetNotRegistered("show", name);
|
|
2468
|
+
return this.resolveSkinFromAsset(name, kind, asset);
|
|
2469
|
+
}
|
|
2470
|
+
async applyAssetToTarget(target, value, util) {
|
|
2471
|
+
const name = normalizeName(value);
|
|
2472
|
+
const kind = this.assetRegistry.get(name);
|
|
2473
|
+
let skinId;
|
|
2474
|
+
if (!kind) throw this.assetNotRegistered("show", name);
|
|
2475
|
+
if (!this.runtime.targets.includes(target)) return;
|
|
2476
|
+
if (kind === "text") await this.applyTextToTarget(target, name, util);
|
|
2477
|
+
else if (kind === "external" || kind === "costume" || kind === "backdrop") {
|
|
2478
|
+
const skin = await this.resolveSkin(name);
|
|
2479
|
+
if (!this.runtime.targets.includes(target)) return;
|
|
2480
|
+
this.applySkinToTarget(target, skin);
|
|
2481
|
+
skinId = skin.skinId;
|
|
2482
|
+
} else throw this.assetTypeMismatch("show", name, "image or text", kind);
|
|
2483
|
+
if (!this.runtime.targets.includes(target)) return;
|
|
2484
|
+
this.setDisplayBinding(target, name, kind, skinId);
|
|
2485
|
+
}
|
|
2486
|
+
setDisplayBinding(target, name, kind, skinId) {
|
|
2487
|
+
if (kind === "sound") {
|
|
2488
|
+
this.displayedAssets.delete(target.id);
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
this.displayedAssets.set(target.id, {
|
|
2492
|
+
assetName: name,
|
|
2493
|
+
assetKind: kind,
|
|
2494
|
+
skinId: skinId ?? null
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
isDisplayBindingCurrent(target, binding) {
|
|
2498
|
+
if (binding.assetKind === "text") return true;
|
|
2499
|
+
if (binding.skinId === null || target.drawableID === void 0 || target.drawableID === null) return false;
|
|
2500
|
+
return this.renderer._allDrawables?.[target.drawableID]?.skin?.id === binding.skinId;
|
|
2501
|
+
}
|
|
2502
|
+
applyResolvedSkinToTarget(target, name, skin) {
|
|
2503
|
+
const kind = this.assetRegistry.get(name);
|
|
2504
|
+
if (!kind) throw this.assetNotRegistered("show", name);
|
|
2505
|
+
if (kind !== "external" && kind !== "costume" && kind !== "backdrop") throw this.assetTypeMismatch("show", name, "image", kind);
|
|
2506
|
+
this.applySkinToTarget(target, skin);
|
|
2507
|
+
this.setDisplayBinding(target, name, kind, skin.skinId);
|
|
2508
|
+
}
|
|
2509
|
+
async applyTextToTarget(target, name, util) {
|
|
2510
|
+
if (target.isStage) throw this.assetTypeMismatch("show on stage", name, "image", "text");
|
|
2511
|
+
const reference = this.textAssets.get(name);
|
|
2512
|
+
if (!reference) throw this.assetNotRegistered("show", name);
|
|
2513
|
+
await this.applyTextReferenceToTarget(target, name, reference, util);
|
|
2514
|
+
}
|
|
2515
|
+
async applyTextReferenceToTarget(target, name, reference, util) {
|
|
2516
|
+
if (target.isStage) throw this.assetTypeMismatch("show on stage", name, "image", "text");
|
|
2517
|
+
const temporaryVariables = this.requireTemporaryVariables("show", name);
|
|
2518
|
+
const getRuntimeVariable = (variableName) => temporaryVariables.getRuntimeVariable({ VAR: variableName });
|
|
2519
|
+
let style;
|
|
2520
|
+
try {
|
|
2521
|
+
style = resolveTextStyle(name, this.runtime.stageWidth, getRuntimeVariable);
|
|
2522
|
+
} catch (error) {
|
|
2523
|
+
throw new AssetManagerError("STYLE_VALUE_INVALID", errorMessage(error), {
|
|
2524
|
+
operation: "show",
|
|
2525
|
+
assetName: name,
|
|
2526
|
+
hint: "Correct the stored text style value before showing this asset.",
|
|
2527
|
+
cause: error
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
const setFont = this.requireAnimatedTextOpcode("text_setFont", name);
|
|
2531
|
+
const setColor = this.requireAnimatedTextOpcode("text_setColor", name);
|
|
2532
|
+
const setWidth = this.requireAnimatedTextOpcode("text_setWidth", name);
|
|
2533
|
+
const setOutlineWidth = this.runtime.getOpcodeFunction?.("text_setOutlineWidth");
|
|
2534
|
+
const setOutlineColor = this.runtime.getOpcodeFunction?.("text_setOutlineColor");
|
|
2535
|
+
const displayText = this.requireAnimatedTextOpcode(style.animation === "none" ? "text_setText" : "text_animateText", name);
|
|
2536
|
+
const blockUtility = {
|
|
2537
|
+
...util,
|
|
2538
|
+
target,
|
|
2539
|
+
runtime: util?.runtime ?? this.runtime
|
|
2540
|
+
};
|
|
2541
|
+
const text = getRuntimeVariable(reference.runtimeVariableName);
|
|
2542
|
+
await Promise.resolve(setFont({ FONT: style.font }, blockUtility));
|
|
2543
|
+
await Promise.resolve(setColor({ COLOR: style.color }, blockUtility));
|
|
2544
|
+
await Promise.resolve(setWidth({
|
|
2545
|
+
WIDTH: style.width,
|
|
2546
|
+
ALIGN: style.align
|
|
2547
|
+
}, blockUtility));
|
|
2548
|
+
if (setOutlineWidth) await Promise.resolve(setOutlineWidth({ WIDTH: 2 }, blockUtility));
|
|
2549
|
+
if (setOutlineColor) await Promise.resolve(setOutlineColor({ COLOR: DEFAULT_OUTLINE_COLOR }, blockUtility));
|
|
2550
|
+
const displayResult = displayText(style.animation === "none" ? { TEXT: String(text ?? "") } : {
|
|
2551
|
+
ANIMATE: style.animation,
|
|
2552
|
+
TEXT: String(text ?? "")
|
|
2553
|
+
}, blockUtility);
|
|
2554
|
+
if (style.animation === "none") await Promise.resolve(displayResult);
|
|
2555
|
+
else Promise.resolve(displayResult).catch((error) => {
|
|
2556
|
+
console.error(new AssetManagerError("ANIMATION_FAILED", `Animated Text failed for asset "${name}": ${errorMessage(error)}`, {
|
|
2557
|
+
operation: "animateText",
|
|
2558
|
+
assetName: name,
|
|
2559
|
+
hint: "Check the Animated Text extension and animation value.",
|
|
2560
|
+
cause: error
|
|
2561
|
+
}));
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
requireTemporaryVariables(operation = "setTextValue", assetName) {
|
|
2565
|
+
const temporaryVariables = this.runtime.ext_lmsTempVars2;
|
|
2566
|
+
if (!temporaryVariables?.getRuntimeVariable) throw new AssetManagerError("DEPENDENCY_MISSING", "Temporary Variables extension is not loaded.", {
|
|
2567
|
+
operation,
|
|
2568
|
+
assetName,
|
|
2569
|
+
hint: "Load the lmsTempVars2 extension before using runtime text assets."
|
|
2570
|
+
});
|
|
2571
|
+
return temporaryVariables;
|
|
2572
|
+
}
|
|
2573
|
+
setRuntimeVariable(name, value) {
|
|
2574
|
+
const assetName = name.split(":")[1];
|
|
2575
|
+
const temporaryVariables = this.requireTemporaryVariables("setRuntimeVariable", assetName);
|
|
2576
|
+
if (!temporaryVariables.setRuntimeVariable) throw new AssetManagerError("DEPENDENCY_MISSING", "Temporary Variables extension does not support setting runtime variables.", {
|
|
2577
|
+
operation: "setRuntimeVariable",
|
|
2578
|
+
assetName,
|
|
2579
|
+
hint: "Load a Temporary Variables version that provides setRuntimeVariable."
|
|
2580
|
+
});
|
|
2581
|
+
temporaryVariables.setRuntimeVariable({
|
|
2582
|
+
VAR: name,
|
|
2583
|
+
STRING: value
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
requireAnimatedTextOpcode(opcode, assetName) {
|
|
2587
|
+
const implementation = this.runtime.getOpcodeFunction?.(opcode);
|
|
2588
|
+
if (!implementation) throw new AssetManagerError("DEPENDENCY_MISSING", `Animated Text extension is not loaded or does not provide ${opcode}.`, {
|
|
2589
|
+
operation: "show",
|
|
2590
|
+
assetName,
|
|
2591
|
+
hint: "Load a compatible Animated Text extension before showing text assets."
|
|
2592
|
+
});
|
|
2593
|
+
return implementation;
|
|
2594
|
+
}
|
|
2595
|
+
async ensureExternalSkin(name) {
|
|
2596
|
+
const asset = this.externalAssets.get(name);
|
|
2597
|
+
if (!asset) throw this.assetNotRegistered("show", name);
|
|
2598
|
+
return this.ensureExternalAssetSkin(asset, name);
|
|
2599
|
+
}
|
|
2600
|
+
async resolveProjectImageStorageAsset(name, costume) {
|
|
2601
|
+
if (costume.asset?.data) return costume.asset;
|
|
2602
|
+
const assetId = costume.assetId;
|
|
2603
|
+
const dataFormat = costume.dataFormat?.toLowerCase();
|
|
2604
|
+
const storage = this.runtime.storage;
|
|
2605
|
+
if (!assetId || !dataFormat || !storage) throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", `Project image bytes are unavailable for asset "${name}".`, {
|
|
2606
|
+
operation: "resolveDOMImageResource",
|
|
2607
|
+
assetName: name,
|
|
2608
|
+
hint: "Resolve the resource while its project costume and VM storage are available."
|
|
2609
|
+
});
|
|
2610
|
+
const cached = storage.get?.(assetId);
|
|
2611
|
+
if (cached?.data) return cached;
|
|
2612
|
+
const assetType = dataFormat === "svg" ? storage.AssetType.ImageVector : storage.AssetType.ImageBitmap;
|
|
2613
|
+
const loaded = await storage.load?.(assetType, assetId, dataFormat);
|
|
2614
|
+
if (!loaded?.data) throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", `Project image bytes are unavailable for asset "${name}".`, {
|
|
2615
|
+
operation: "resolveDOMImageResource",
|
|
2616
|
+
assetName: name,
|
|
2617
|
+
hint: "Keep the project asset available in VM storage until the resource is resolved."
|
|
2618
|
+
});
|
|
2619
|
+
return loaded;
|
|
2620
|
+
}
|
|
2621
|
+
cancelledDOMImageResolution(name) {
|
|
2622
|
+
const error = /* @__PURE__ */ new Error(`DOM image resource resolution was cancelled: ${JSON.stringify(name)}`);
|
|
2623
|
+
error.name = "AbortError";
|
|
2624
|
+
return error;
|
|
2625
|
+
}
|
|
2626
|
+
async resolveExtensionDOMImageResource(nameInput) {
|
|
2627
|
+
const name = normalizeName(nameInput);
|
|
2628
|
+
const version = this.domImageResourceVersions.get(name) ?? 0;
|
|
2629
|
+
let entry = this.domImageResourceBackings.get(name);
|
|
2630
|
+
if (entry?.version !== version) {
|
|
2631
|
+
let nextEntry;
|
|
2632
|
+
const promise = (async () => {
|
|
2633
|
+
const resolved = await this.resolveImageAssetBytes({ NAME: name });
|
|
2634
|
+
const backing = await createDOMImageResourceBacking({
|
|
2635
|
+
name,
|
|
2636
|
+
bytes: resolved.bytes,
|
|
2637
|
+
mimeType: resolved.mimeType
|
|
2638
|
+
}, (idleBacking) => {
|
|
2639
|
+
if (this.domImageResourceBackings.get(name) === nextEntry && nextEntry.backing === idleBacking) this.domImageResourceBackings.delete(name);
|
|
2640
|
+
});
|
|
2641
|
+
nextEntry.backing = backing;
|
|
2642
|
+
return backing;
|
|
2643
|
+
})();
|
|
2644
|
+
nextEntry = {
|
|
2645
|
+
version,
|
|
2646
|
+
promise
|
|
2647
|
+
};
|
|
2648
|
+
entry = nextEntry;
|
|
2649
|
+
this.domImageResourceBackings.set(name, entry);
|
|
2650
|
+
try {
|
|
2651
|
+
await promise;
|
|
2652
|
+
} catch (error) {
|
|
2653
|
+
if (this.domImageResourceBackings.get(name) === entry) this.domImageResourceBackings.delete(name);
|
|
2654
|
+
throw error;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
const backing = await entry.promise;
|
|
2658
|
+
let controller;
|
|
2659
|
+
const resource = backing.acquire(() => {
|
|
2660
|
+
if (controller) this.releaseActiveDOMImageResource(controller);
|
|
2661
|
+
});
|
|
2662
|
+
if ((this.domImageResourceVersions.get(name) ?? 0) !== version || !this.assetRegistry.has(name)) {
|
|
2663
|
+
resource.release();
|
|
2664
|
+
throw this.cancelledDOMImageResolution(name);
|
|
2665
|
+
}
|
|
2666
|
+
controller = {
|
|
2667
|
+
name,
|
|
2668
|
+
resource
|
|
2669
|
+
};
|
|
2670
|
+
this.activeDOMImageResources.add(controller);
|
|
2671
|
+
const named = this.activeDOMImageResourcesByName.get(name) ?? /* @__PURE__ */ new Set();
|
|
2672
|
+
named.add(controller);
|
|
2673
|
+
this.activeDOMImageResourcesByName.set(name, named);
|
|
2674
|
+
return resource;
|
|
2675
|
+
}
|
|
2676
|
+
releaseActiveDOMImageResource(controller) {
|
|
2677
|
+
this.activeDOMImageResources.delete(controller);
|
|
2678
|
+
const named = this.activeDOMImageResourcesByName.get(controller.name);
|
|
2679
|
+
named?.delete(controller);
|
|
2680
|
+
if (named?.size === 0) this.activeDOMImageResourcesByName.delete(controller.name);
|
|
2681
|
+
}
|
|
2682
|
+
invalidateDOMImageResources(name) {
|
|
2683
|
+
this.domImageResourceVersions.set(name, (this.domImageResourceVersions.get(name) ?? 0) + 1);
|
|
2684
|
+
this.domImageResourceBackings.delete(name);
|
|
2685
|
+
for (const controller of [...this.activeDOMImageResourcesByName.get(name) ?? []]) controller.resource.release();
|
|
2686
|
+
}
|
|
2687
|
+
releaseAllDOMImageResources() {
|
|
2688
|
+
const names = /* @__PURE__ */ new Set([...this.domImageResourceBackings.keys(), ...this.activeDOMImageResourcesByName.keys()]);
|
|
2689
|
+
for (const name of names) this.domImageResourceVersions.set(name, (this.domImageResourceVersions.get(name) ?? 0) + 1);
|
|
2690
|
+
this.domImageResourceBackings.clear();
|
|
2691
|
+
for (const controller of [...this.activeDOMImageResources]) controller.resource.release();
|
|
2692
|
+
}
|
|
2693
|
+
startListeningForDOMImageLifecycle() {
|
|
2694
|
+
if (this.listeningForDOMImageLifecycle || !this.runtime.on) return;
|
|
2695
|
+
this.listeningForDOMImageLifecycle = true;
|
|
2696
|
+
this.runtime.on("PROJECT_STOP_ALL", this.releaseAllDOMImageResourcesForLifecycle);
|
|
2697
|
+
this.runtime.on("PROJECT_LOADED", this.releaseAllDOMImageResourcesForLifecycle);
|
|
2698
|
+
this.runtime.on("RUNTIME_DISPOSED", this.releaseDOMImageResourcesForRuntimeDispose);
|
|
2699
|
+
}
|
|
2700
|
+
stopListeningForDOMImageLifecycle() {
|
|
2701
|
+
if (!this.listeningForDOMImageLifecycle || !this.runtime.off) return;
|
|
2702
|
+
this.listeningForDOMImageLifecycle = false;
|
|
2703
|
+
this.runtime.off("PROJECT_STOP_ALL", this.releaseAllDOMImageResourcesForLifecycle);
|
|
2704
|
+
this.runtime.off("PROJECT_LOADED", this.releaseAllDOMImageResourcesForLifecycle);
|
|
2705
|
+
this.runtime.off("RUNTIME_DISPOSED", this.releaseDOMImageResourcesForRuntimeDispose);
|
|
2706
|
+
}
|
|
2707
|
+
createNamedBodyProvider() {
|
|
2708
|
+
let providerReleased = false;
|
|
2709
|
+
const requireProvider = () => {
|
|
2710
|
+
if (providerReleased) throw new NamedDataError("NAMED_DATA_PROVIDER_RELEASED", "The named asset body provider has been released.");
|
|
2711
|
+
};
|
|
2712
|
+
return Object.freeze({
|
|
2713
|
+
namespace: NAMED_ASSET_BODY_NAMESPACE,
|
|
2714
|
+
kind: "asset",
|
|
2715
|
+
canResolve: (reference, representation) => !providerReleased && reference.namespace === "asset" && reference.kind === "asset" && reference.scope === "project" && representation === "raw",
|
|
2716
|
+
stat: async (reference, representation, context) => {
|
|
2717
|
+
requireProvider();
|
|
2718
|
+
const resolved = await this.resolveNamedBodySource(reference, representation, context);
|
|
2719
|
+
requireProvider();
|
|
2720
|
+
return resolved.metadata;
|
|
2721
|
+
},
|
|
2722
|
+
openBody: async (reference, representation, context) => {
|
|
2723
|
+
requireProvider();
|
|
2724
|
+
const lifecycleVersion = this.namedBodyLifecycleVersion;
|
|
2725
|
+
const { metadata, data } = await this.resolveNamedBodySource(reference, representation, context);
|
|
2726
|
+
requireProvider();
|
|
2727
|
+
if (this.namedBodyLifecycleVersion !== lifecycleVersion) throw namedBodyAbortError();
|
|
2728
|
+
throwIfNamedBodyAborted(context.signal);
|
|
2729
|
+
let body = new Uint8Array(data.slice(0));
|
|
2730
|
+
let released = false;
|
|
2731
|
+
let snapshot;
|
|
2732
|
+
const abortRelease = () => release("abort");
|
|
2733
|
+
const release = (reason) => {
|
|
2734
|
+
if (released) return;
|
|
2735
|
+
released = true;
|
|
2736
|
+
body = null;
|
|
2737
|
+
context.signal?.removeEventListener("abort", abortRelease);
|
|
2738
|
+
this.openNamedBodySnapshots.delete(snapshot);
|
|
2739
|
+
};
|
|
2740
|
+
snapshot = Object.freeze({
|
|
2741
|
+
...metadata,
|
|
2742
|
+
get body() {
|
|
2743
|
+
if (!body) throw new NamedDataError("NAMED_DATA_PROVIDER_RELEASED", "The named asset body snapshot has been released.");
|
|
2744
|
+
return body;
|
|
2745
|
+
},
|
|
2746
|
+
release
|
|
2747
|
+
});
|
|
2748
|
+
this.openNamedBodySnapshots.add(snapshot);
|
|
2749
|
+
context.signal?.addEventListener("abort", abortRelease, { once: true });
|
|
2750
|
+
if (context.signal?.aborted) {
|
|
2751
|
+
release("abort");
|
|
2752
|
+
throw namedBodyAbortError();
|
|
2753
|
+
}
|
|
2754
|
+
return snapshot;
|
|
2755
|
+
},
|
|
2756
|
+
clearSession: () => {
|
|
2757
|
+
this.releaseOpenNamedBodySnapshots();
|
|
2758
|
+
},
|
|
2759
|
+
release: (reason) => {
|
|
2760
|
+
if (providerReleased) return;
|
|
2761
|
+
providerReleased = true;
|
|
2762
|
+
this.releaseOpenNamedBodySnapshots();
|
|
2763
|
+
this.stopListeningForNamedBodyLifecycle();
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
async resolveNamedBodySource(referenceInput, representation, context) {
|
|
2768
|
+
throwIfNamedBodyAborted(context.signal);
|
|
2769
|
+
const reference = requireNamedAssetBodyReference(referenceInput);
|
|
2770
|
+
if (reference.scope !== "project" || context.project === void 0) throw new NamedDataError("NAMED_DATA_SCOPE_MISMATCH", "Named asset bodies require project scope and project context.");
|
|
2771
|
+
if (representation !== "raw") throw new NamedDataError("NAMED_DATA_REPRESENTATION_UNSUPPORTED", `Named assets cannot be rendered as ${representation}.`);
|
|
2772
|
+
const name = normalizeName(reference.name);
|
|
2773
|
+
await this.registrationCommits.get(name)?.catch(() => void 0);
|
|
2774
|
+
throwIfNamedBodyAborted(context.signal);
|
|
2775
|
+
const kind = this.assetRegistry.get(name);
|
|
2776
|
+
if (!kind) throw new NamedDataError("NAMED_DATA_NOT_FOUND", `No registered asset has the name ${JSON.stringify(name)}.`);
|
|
2777
|
+
if (kind !== "external") throw new NamedDataError("NAMED_DATA_REPRESENTATION_UNSUPPORTED", `Asset ${JSON.stringify(name)} is not backed by session memory.`);
|
|
2778
|
+
const asset = this.externalAssets.get(name);
|
|
2779
|
+
if (!asset) throw new NamedDataError("NAMED_DATA_NOT_FOUND", `No in-memory asset has the name ${JSON.stringify(name)}.`);
|
|
2780
|
+
return {
|
|
2781
|
+
metadata: Object.freeze({
|
|
2782
|
+
reference: Object.freeze({
|
|
2783
|
+
...reference,
|
|
2784
|
+
name
|
|
2785
|
+
}),
|
|
2786
|
+
nativeRepresentation: "raw",
|
|
2787
|
+
representation: "raw",
|
|
2788
|
+
mediaType: normalizeMimeType(asset.mimeType, asset.url || name),
|
|
2789
|
+
byteLength: asset.data.byteLength,
|
|
2790
|
+
digest: await sha256Digest(new Uint8Array(asset.data)),
|
|
2791
|
+
revision: String(this.successfulRegistrationVersions.get(name) ?? 0),
|
|
2792
|
+
replayable: true
|
|
2793
|
+
}),
|
|
2794
|
+
data: asset.data
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
releaseOpenNamedBodySnapshots() {
|
|
2798
|
+
this.namedBodyLifecycleVersion += 1;
|
|
2799
|
+
for (const snapshot of [...this.openNamedBodySnapshots]) snapshot.release();
|
|
2800
|
+
}
|
|
2801
|
+
startListeningForNamedBodyLifecycle() {
|
|
2802
|
+
if (this.listeningForNamedBodyLifecycle || !this.runtime.on) return;
|
|
2803
|
+
this.listeningForNamedBodyLifecycle = true;
|
|
2804
|
+
this.runtime.on("PROJECT_STOP_ALL", this.releaseNamedBodySnapshotsForLifecycle);
|
|
2805
|
+
this.runtime.on("PROJECT_LOADED", this.releaseNamedBodySnapshotsForLifecycle);
|
|
2806
|
+
this.runtime.on("RUNTIME_DISPOSED", this.unregisterNamedBodyProviderForRuntimeDispose);
|
|
2807
|
+
}
|
|
2808
|
+
stopListeningForNamedBodyLifecycle() {
|
|
2809
|
+
if (!this.listeningForNamedBodyLifecycle || !this.runtime.off) return;
|
|
2810
|
+
this.listeningForNamedBodyLifecycle = false;
|
|
2811
|
+
this.runtime.off("PROJECT_STOP_ALL", this.releaseNamedBodySnapshotsForLifecycle);
|
|
2812
|
+
this.runtime.off("PROJECT_LOADED", this.releaseNamedBodySnapshotsForLifecycle);
|
|
2813
|
+
this.runtime.off("RUNTIME_DISPOSED", this.unregisterNamedBodyProviderForRuntimeDispose);
|
|
2814
|
+
}
|
|
2815
|
+
async ensureExternalAssetSkin(asset, name) {
|
|
2816
|
+
asset.mimeType = normalizeMimeType(asset.mimeType, asset.url || name);
|
|
2817
|
+
if (!asset.mimeType.startsWith("image/")) throw this.assetTypeMismatch("show", name, "image", `external/${this.externalMediaKind(asset)}`);
|
|
2818
|
+
if (asset.skinId !== null) return asset.skinId;
|
|
2819
|
+
const blob = new Blob([asset.data], { type: asset.mimeType });
|
|
2820
|
+
asset.skinId = asset.mimeType === "image/svg+xml" ? this.renderer.createSVGSkin(await blob.text()) : this.renderer.createBitmapSkin(await createImageBitmap(blob), asset.bitmapResolution ?? 1);
|
|
2821
|
+
return asset.skinId;
|
|
2822
|
+
}
|
|
2823
|
+
async resolveSkinFromAsset(name, kind, asset) {
|
|
2824
|
+
if (kind === "external" && asset.kind === "external") return {
|
|
2825
|
+
skinId: await this.ensureExternalAssetSkin(asset, name),
|
|
2826
|
+
sourceSize: null
|
|
2827
|
+
};
|
|
2828
|
+
if ((kind === "costume" || kind === "backdrop") && (asset.kind === "costume" || asset.kind === "backdrop")) {
|
|
2829
|
+
const { target, costume } = this.resolveCostumeAssetReference(name, asset);
|
|
2830
|
+
return {
|
|
2831
|
+
skinId: costume.skinId,
|
|
2832
|
+
sourceSize: target.isStage || !Number.isFinite(target.size) ? null : target.size
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
throw this.assetTypeMismatch("show", name, "image", kind);
|
|
2836
|
+
}
|
|
2837
|
+
resolveCostumeReference(name) {
|
|
2838
|
+
const reference = this.costumeAssets.get(name);
|
|
2839
|
+
if (!reference) throw this.assetNotRegistered("show", name);
|
|
2840
|
+
return this.resolveCostumeAssetReference(name, reference);
|
|
2841
|
+
}
|
|
2842
|
+
resolveCostumeAssetReference(name, reference) {
|
|
2843
|
+
const target = this.resolveReferencedTarget(reference.targetId, reference.targetName, reference.isStage);
|
|
2844
|
+
const costume = this.findCostume(target, reference.costumeName, reference.assetId);
|
|
2845
|
+
if (!costume) {
|
|
2846
|
+
const candidates = suggestNames(reference.costumeName, (target.sprite?.costumes ?? []).map((candidate) => candidate.name));
|
|
2847
|
+
throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", `Costume no longer exists: ${reference.targetName}/${reference.costumeName}.`, {
|
|
2848
|
+
operation: "show",
|
|
2849
|
+
assetName: name,
|
|
2850
|
+
actorName: reference.targetName,
|
|
2851
|
+
candidates,
|
|
2852
|
+
hint: suggestionHint(candidates)
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
if (typeof costume.skinId !== "number") throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", `Costume skin is not available: ${reference.targetName}/${reference.costumeName}.`, {
|
|
2856
|
+
operation: "show",
|
|
2857
|
+
assetName: name,
|
|
2858
|
+
actorName: reference.targetName,
|
|
2859
|
+
hint: "Wait for the project costume to finish loading and try again."
|
|
2860
|
+
});
|
|
2861
|
+
return {
|
|
2862
|
+
target,
|
|
2863
|
+
costume
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
resolveSoundAssetReference(name) {
|
|
2867
|
+
const reference = this.soundAssets.get(name);
|
|
2868
|
+
if (!reference) throw this.assetNotRegistered("playSound", name);
|
|
2869
|
+
const target = this.resolveReferencedTarget(reference.targetId, reference.targetName, reference.isStage);
|
|
2870
|
+
const sound = this.findSound(target, reference.soundName, reference.assetId);
|
|
2871
|
+
if (!sound) {
|
|
2872
|
+
const candidates = suggestNames(reference.soundName, (target.sprite?.sounds ?? []).map((candidate) => candidate.name));
|
|
2873
|
+
throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", `Sound no longer exists: ${reference.targetName}/${reference.soundName}.`, {
|
|
2874
|
+
operation: "playSound",
|
|
2875
|
+
assetName: name,
|
|
2876
|
+
actorName: reference.targetName,
|
|
2877
|
+
candidates,
|
|
2878
|
+
hint: suggestionHint(candidates)
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
return {
|
|
2882
|
+
target,
|
|
2883
|
+
sound
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
resolveSoundReference(name) {
|
|
2887
|
+
const { target, sound } = this.resolveSoundAssetReference(name);
|
|
2888
|
+
if (!sound.soundId) throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", `Sound ID is not available: ${target.sprite?.name ?? target.id}/${sound.name}.`, {
|
|
2889
|
+
operation: "playSound",
|
|
2890
|
+
assetName: name,
|
|
2891
|
+
actorName: target.sprite?.name ?? target.id,
|
|
2892
|
+
hint: "Wait for the project sound to finish loading and try again."
|
|
2893
|
+
});
|
|
2894
|
+
const soundBank = target.sprite?.soundBank;
|
|
2895
|
+
if (!soundBank) throw new AssetManagerError("DEPENDENCY_MISSING", `Sound bank is not available: ${target.sprite?.name ?? target.id}.`, {
|
|
2896
|
+
operation: "playSound",
|
|
2897
|
+
assetName: name,
|
|
2898
|
+
actorName: target.sprite?.name ?? target.id,
|
|
2899
|
+
hint: "Use a TurboWarp runtime with sound support."
|
|
2900
|
+
});
|
|
2901
|
+
return {
|
|
2902
|
+
target,
|
|
2903
|
+
sound,
|
|
2904
|
+
soundBank
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
deleteOwnedSkinIfExists(asset) {
|
|
2908
|
+
if (!asset || asset.skinId === null) return;
|
|
2909
|
+
try {
|
|
2910
|
+
this.renderer.destroySkin(asset.skinId);
|
|
2911
|
+
} catch (error) {
|
|
2912
|
+
console.warn("Failed to destroy skin", error);
|
|
2913
|
+
}
|
|
2914
|
+
asset.skinId = null;
|
|
2915
|
+
}
|
|
2916
|
+
applySkinToTarget(target, skin) {
|
|
2917
|
+
if (target.drawableID === void 0 || target.drawableID === null) throw new AssetManagerError("SPRITE_NOT_FOUND", `Target drawable not found: ${target.sprite?.name ?? "unknown"}.`, {
|
|
2918
|
+
operation: "show",
|
|
2919
|
+
actorName: target.sprite?.name ?? target.id,
|
|
2920
|
+
hint: "Use a live target with an initialized renderer drawable."
|
|
2921
|
+
});
|
|
2922
|
+
this.renderer.updateDrawableSkinId(target.drawableID, skin.skinId);
|
|
2923
|
+
if (!target.isStage && target.isOriginal && skin.sourceSize !== null && target.size !== skin.sourceSize) target.setSize(skin.sourceSize);
|
|
2924
|
+
target.emitVisualChange?.();
|
|
2925
|
+
this.runtime.requestRedraw?.();
|
|
2926
|
+
}
|
|
2927
|
+
async playResolvedSound(value, waitUntilDone) {
|
|
2928
|
+
const name = normalizeName(value);
|
|
2929
|
+
const kind = this.assetRegistry.get(name);
|
|
2930
|
+
if (!kind) throw this.assetNotRegistered("playSound", name);
|
|
2931
|
+
if (kind === "external") {
|
|
2932
|
+
await this.playExternalSound(name, waitUntilDone);
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
if (kind === "sound") {
|
|
2936
|
+
await this.playProjectSound(name, waitUntilDone);
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
throw this.assetTypeMismatch("playSound", name, "audio", kind);
|
|
2940
|
+
}
|
|
2941
|
+
async resolveAudioBytes(name) {
|
|
2942
|
+
const kind = this.assetRegistry.get(name);
|
|
2943
|
+
if (!kind) throw this.assetNotRegistered("createAudioVoice", name);
|
|
2944
|
+
if (kind === "external") {
|
|
2945
|
+
const asset = this.externalAssets.get(name);
|
|
2946
|
+
if (!asset) throw this.assetNotRegistered("createAudioVoice", name);
|
|
2947
|
+
asset.mimeType = normalizeMimeType(asset.mimeType, asset.url || name);
|
|
2948
|
+
if (!asset.mimeType.startsWith("audio/")) throw this.assetTypeMismatch("createAudioVoice", name, "audio", `external/${this.externalMediaKind(asset)}`);
|
|
2949
|
+
return {
|
|
2950
|
+
bytes: asset.data,
|
|
2951
|
+
mimeType: asset.mimeType
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
if (kind !== "sound") throw this.assetTypeMismatch("createAudioVoice", name, "audio", kind);
|
|
2955
|
+
const { target, sound } = this.resolveSoundAssetReference(name);
|
|
2956
|
+
const assetId = sound.assetId;
|
|
2957
|
+
const dataFormat = sound.dataFormat;
|
|
2958
|
+
const storage = this.runtime.storage;
|
|
2959
|
+
let asset = sound.asset ?? (assetId ? storage?.get?.(assetId) : null);
|
|
2960
|
+
if (!asset && assetId && dataFormat && storage?.load && storage.AssetType.Sound !== void 0) try {
|
|
2961
|
+
asset = await storage.load(storage.AssetType.Sound, assetId, dataFormat);
|
|
2962
|
+
} catch (error) {
|
|
2963
|
+
throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", `Sound bytes could not be loaded: ${target.sprite?.name ?? target.id}/${sound.name}.`, {
|
|
2964
|
+
operation: "createAudioVoice",
|
|
2965
|
+
assetName: name,
|
|
2966
|
+
actorName: target.sprite?.name ?? target.id,
|
|
2967
|
+
hint: "Wait for the project sound to finish loading and try again.",
|
|
2968
|
+
cause: error
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
if (!asset?.data) throw new AssetManagerError("SOURCE_ASSET_NOT_FOUND", `Sound bytes are not available: ${target.sprite?.name ?? target.id}/${sound.name}.`, {
|
|
2972
|
+
operation: "createAudioVoice",
|
|
2973
|
+
assetName: name,
|
|
2974
|
+
actorName: target.sprite?.name ?? target.id,
|
|
2975
|
+
hint: "Use a TurboWarp runtime that retains or can reload project sound assets."
|
|
2976
|
+
});
|
|
2977
|
+
return {
|
|
2978
|
+
bytes: asset.data,
|
|
2979
|
+
mimeType: this.projectAssetMimeType(dataFormat, "audio")
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
startBrowserAudioVoice(name, bytes, mimeType, initialGain, operation) {
|
|
2983
|
+
const blobBytes = bytes instanceof Uint8Array ? new Uint8Array(bytes).buffer : bytes;
|
|
2984
|
+
const objectUrl = URL.createObjectURL(new Blob([blobBytes], { type: mimeType }));
|
|
2985
|
+
let audio;
|
|
2986
|
+
try {
|
|
2987
|
+
audio = new Audio(objectUrl);
|
|
2988
|
+
audio.volume = initialGain;
|
|
2989
|
+
} catch (error) {
|
|
2990
|
+
URL.revokeObjectURL(objectUrl);
|
|
2991
|
+
throw this.playbackError(name, error, operation);
|
|
2992
|
+
}
|
|
2993
|
+
let active = true;
|
|
2994
|
+
let resolveEnded;
|
|
2995
|
+
let rejectEnded;
|
|
2996
|
+
const ended = new Promise((resolve, reject) => {
|
|
2997
|
+
resolveEnded = resolve;
|
|
2998
|
+
rejectEnded = reject;
|
|
2999
|
+
});
|
|
3000
|
+
ended.catch(() => {});
|
|
3001
|
+
const cleanup = (error) => {
|
|
3002
|
+
if (!active) return;
|
|
3003
|
+
active = false;
|
|
3004
|
+
audio.removeEventListener("ended", handleEnded);
|
|
3005
|
+
audio.removeEventListener("error", handleError);
|
|
3006
|
+
this.playingAudio.delete(audio);
|
|
3007
|
+
this.audioVoiceStops.delete(audio);
|
|
3008
|
+
URL.revokeObjectURL(objectUrl);
|
|
3009
|
+
if (error === void 0) resolveEnded();
|
|
3010
|
+
else rejectEnded(error);
|
|
3011
|
+
};
|
|
3012
|
+
const handleEnded = () => cleanup();
|
|
3013
|
+
const handleError = () => cleanup(operation === "createAudioVoice" ? this.playbackError(name, /* @__PURE__ */ new Error("The browser audio element reported a playback error."), operation) : void 0);
|
|
3014
|
+
const stop = () => {
|
|
3015
|
+
if (!active) return;
|
|
3016
|
+
try {
|
|
3017
|
+
audio.pause();
|
|
3018
|
+
audio.currentTime = 0;
|
|
3019
|
+
} catch {}
|
|
3020
|
+
cleanup();
|
|
3021
|
+
};
|
|
3022
|
+
const voice = Object.freeze({
|
|
3023
|
+
ended,
|
|
3024
|
+
setGain(value) {
|
|
3025
|
+
const gain = normalizeAudioVoiceGain(value);
|
|
3026
|
+
if (active) audio.volume = gain;
|
|
3027
|
+
},
|
|
3028
|
+
stop
|
|
3029
|
+
});
|
|
3030
|
+
audio.addEventListener("ended", handleEnded, { once: true });
|
|
3031
|
+
audio.addEventListener("error", handleError, { once: true });
|
|
3032
|
+
this.playingAudio.set(audio, name);
|
|
3033
|
+
this.audioVoiceStops.set(audio, stop);
|
|
3034
|
+
let playResult;
|
|
3035
|
+
try {
|
|
3036
|
+
playResult = audio.play();
|
|
3037
|
+
} catch (error) {
|
|
3038
|
+
const playbackError = this.playbackError(name, error, operation);
|
|
3039
|
+
cleanup(playbackError);
|
|
3040
|
+
return {
|
|
3041
|
+
voice,
|
|
3042
|
+
started: Promise.reject(playbackError)
|
|
3043
|
+
};
|
|
3044
|
+
}
|
|
3045
|
+
return {
|
|
3046
|
+
voice,
|
|
3047
|
+
started: Promise.resolve(playResult).catch((error) => {
|
|
3048
|
+
const playbackError = this.playbackError(name, error, operation);
|
|
3049
|
+
cleanup(playbackError);
|
|
3050
|
+
throw playbackError;
|
|
3051
|
+
})
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
async playExternalSound(name, waitUntilDone) {
|
|
3055
|
+
const asset = this.externalAssets.get(name);
|
|
3056
|
+
if (!asset) throw this.assetNotRegistered("playSound", name);
|
|
3057
|
+
asset.mimeType = normalizeMimeType(asset.mimeType, asset.url || name);
|
|
3058
|
+
if (!asset.mimeType.startsWith("audio/")) throw this.assetTypeMismatch("playSound", name, "audio", `external/${this.externalMediaKind(asset)}`);
|
|
3059
|
+
const playback = this.startBrowserAudioVoice(name, asset.data, asset.mimeType, 1, "playSound");
|
|
3060
|
+
if (!waitUntilDone) {
|
|
3061
|
+
playback.started.catch((error) => console.error(error));
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
await playback.started;
|
|
3065
|
+
await playback.voice.ended;
|
|
3066
|
+
}
|
|
3067
|
+
stopExternalAudio(audio) {
|
|
3068
|
+
const stopVoice = this.audioVoiceStops.get(audio);
|
|
3069
|
+
if (stopVoice) {
|
|
3070
|
+
stopVoice();
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
try {
|
|
3074
|
+
audio.pause();
|
|
3075
|
+
audio.currentTime = 0;
|
|
3076
|
+
audio.dispatchEvent(new Event("ended"));
|
|
3077
|
+
} catch {} finally {
|
|
3078
|
+
this.playingAudio.delete(audio);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
async playProjectSound(name, waitUntilDone) {
|
|
3082
|
+
const { target, sound, soundBank } = this.resolveSoundReference(name);
|
|
3083
|
+
const playResult = soundBank.playSound(target, sound.soundId);
|
|
3084
|
+
const playPromise = Promise.resolve(playResult);
|
|
3085
|
+
if (!waitUntilDone) {
|
|
3086
|
+
playPromise.catch((error) => console.error(this.playbackError(name, error)));
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
try {
|
|
3090
|
+
await playPromise;
|
|
3091
|
+
} catch (error) {
|
|
3092
|
+
throw this.playbackError(name, error);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
playbackError(name, cause, operation = "playSound") {
|
|
3096
|
+
return new AssetManagerError("PLAYBACK_FAILED", `Failed to play audio asset "${name}": ${errorMessage(cause)}`, {
|
|
3097
|
+
operation,
|
|
3098
|
+
assetName: name,
|
|
3099
|
+
hint: "Check browser audio permissions and the registered audio resource.",
|
|
3100
|
+
cause
|
|
3101
|
+
});
|
|
3102
|
+
}
|
|
3103
|
+
projectAssetMimeType(dataFormat, kind) {
|
|
3104
|
+
if (dataFormat) {
|
|
3105
|
+
const guessed = guessMimeType(`asset.${dataFormat}`);
|
|
3106
|
+
if (guessed !== "application/octet-stream") return guessed;
|
|
3107
|
+
}
|
|
3108
|
+
return kind === "image" ? "image/x-scratch-costume" : "audio/x-scratch-sound";
|
|
3109
|
+
}
|
|
3110
|
+
};
|
|
3111
|
+
//#endregion
|
|
3112
|
+
//#region src/animation.ts
|
|
3113
|
+
/**
|
|
3114
|
+
* Asset Cache with actor-level background asset animation.
|
|
3115
|
+
*
|
|
3116
|
+
* ACTOR is resolved from a clone-local actorName variable before falling back
|
|
3117
|
+
* to the existing named-sprite behaviour of setSpriteSkin.
|
|
3118
|
+
* ASSETS and DURATIONS are comma-separated strings. ASSETS contains registered
|
|
3119
|
+
* image or audio asset names. Each duration is the interval before the next
|
|
3120
|
+
* action, and a zero groups those adjacent actions. Loop has one interval per
|
|
3121
|
+
* action; sequence omits the final interval. COSTUMES remains accepted as a
|
|
3122
|
+
* compatibility alias for projects saved with the earlier block.
|
|
3123
|
+
*/
|
|
3124
|
+
var AnimatedAssetManagerExtension = class extends AssetManagerExtension {
|
|
3125
|
+
constructor(featureFlags = FEATURE_FLAGS) {
|
|
3126
|
+
super(featureFlags);
|
|
3127
|
+
_defineProperty(this, "actorAnimations", /* @__PURE__ */ new Map());
|
|
3128
|
+
_defineProperty(this, "animationGeneration", 0);
|
|
3129
|
+
const stopAll = () => this.stopAllActorAnimations();
|
|
3130
|
+
const stopTarget = (target) => {
|
|
3131
|
+
if (target) this.stopTarget(target);
|
|
3132
|
+
};
|
|
3133
|
+
Scratch.vm.runtime.on?.("PROJECT_STOP_ALL", stopAll);
|
|
3134
|
+
Scratch.vm.runtime.on?.("PROJECT_START", stopAll);
|
|
3135
|
+
Scratch.vm.runtime.on?.("STOP_FOR_TARGET", stopTarget);
|
|
3136
|
+
Scratch.vm.runtime.on?.("RUNTIME_DISPOSED", stopAll);
|
|
3137
|
+
}
|
|
3138
|
+
async setThisSpriteSkin(args, util) {
|
|
3139
|
+
this.stopTarget(util.target);
|
|
3140
|
+
await super.setThisSpriteSkin(args, util);
|
|
3141
|
+
}
|
|
3142
|
+
async setSpriteSkin(args, util) {
|
|
3143
|
+
const actor = this.requireActorName(args.ACTOR ?? args.SPRITE);
|
|
3144
|
+
const target = this.resolveActorTarget(actor, util);
|
|
3145
|
+
this.stopActor(actor);
|
|
3146
|
+
await this.applyAssetToTarget(target, args.NAME, util);
|
|
3147
|
+
}
|
|
3148
|
+
startActorLoop(args, util) {
|
|
3149
|
+
const actor = this.requireActorName(args.ACTOR);
|
|
3150
|
+
const target = this.resolveActorTarget(actor, util);
|
|
3151
|
+
const assets = this.getAnimationAssetsInput(args);
|
|
3152
|
+
if (!assets.text) {
|
|
3153
|
+
if (normalizeName(args.DURATIONS)) throw new Error(`DURATIONS must be empty when ${assets.argumentName} is empty.`);
|
|
3154
|
+
this.stopActor(actor);
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
this.startActorAnimation(actor, target, this.parseAnimation(assets.text, args.DURATIONS, assets.argumentName, "loop"));
|
|
3158
|
+
}
|
|
3159
|
+
startActorSequence(args, util) {
|
|
3160
|
+
const actor = this.requireActorName(args.ACTOR);
|
|
3161
|
+
const target = this.resolveActorTarget(actor, util);
|
|
3162
|
+
const assets = this.getAnimationAssetsInput(args);
|
|
3163
|
+
if (!assets.text) throw new Error(`${assets.argumentName} is empty.`);
|
|
3164
|
+
this.startActorAnimation(actor, target, this.parseAnimation(assets.text, args.DURATIONS, assets.argumentName, "sequence"));
|
|
3165
|
+
}
|
|
3166
|
+
stopActorAnimation(args, util) {
|
|
3167
|
+
const actor = this.requireActorName(args.ACTOR);
|
|
3168
|
+
this.resolveActorTarget(actor, util);
|
|
3169
|
+
this.stopActor(actor);
|
|
3170
|
+
}
|
|
3171
|
+
async finishAllActorSequences() {
|
|
3172
|
+
const pending = [];
|
|
3173
|
+
for (const [actor, state] of [...this.actorAnimations]) {
|
|
3174
|
+
if (state.mode !== "sequence") continue;
|
|
3175
|
+
const finalImage = [...state.actions].reverse().find((action) => action.kind === "image");
|
|
3176
|
+
this.stopActor(actor);
|
|
3177
|
+
if (!finalImage || !this.runtime.targets.includes(state.target)) continue;
|
|
3178
|
+
pending.push(this.resolveSkin(finalImage.assetName).then((skin) => {
|
|
3179
|
+
if (this.runtime.targets.includes(state.target)) this.applyResolvedSkinToTarget(state.target, finalImage.assetName, skin);
|
|
3180
|
+
}));
|
|
3181
|
+
}
|
|
3182
|
+
await Promise.all(pending);
|
|
3183
|
+
}
|
|
3184
|
+
deleteAllMemoryAssets() {
|
|
3185
|
+
this.stopAllActorAnimations();
|
|
3186
|
+
super.deleteAllMemoryAssets();
|
|
3187
|
+
}
|
|
3188
|
+
getAnimationAssetsInput(args) {
|
|
3189
|
+
if (args.ASSETS !== void 0) return {
|
|
3190
|
+
text: normalizeName(args.ASSETS),
|
|
3191
|
+
argumentName: "ASSETS"
|
|
3192
|
+
};
|
|
3193
|
+
return {
|
|
3194
|
+
text: normalizeName(args.COSTUMES),
|
|
3195
|
+
argumentName: "COSTUMES"
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
requireActorName(value) {
|
|
3199
|
+
const actor = normalizeName(value);
|
|
3200
|
+
if (!actor) throw new AssetManagerError("SPRITE_NOT_FOUND", "Actor name is empty.", {
|
|
3201
|
+
operation: "resolveActor",
|
|
3202
|
+
actorName: actor,
|
|
3203
|
+
hint: "Provide the name of a project sprite or actor clone."
|
|
3204
|
+
});
|
|
3205
|
+
return actor;
|
|
3206
|
+
}
|
|
3207
|
+
actorNameOf(target) {
|
|
3208
|
+
return normalizeName(target.lookupVariableByNameAndType?.("actorName", "")?.value);
|
|
3209
|
+
}
|
|
3210
|
+
resolveActorTarget(actor, util) {
|
|
3211
|
+
const invokingTarget = util?.target;
|
|
3212
|
+
if (invokingTarget && !invokingTarget.isStage && (this.actorNameOf(invokingTarget) === actor || invokingTarget.sprite?.name === actor)) return invokingTarget;
|
|
3213
|
+
const actorNameMatches = this.runtime.targets.filter((target) => !target.isStage && this.actorNameOf(target) === actor);
|
|
3214
|
+
const matches = actorNameMatches.length > 0 ? actorNameMatches : this.runtime.targets.filter((target) => !target.isStage && target.sprite?.name === actor);
|
|
3215
|
+
if (matches.length > 1) throw new AssetManagerError("SPRITE_NAME_AMBIGUOUS", `Actor name is not unique: ${actor}.`, {
|
|
3216
|
+
operation: "resolveActor",
|
|
3217
|
+
actorName: actor,
|
|
3218
|
+
hint: "Give every actor target a unique name."
|
|
3219
|
+
});
|
|
3220
|
+
const target = matches[0] ?? this.findTargetByName(actor);
|
|
3221
|
+
if (!target) {
|
|
3222
|
+
const candidates = suggestNames(actor, [...new Set(this.runtime.targets.flatMap((candidate) => {
|
|
3223
|
+
if (candidate.isStage) return [];
|
|
3224
|
+
return [this.actorNameOf(candidate), candidate.sprite?.name ?? ""].filter(Boolean);
|
|
3225
|
+
}))]);
|
|
3226
|
+
throw new AssetManagerError("SPRITE_NOT_FOUND", `Actor not found: ${actor}.`, {
|
|
3227
|
+
operation: "resolveActor",
|
|
3228
|
+
actorName: actor,
|
|
3229
|
+
candidates,
|
|
3230
|
+
hint: suggestionHint(candidates)
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
return target;
|
|
3234
|
+
}
|
|
3235
|
+
parseAnimation(assetsValue, durationsValue, argumentName, mode) {
|
|
3236
|
+
const assetNames = String(assetsValue ?? "").split(",").map((value) => value.trim());
|
|
3237
|
+
const durationsText = normalizeName(durationsValue);
|
|
3238
|
+
const durationTexts = durationsText ? durationsText.split(",").map((value) => value.trim()) : [];
|
|
3239
|
+
if (assetNames.some((name) => !name)) throw new Error(`${argumentName} contains an empty item.`);
|
|
3240
|
+
if (durationTexts.some((duration) => !duration)) throw new Error("DURATIONS contains an empty item.");
|
|
3241
|
+
const expectedDurationCount = mode === "loop" ? assetNames.length : assetNames.length - 1;
|
|
3242
|
+
if (durationTexts.length !== expectedDurationCount) throw new Error(`${mode} requires ${expectedDurationCount} DURATIONS items for ${assetNames.length} ${argumentName} items, but received ${durationTexts.length}.`);
|
|
3243
|
+
const intervalsMs = durationTexts.map((duration, index) => {
|
|
3244
|
+
const seconds = Number(duration);
|
|
3245
|
+
if (!Number.isFinite(seconds) || seconds < 0) throw new Error(`DURATIONS item ${index + 1} must be a non-negative number: ${duration}`);
|
|
3246
|
+
return seconds * 1e3;
|
|
3247
|
+
});
|
|
3248
|
+
if (mode === "loop" && !intervalsMs.some((durationMs) => durationMs > 0)) throw new Error("DURATIONS for loop must contain at least one positive number.");
|
|
3249
|
+
return {
|
|
3250
|
+
actions: assetNames.map((assetName) => this.createAnimationAction(assetName)),
|
|
3251
|
+
intervalsMs,
|
|
3252
|
+
mode
|
|
3253
|
+
};
|
|
3254
|
+
}
|
|
3255
|
+
startActorAnimation(actor, target, definition) {
|
|
3256
|
+
this.stopActor(actor);
|
|
3257
|
+
const state = {
|
|
3258
|
+
...definition,
|
|
3259
|
+
actor,
|
|
3260
|
+
target,
|
|
3261
|
+
actionIndex: 0,
|
|
3262
|
+
deadline: performance.now(),
|
|
3263
|
+
timer: null,
|
|
3264
|
+
generation: ++this.animationGeneration
|
|
3265
|
+
};
|
|
3266
|
+
this.actorAnimations.set(actor, state);
|
|
3267
|
+
this.showCurrentStep(actor, state);
|
|
3268
|
+
}
|
|
3269
|
+
createAnimationAction(assetName) {
|
|
3270
|
+
if (!this.isLoaded({ NAME: assetName })) throw this.assetNotRegistered("animate", assetName);
|
|
3271
|
+
const mimeType = this.getAssetMimeType({ NAME: assetName });
|
|
3272
|
+
if (mimeType.startsWith("image/")) return {
|
|
3273
|
+
assetName,
|
|
3274
|
+
kind: "image"
|
|
3275
|
+
};
|
|
3276
|
+
if (mimeType.startsWith("audio/")) return {
|
|
3277
|
+
assetName,
|
|
3278
|
+
kind: "audio"
|
|
3279
|
+
};
|
|
3280
|
+
throw this.assetTypeMismatch("animate", assetName, "image or audio", mimeType || "unknown MIME type");
|
|
3281
|
+
}
|
|
3282
|
+
async showCurrentStep(actor, state) {
|
|
3283
|
+
if (!this.isCurrent(actor, state)) return;
|
|
3284
|
+
const target = state.target;
|
|
3285
|
+
if (!this.runtime.targets.includes(target)) {
|
|
3286
|
+
this.stopActor(actor);
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
const batch = this.getCurrentBatch(state);
|
|
3290
|
+
if (!batch) {
|
|
3291
|
+
this.stopActor(actor);
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
try {
|
|
3295
|
+
let selectedImageIndex = -1;
|
|
3296
|
+
for (let index = 0; index < batch.actions.length; index += 1) if (batch.actions[index]?.kind === "image") selectedImageIndex = index;
|
|
3297
|
+
const selectedImage = batch.actions[selectedImageIndex];
|
|
3298
|
+
const selectedSkin = await (selectedImage ? this.resolveSkin(selectedImage.assetName) : Promise.resolve(null));
|
|
3299
|
+
if (!this.isCurrent(actor, state)) return;
|
|
3300
|
+
if (!this.runtime.targets.includes(target)) {
|
|
3301
|
+
this.stopActor(actor);
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
const soundStarts = [];
|
|
3305
|
+
for (let index = 0; index < batch.actions.length; index += 1) {
|
|
3306
|
+
const action = batch.actions[index];
|
|
3307
|
+
if (!action) continue;
|
|
3308
|
+
if (action.kind === "audio") soundStarts.push(this.playResolvedSound(action.assetName, false));
|
|
3309
|
+
else if (index === selectedImageIndex && selectedSkin) this.applyResolvedSkinToTarget(target, action.assetName, selectedSkin);
|
|
3310
|
+
}
|
|
3311
|
+
await Promise.all(soundStarts);
|
|
3312
|
+
} catch (error) {
|
|
3313
|
+
this.stopActor(actor);
|
|
3314
|
+
const assetNames = batch.actions.map((action) => action.assetName).join(", ");
|
|
3315
|
+
console.error(new AssetManagerError("ANIMATION_FAILED", `Failed to run actor "${target.sprite?.name ?? target.id}" actions "${assetNames}": ${errorMessage(error)}`, {
|
|
3316
|
+
operation: "animateActor",
|
|
3317
|
+
actorName: target.sprite?.name ?? target.id,
|
|
3318
|
+
hint: "Check that every animation asset is still registered and usable.",
|
|
3319
|
+
cause: error
|
|
3320
|
+
}));
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
if (!this.isCurrent(actor, state)) return;
|
|
3324
|
+
const intervalMs = batch.intervalMs;
|
|
3325
|
+
const nextActionIndex = batch.nextActionIndex;
|
|
3326
|
+
if (intervalMs === null || nextActionIndex === null) {
|
|
3327
|
+
this.actorAnimations.delete(actor);
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
state.deadline += intervalMs;
|
|
3331
|
+
const now = performance.now();
|
|
3332
|
+
if (state.deadline <= now) state.deadline = now + intervalMs;
|
|
3333
|
+
const delay = state.deadline - now;
|
|
3334
|
+
state.timer = setTimeout(() => this.advance(actor, state, nextActionIndex), delay);
|
|
3335
|
+
}
|
|
3336
|
+
getCurrentBatch(state) {
|
|
3337
|
+
if (!state.actions[state.actionIndex]) return null;
|
|
3338
|
+
const actions = [];
|
|
3339
|
+
let actionIndex = state.actionIndex;
|
|
3340
|
+
for (let count = 0; count < state.actions.length; count += 1) {
|
|
3341
|
+
const action = state.actions[actionIndex];
|
|
3342
|
+
if (!action) return null;
|
|
3343
|
+
actions.push(action);
|
|
3344
|
+
const intervalMs = state.intervalsMs[actionIndex];
|
|
3345
|
+
if (intervalMs === void 0) return {
|
|
3346
|
+
actions,
|
|
3347
|
+
intervalMs: null,
|
|
3348
|
+
nextActionIndex: null
|
|
3349
|
+
};
|
|
3350
|
+
const nextActionIndex = (actionIndex + 1) % state.actions.length;
|
|
3351
|
+
if (intervalMs > 0) return {
|
|
3352
|
+
actions,
|
|
3353
|
+
intervalMs,
|
|
3354
|
+
nextActionIndex
|
|
3355
|
+
};
|
|
3356
|
+
actionIndex = nextActionIndex;
|
|
3357
|
+
}
|
|
3358
|
+
return null;
|
|
3359
|
+
}
|
|
3360
|
+
advance(actor, state, nextActionIndex) {
|
|
3361
|
+
if (!this.isCurrent(actor, state)) return;
|
|
3362
|
+
state.timer = null;
|
|
3363
|
+
state.actionIndex = nextActionIndex;
|
|
3364
|
+
this.showCurrentStep(actor, state);
|
|
3365
|
+
}
|
|
3366
|
+
isCurrent(actor, state) {
|
|
3367
|
+
return this.actorAnimations.get(actor)?.generation === state.generation;
|
|
3368
|
+
}
|
|
3369
|
+
stopActor(actor) {
|
|
3370
|
+
const state = this.actorAnimations.get(actor);
|
|
3371
|
+
if (!state) return;
|
|
3372
|
+
this.actorAnimations.delete(actor);
|
|
3373
|
+
if (state.timer !== null) clearTimeout(state.timer);
|
|
3374
|
+
}
|
|
3375
|
+
stopTarget(target) {
|
|
3376
|
+
for (const [actor, state] of this.actorAnimations) if (state.target === target) this.stopActor(actor);
|
|
3377
|
+
}
|
|
3378
|
+
stopAllActorAnimations() {
|
|
3379
|
+
for (const actor of [...this.actorAnimations.keys()]) this.stopActor(actor);
|
|
3380
|
+
}
|
|
3381
|
+
};
|
|
3382
|
+
//#endregion
|
|
3383
|
+
//#region src/index.ts
|
|
3384
|
+
if (!Scratch.extensions.unsandboxed) throw new Error("Asset Cache must run unsandboxed.");
|
|
3385
|
+
Scratch.extensions.register(new AnimatedAssetManagerExtension());
|
|
3386
|
+
//#endregion
|
|
3387
|
+
|
|
3388
|
+
})(Scratch);
|