@ringozz/godot 4.7.2-614 → 4.7.2-616
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/README.md +148 -146
- package/package.json +7 -4
- package/src/assets.d.ts +130 -130
- package/src/debug.ts +357 -357
- package/src/editor.ts +107 -0
- package/src/index.ts +54 -50
- package/src/load.ts +231 -231
- package/src/preload.ts +253 -253
- package/src/runtime.ts +107 -107
- package/src/web-image.ts +153 -117
package/src/load.ts
CHANGED
|
@@ -1,231 +1,231 @@
|
|
|
1
|
-
/**********************************************************************
|
|
2
|
-
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
-
***********************************************************************/
|
|
4
|
-
|
|
5
|
-
import { DirAccess } from '../gen/classes/DirAccess.ts';
|
|
6
|
-
import { FileAccess } from '../gen/classes/FileAccess.ts';
|
|
7
|
-
import { ProjectSettings } from '../gen/classes/ProjectSettings.ts';
|
|
8
|
-
import { ResourceLoader, ThreadLoadStatus } from '../gen/classes/ResourceLoader.ts';
|
|
9
|
-
import { ResourceUID } from '../gen/classes/ResourceUID.ts';
|
|
10
|
-
import type { Resource } from '../gen/classes/Resource.ts';
|
|
11
|
-
import { cleanupHooks, stageFile } from './runtime.ts';
|
|
12
|
-
import { decodeCtex } from './web-image.ts';
|
|
13
|
-
|
|
14
|
-
// Each loaded wrapper is released at shutdown via a `cleanupHooks` entry
|
|
15
|
-
// holding a WeakRef: module-scope asset imports (the preload modules' memoized
|
|
16
|
-
// load promises) keep their wrappers reachable, so GC alone can't collect them
|
|
17
|
-
// before the engine stops. WeakRef keeps the entries from pinning resources
|
|
18
|
-
// during the app's lifetime.
|
|
19
|
-
function track<T extends Resource>(res: T): T {
|
|
20
|
-
const ref = new WeakRef<Resource>(res);
|
|
21
|
-
cleanupHooks.add(() => {
|
|
22
|
-
const r = ref.deref();
|
|
23
|
-
if (r) {
|
|
24
|
-
try { r.free(); } catch {}
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
return res;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* A Godot `Resource` subclass constructor: its `.name` is the registered class
|
|
32
|
-
* name (used as the `ResourceLoader` type hint), and `InstanceType<C>` is the
|
|
33
|
-
* loaded resource type.
|
|
34
|
-
*/
|
|
35
|
-
type ResourceConstructor = { new(...args: any[]): Resource } & Function;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* A file to stage before loading, keyed by `res://` path: sidecars (`.import`)
|
|
39
|
-
* arrive as `content` (text); imported products (`.scn`/`.ctex`) arrive as
|
|
40
|
-
* `path` (fetched).
|
|
41
|
-
*/
|
|
42
|
-
interface AssetFile {
|
|
43
|
-
content?: string;
|
|
44
|
-
path?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type AssetFiles = Record<string, AssetFile>;
|
|
48
|
-
|
|
49
|
-
const UID_RE = /uid="(uid:\/\/[\w.]+)"/;
|
|
50
|
-
|
|
51
|
-
// Godot encodes `uid://` numbers in base 34 over `a-y` then `0-8`
|
|
52
|
-
// (core/io/resource_uid.cpp). Decode in JS with BigInt: `ResourceUID.textToId`
|
|
53
|
-
// returns a JS number, and these ids exceed `Number.MAX_SAFE_INTEGER`, so the
|
|
54
|
-
// low digits would be lost. BigInt flows through `_C` losslessly (int64).
|
|
55
|
-
const UID_CHARS = 'abcdefghijklmnopqrstuvwxy012345678';
|
|
56
|
-
|
|
57
|
-
function uidToId(uid: string): bigint {
|
|
58
|
-
let id = 0n;
|
|
59
|
-
for (let i = 6; i < uid.length; i++) {
|
|
60
|
-
id = id * 34n + BigInt(UID_CHARS.indexOf(uid[i]));
|
|
61
|
-
}
|
|
62
|
-
return id;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Registers an asset's `uid://` → `res://` path so scene ext_resource references
|
|
67
|
-
* resolve it (Godot's `ResourceUID` map) instead of warning and falling back to
|
|
68
|
-
* the stored text path. Idempotent; skips unknown/malformed uids.
|
|
69
|
-
*/
|
|
70
|
-
function registerAssetUid(uid: string, resPath: string): void {
|
|
71
|
-
const id = uidToId(uid);
|
|
72
|
-
if (id !== 0n && !ResourceUID.hasId(id as unknown as number)) {
|
|
73
|
-
ResourceUID.addId(id as unknown as number, resPath);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function materialize([resPath, entry]: [string, AssetFile]): Promise<void> {
|
|
78
|
-
if (FileAccess.fileExists(resPath)) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
let bytes: Uint8Array;
|
|
82
|
-
if (entry.content !== undefined) {
|
|
83
|
-
// Sidecars (`.import`) and native text sources carry the asset's `uid=`;
|
|
84
|
-
// register it so scenes resolve by uid. `.import` maps to the source path
|
|
85
|
-
// (resPath minus the suffix); native files map to themselves.
|
|
86
|
-
const uid = entry.content.match(UID_RE)?.[1];
|
|
87
|
-
if (uid) {
|
|
88
|
-
registerAssetUid(uid, resPath.endsWith('.import') ? resPath.slice(0, -'.import'.length) : resPath);
|
|
89
|
-
}
|
|
90
|
-
bytes = new TextEncoder().encode(entry.content);
|
|
91
|
-
} else if (entry.path) {
|
|
92
|
-
const res = await fetch(entry.path);
|
|
93
|
-
if (!res.ok) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
bytes = new Uint8Array(await res.arrayBuffer());
|
|
97
|
-
if (resPath.endsWith('.ctex')) {
|
|
98
|
-
// On web, decode embedded PNG/WebP blobs with the browser before
|
|
99
|
-
// staging, so the engine never runs an image codec (see web-image.ts).
|
|
100
|
-
bytes = await decodeCtex(bytes);
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
stageFile?.(ProjectSettings.globalizePath(resPath), bytes);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const nextTick = () => new Promise(requestAnimationFrame);
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* An asset module as generated by `@ringozz/godot/preload`: `default` is the
|
|
112
|
-
* load promise, `materialize` resolves once its bundled files (plus its deps'
|
|
113
|
-
* files) are staged on the engine's filesystem.
|
|
114
|
-
*/
|
|
115
|
-
interface AssetModule {
|
|
116
|
-
default: Promise<unknown>;
|
|
117
|
-
materialize: Promise<unknown>;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Stages the asset's bundled files before loading: `.import` sidecars as text
|
|
122
|
-
* and imported products (`.scn`/`.ctex`) as fetched paths. On web the bytes are
|
|
123
|
-
* written straight into Emscripten's MEMFS via Godot's `copyToFS` (JS-heap only,
|
|
124
|
-
* no wasm copy); a no-op on desktop where the files already exist. `deps`
|
|
125
|
-
* supplies referenced asset modules whose file-staging is awaited (so Godot can
|
|
126
|
-
* resolve them) and whose load failures are logged.
|
|
127
|
-
*/
|
|
128
|
-
function materializeFiles(files: AssetFiles, deps: AssetModule[] = []): Promise<unknown> {
|
|
129
|
-
for (const dep of deps) {
|
|
130
|
-
dep.default.catch((err) => console.error('[godot] dependency load failed:', err));
|
|
131
|
-
}
|
|
132
|
-
if (stageFile === undefined) {
|
|
133
|
-
// Desktop: the files already exist on disk (res:// = cwd) and uids come
|
|
134
|
-
// from `.godot/uid_cache.bin`, so there is nothing to stage.
|
|
135
|
-
return Promise.resolve();
|
|
136
|
-
}
|
|
137
|
-
return Promise.all([...Object.entries(files).map(materialize), ...deps.map((dep) => dep.materialize)]);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Returns a promise resolving to the resource already cached in the engine at
|
|
142
|
-
* `path` (`ResourceCache`), or `null` when it isn't loaded yet. Used by
|
|
143
|
-
* {@link loadAsset} to skip file staging and the threaded load when a
|
|
144
|
-
* re-evaluated module (e.g. web HMR) references an asset that is still cached.
|
|
145
|
-
* `getCachedRef` returns the same JS wrapper as `loadThreadedGet` (instance
|
|
146
|
-
* binding), so identity is preserved.
|
|
147
|
-
*/
|
|
148
|
-
function cachedResource<C extends ResourceConstructor>(path: string): Promise<InstanceType<C>> | null {
|
|
149
|
-
const result = ResourceLoader.getCachedRef(path) as InstanceType<C>;
|
|
150
|
-
return result ? Promise.resolve(result) : null;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Entry point for the generated asset modules (`@ringozz/godot/preload`): checks
|
|
155
|
-
* the engine's `ResourceCache` once (`cachedResource`), stages the bundled files
|
|
156
|
-
* via `materializeFiles` when needed, then loads via {@link loadResourceAsync},
|
|
157
|
-
* memoizing the resulting load promise on `data` (the module's
|
|
158
|
-
* `import.meta.hot.data`, carried across HMR re-evaluations; `{}` on desktop
|
|
159
|
-
* where modules evaluate once). Memoization keeps `use()` seeing the **same**
|
|
160
|
-
* fulfilled promise object across re-evaluations — no Suspense fallback flash on
|
|
161
|
-
* hot reload; a rejected load is evicted so the next evaluation retries. Returns
|
|
162
|
-
* the `materialize` promise (own + deps' files staged) and the load promise.
|
|
163
|
-
*/
|
|
164
|
-
export function loadAsset<C extends ResourceConstructor>(
|
|
165
|
-
path: string,
|
|
166
|
-
cls: C,
|
|
167
|
-
files: AssetFiles,
|
|
168
|
-
deps: AssetModule[] = [],
|
|
169
|
-
data: Record<string, unknown> = {},
|
|
170
|
-
): { materialize: Promise<unknown>; load: Promise<InstanceType<C>> } {
|
|
171
|
-
const cached = cachedResource<C>(path);
|
|
172
|
-
if (cached) cached.then(track);
|
|
173
|
-
const materialize = cached ? Promise.resolve() : materializeFiles(files, deps);
|
|
174
|
-
const existing = data[path] as Promise<InstanceType<C>> | undefined;
|
|
175
|
-
const load = existing ?? cached ?? materialize.then(() => loadResourceAsync(path, cls, files));
|
|
176
|
-
data[path] = load;
|
|
177
|
-
load.catch(() => {
|
|
178
|
-
if (data[path] === load) delete data[path];
|
|
179
|
-
});
|
|
180
|
-
return { materialize, load };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Loads a resource in the background. `cls` supplies both the `ResourceLoader`
|
|
185
|
-
* type hint (its registered `.name`) and the return type. On web, call
|
|
186
|
-
* {@link materializeFiles} with the asset's bundled files first; on desktop the
|
|
187
|
-
* files already exist. Pass the same `files` map to have the staged files
|
|
188
|
-
* deleted from MEMFS once the resource is loaded (the resource stays cached in
|
|
189
|
-
* the engine, so the bytes are no longer needed).
|
|
190
|
-
*/
|
|
191
|
-
export async function loadResourceAsync<C extends ResourceConstructor>(
|
|
192
|
-
path: string,
|
|
193
|
-
cls: C,
|
|
194
|
-
files?: AssetFiles,
|
|
195
|
-
): Promise<InstanceType<C>> {
|
|
196
|
-
const err = ResourceLoader.loadThreadedRequest(path, cls.name);
|
|
197
|
-
if (err) {
|
|
198
|
-
throw new Error(`loadResourceAsync(${path}): loadThreadedRequest failed (${err})`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
let result: InstanceType<C>;
|
|
202
|
-
while (true) {
|
|
203
|
-
const status = ResourceLoader.loadThreadedGetStatus(path);
|
|
204
|
-
if (status === ThreadLoadStatus.THREAD_LOAD_LOADED) {
|
|
205
|
-
result = ResourceLoader.loadThreadedGet(path) as InstanceType<C>;
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
if (status === ThreadLoadStatus.THREAD_LOAD_FAILED || status === ThreadLoadStatus.THREAD_LOAD_INVALID_RESOURCE) {
|
|
209
|
-
// Collect the engine's LoadToken so a failed load doesn't leave it
|
|
210
|
-
// registered (a bare `RefCounted` leaked at exit). Safe no-op when
|
|
211
|
-
// no token exists.
|
|
212
|
-
ResourceLoader.loadThreadedGet(path);
|
|
213
|
-
throw new Error(`loadResourceAsync(${path}): load failed (status ${status})`);
|
|
214
|
-
}
|
|
215
|
-
await nextTick();
|
|
216
|
-
}
|
|
217
|
-
if (files && stageFile !== undefined) {
|
|
218
|
-
// Keep `.import` sidecars — they route imported source paths to their
|
|
219
|
-
// products (ResourceFormatImporter recognizes a path by its sidecar).
|
|
220
|
-
// Delete everything else: the products are read only on a cache miss, and
|
|
221
|
-
// the resource stays cached after loading, so the bytes are dead weight.
|
|
222
|
-
// Best-effort: a file may already be gone (another module's cleanup or a
|
|
223
|
-
// cache-miss re-stage).
|
|
224
|
-
for (const resPath of Object.keys(files)) {
|
|
225
|
-
if (!resPath.endsWith('.import')) {
|
|
226
|
-
DirAccess.removeAbsolute(resPath);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return track(result);
|
|
231
|
-
}
|
|
1
|
+
/**********************************************************************
|
|
2
|
+
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
+
***********************************************************************/
|
|
4
|
+
|
|
5
|
+
import { DirAccess } from '../gen/classes/DirAccess.ts';
|
|
6
|
+
import { FileAccess } from '../gen/classes/FileAccess.ts';
|
|
7
|
+
import { ProjectSettings } from '../gen/classes/ProjectSettings.ts';
|
|
8
|
+
import { ResourceLoader, ThreadLoadStatus } from '../gen/classes/ResourceLoader.ts';
|
|
9
|
+
import { ResourceUID } from '../gen/classes/ResourceUID.ts';
|
|
10
|
+
import type { Resource } from '../gen/classes/Resource.ts';
|
|
11
|
+
import { cleanupHooks, stageFile } from './runtime.ts';
|
|
12
|
+
import { decodeCtex } from './web-image.ts';
|
|
13
|
+
|
|
14
|
+
// Each loaded wrapper is released at shutdown via a `cleanupHooks` entry
|
|
15
|
+
// holding a WeakRef: module-scope asset imports (the preload modules' memoized
|
|
16
|
+
// load promises) keep their wrappers reachable, so GC alone can't collect them
|
|
17
|
+
// before the engine stops. WeakRef keeps the entries from pinning resources
|
|
18
|
+
// during the app's lifetime.
|
|
19
|
+
function track<T extends Resource>(res: T): T {
|
|
20
|
+
const ref = new WeakRef<Resource>(res);
|
|
21
|
+
cleanupHooks.add(() => {
|
|
22
|
+
const r = ref.deref();
|
|
23
|
+
if (r) {
|
|
24
|
+
try { r.free(); } catch {}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return res;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A Godot `Resource` subclass constructor: its `.name` is the registered class
|
|
32
|
+
* name (used as the `ResourceLoader` type hint), and `InstanceType<C>` is the
|
|
33
|
+
* loaded resource type.
|
|
34
|
+
*/
|
|
35
|
+
type ResourceConstructor = { new(...args: any[]): Resource } & Function;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A file to stage before loading, keyed by `res://` path: sidecars (`.import`)
|
|
39
|
+
* arrive as `content` (text); imported products (`.scn`/`.ctex`) arrive as
|
|
40
|
+
* `path` (fetched).
|
|
41
|
+
*/
|
|
42
|
+
interface AssetFile {
|
|
43
|
+
content?: string;
|
|
44
|
+
path?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type AssetFiles = Record<string, AssetFile>;
|
|
48
|
+
|
|
49
|
+
const UID_RE = /uid="(uid:\/\/[\w.]+)"/;
|
|
50
|
+
|
|
51
|
+
// Godot encodes `uid://` numbers in base 34 over `a-y` then `0-8`
|
|
52
|
+
// (core/io/resource_uid.cpp). Decode in JS with BigInt: `ResourceUID.textToId`
|
|
53
|
+
// returns a JS number, and these ids exceed `Number.MAX_SAFE_INTEGER`, so the
|
|
54
|
+
// low digits would be lost. BigInt flows through `_C` losslessly (int64).
|
|
55
|
+
const UID_CHARS = 'abcdefghijklmnopqrstuvwxy012345678';
|
|
56
|
+
|
|
57
|
+
function uidToId(uid: string): bigint {
|
|
58
|
+
let id = 0n;
|
|
59
|
+
for (let i = 6; i < uid.length; i++) {
|
|
60
|
+
id = id * 34n + BigInt(UID_CHARS.indexOf(uid[i]));
|
|
61
|
+
}
|
|
62
|
+
return id;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Registers an asset's `uid://` → `res://` path so scene ext_resource references
|
|
67
|
+
* resolve it (Godot's `ResourceUID` map) instead of warning and falling back to
|
|
68
|
+
* the stored text path. Idempotent; skips unknown/malformed uids.
|
|
69
|
+
*/
|
|
70
|
+
function registerAssetUid(uid: string, resPath: string): void {
|
|
71
|
+
const id = uidToId(uid);
|
|
72
|
+
if (id !== 0n && !ResourceUID.hasId(id as unknown as number)) {
|
|
73
|
+
ResourceUID.addId(id as unknown as number, resPath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function materialize([resPath, entry]: [string, AssetFile]): Promise<void> {
|
|
78
|
+
if (FileAccess.fileExists(resPath)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let bytes: Uint8Array;
|
|
82
|
+
if (entry.content !== undefined) {
|
|
83
|
+
// Sidecars (`.import`) and native text sources carry the asset's `uid=`;
|
|
84
|
+
// register it so scenes resolve by uid. `.import` maps to the source path
|
|
85
|
+
// (resPath minus the suffix); native files map to themselves.
|
|
86
|
+
const uid = entry.content.match(UID_RE)?.[1];
|
|
87
|
+
if (uid) {
|
|
88
|
+
registerAssetUid(uid, resPath.endsWith('.import') ? resPath.slice(0, -'.import'.length) : resPath);
|
|
89
|
+
}
|
|
90
|
+
bytes = new TextEncoder().encode(entry.content);
|
|
91
|
+
} else if (entry.path) {
|
|
92
|
+
const res = await fetch(entry.path);
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
bytes = new Uint8Array(await res.arrayBuffer());
|
|
97
|
+
if (resPath.endsWith('.ctex')) {
|
|
98
|
+
// On web, decode embedded PNG/WebP blobs with the browser before
|
|
99
|
+
// staging, so the engine never runs an image codec (see web-image.ts).
|
|
100
|
+
bytes = await decodeCtex(bytes);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
stageFile?.(ProjectSettings.globalizePath(resPath), bytes);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const nextTick = () => new Promise(requestAnimationFrame);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* An asset module as generated by `@ringozz/godot/preload`: `default` is the
|
|
112
|
+
* load promise, `materialize` resolves once its bundled files (plus its deps'
|
|
113
|
+
* files) are staged on the engine's filesystem.
|
|
114
|
+
*/
|
|
115
|
+
interface AssetModule {
|
|
116
|
+
default: Promise<unknown>;
|
|
117
|
+
materialize: Promise<unknown>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Stages the asset's bundled files before loading: `.import` sidecars as text
|
|
122
|
+
* and imported products (`.scn`/`.ctex`) as fetched paths. On web the bytes are
|
|
123
|
+
* written straight into Emscripten's MEMFS via Godot's `copyToFS` (JS-heap only,
|
|
124
|
+
* no wasm copy); a no-op on desktop where the files already exist. `deps`
|
|
125
|
+
* supplies referenced asset modules whose file-staging is awaited (so Godot can
|
|
126
|
+
* resolve them) and whose load failures are logged.
|
|
127
|
+
*/
|
|
128
|
+
function materializeFiles(files: AssetFiles, deps: AssetModule[] = []): Promise<unknown> {
|
|
129
|
+
for (const dep of deps) {
|
|
130
|
+
dep.default.catch((err) => console.error('[godot] dependency load failed:', err));
|
|
131
|
+
}
|
|
132
|
+
if (stageFile === undefined) {
|
|
133
|
+
// Desktop: the files already exist on disk (res:// = cwd) and uids come
|
|
134
|
+
// from `.godot/uid_cache.bin`, so there is nothing to stage.
|
|
135
|
+
return Promise.resolve();
|
|
136
|
+
}
|
|
137
|
+
return Promise.all([...Object.entries(files).map(materialize), ...deps.map((dep) => dep.materialize)]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns a promise resolving to the resource already cached in the engine at
|
|
142
|
+
* `path` (`ResourceCache`), or `null` when it isn't loaded yet. Used by
|
|
143
|
+
* {@link loadAsset} to skip file staging and the threaded load when a
|
|
144
|
+
* re-evaluated module (e.g. web HMR) references an asset that is still cached.
|
|
145
|
+
* `getCachedRef` returns the same JS wrapper as `loadThreadedGet` (instance
|
|
146
|
+
* binding), so identity is preserved.
|
|
147
|
+
*/
|
|
148
|
+
function cachedResource<C extends ResourceConstructor>(path: string): Promise<InstanceType<C>> | null {
|
|
149
|
+
const result = ResourceLoader.getCachedRef(path) as InstanceType<C>;
|
|
150
|
+
return result ? Promise.resolve(result) : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Entry point for the generated asset modules (`@ringozz/godot/preload`): checks
|
|
155
|
+
* the engine's `ResourceCache` once (`cachedResource`), stages the bundled files
|
|
156
|
+
* via `materializeFiles` when needed, then loads via {@link loadResourceAsync},
|
|
157
|
+
* memoizing the resulting load promise on `data` (the module's
|
|
158
|
+
* `import.meta.hot.data`, carried across HMR re-evaluations; `{}` on desktop
|
|
159
|
+
* where modules evaluate once). Memoization keeps `use()` seeing the **same**
|
|
160
|
+
* fulfilled promise object across re-evaluations — no Suspense fallback flash on
|
|
161
|
+
* hot reload; a rejected load is evicted so the next evaluation retries. Returns
|
|
162
|
+
* the `materialize` promise (own + deps' files staged) and the load promise.
|
|
163
|
+
*/
|
|
164
|
+
export function loadAsset<C extends ResourceConstructor>(
|
|
165
|
+
path: string,
|
|
166
|
+
cls: C,
|
|
167
|
+
files: AssetFiles,
|
|
168
|
+
deps: AssetModule[] = [],
|
|
169
|
+
data: Record<string, unknown> = {},
|
|
170
|
+
): { materialize: Promise<unknown>; load: Promise<InstanceType<C>> } {
|
|
171
|
+
const cached = cachedResource<C>(path);
|
|
172
|
+
if (cached) cached.then(track);
|
|
173
|
+
const materialize = cached ? Promise.resolve() : materializeFiles(files, deps);
|
|
174
|
+
const existing = data[path] as Promise<InstanceType<C>> | undefined;
|
|
175
|
+
const load = existing ?? cached ?? materialize.then(() => loadResourceAsync(path, cls, files));
|
|
176
|
+
data[path] = load;
|
|
177
|
+
load.catch(() => {
|
|
178
|
+
if (data[path] === load) delete data[path];
|
|
179
|
+
});
|
|
180
|
+
return { materialize, load };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Loads a resource in the background. `cls` supplies both the `ResourceLoader`
|
|
185
|
+
* type hint (its registered `.name`) and the return type. On web, call
|
|
186
|
+
* {@link materializeFiles} with the asset's bundled files first; on desktop the
|
|
187
|
+
* files already exist. Pass the same `files` map to have the staged files
|
|
188
|
+
* deleted from MEMFS once the resource is loaded (the resource stays cached in
|
|
189
|
+
* the engine, so the bytes are no longer needed).
|
|
190
|
+
*/
|
|
191
|
+
export async function loadResourceAsync<C extends ResourceConstructor>(
|
|
192
|
+
path: string,
|
|
193
|
+
cls: C,
|
|
194
|
+
files?: AssetFiles,
|
|
195
|
+
): Promise<InstanceType<C>> {
|
|
196
|
+
const err = ResourceLoader.loadThreadedRequest(path, cls.name);
|
|
197
|
+
if (err) {
|
|
198
|
+
throw new Error(`loadResourceAsync(${path}): loadThreadedRequest failed (${err})`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let result: InstanceType<C>;
|
|
202
|
+
while (true) {
|
|
203
|
+
const status = ResourceLoader.loadThreadedGetStatus(path);
|
|
204
|
+
if (status === ThreadLoadStatus.THREAD_LOAD_LOADED) {
|
|
205
|
+
result = ResourceLoader.loadThreadedGet(path) as InstanceType<C>;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
if (status === ThreadLoadStatus.THREAD_LOAD_FAILED || status === ThreadLoadStatus.THREAD_LOAD_INVALID_RESOURCE) {
|
|
209
|
+
// Collect the engine's LoadToken so a failed load doesn't leave it
|
|
210
|
+
// registered (a bare `RefCounted` leaked at exit). Safe no-op when
|
|
211
|
+
// no token exists.
|
|
212
|
+
ResourceLoader.loadThreadedGet(path);
|
|
213
|
+
throw new Error(`loadResourceAsync(${path}): load failed (status ${status})`);
|
|
214
|
+
}
|
|
215
|
+
await nextTick();
|
|
216
|
+
}
|
|
217
|
+
if (files && stageFile !== undefined) {
|
|
218
|
+
// Keep `.import` sidecars — they route imported source paths to their
|
|
219
|
+
// products (ResourceFormatImporter recognizes a path by its sidecar).
|
|
220
|
+
// Delete everything else: the products are read only on a cache miss, and
|
|
221
|
+
// the resource stays cached after loading, so the bytes are dead weight.
|
|
222
|
+
// Best-effort: a file may already be gone (another module's cleanup or a
|
|
223
|
+
// cache-miss re-stage).
|
|
224
|
+
for (const resPath of Object.keys(files)) {
|
|
225
|
+
if (!resPath.endsWith('.import')) {
|
|
226
|
+
DirAccess.removeAbsolute(resPath);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return track(result);
|
|
231
|
+
}
|