@ringozz/godot 4.7.1-1 → 4.7.1-10
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 +65 -4
- package/package.json +16 -4
- package/src/assets.d.ts +116 -0
- package/src/boot.browser.ts +41 -0
- package/src/boot.ts +27 -0
- package/src/debug.ts +182 -165
- package/src/index.ts +5 -1
- package/src/load.ts +210 -0
- package/src/preload.ts +198 -0
- package/src/runtime.ts +20 -7
- package/src/web-image.ts +118 -0
package/src/debug.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
3
|
***********************************************************************/
|
|
4
4
|
|
|
5
|
-
import { Rect2, Vector3 } from './index.ts';
|
|
5
|
+
import { Rect2, str, varToStr, Vector3 } from './index.ts';
|
|
6
6
|
import { CanvasItem } from '../gen/classes/CanvasItem.ts';
|
|
7
7
|
import { ClassDB } from '../gen/classes/ClassDB.ts';
|
|
8
8
|
import { Control } from '../gen/classes/Control.ts';
|
|
@@ -14,40 +14,25 @@ import { OS } from '../gen/classes/OS.ts';
|
|
|
14
14
|
import { Time } from '../gen/classes/Time.ts';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Dump a Godot object
|
|
18
|
-
*
|
|
17
|
+
* Dump a Godot value or object as a string.
|
|
18
|
+
* Value types (vectors, arrays, dictionaries, …) render via Godot's `str()`.
|
|
19
|
+
* Objects dump via Godot's `var_to_str` — its own property walk + recursion —
|
|
20
|
+
* with the node's `name` prepended when present.
|
|
19
21
|
*
|
|
20
|
-
* @param obj - Any Godot wrapper (Node, Resource,
|
|
21
|
-
* @param depth - Max recursion depth into sub-objects (default 2)
|
|
22
|
-
* @param maxProps - Max properties to show per object (default 30)
|
|
22
|
+
* @param obj - Any Godot wrapper (Node, Resource, …), value type, or plain value
|
|
23
23
|
*/
|
|
24
|
-
export function dumpStr(obj: unknown
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
let count = 0;
|
|
31
|
-
try {
|
|
32
|
-
const plist = (obj as any).getPropertyList?.();
|
|
33
|
-
if (plist?.size) {
|
|
34
|
-
for (let i = 0; i < plist.size() && count < maxProps; i++) {
|
|
35
|
-
const name = plist.get(i).get('name');
|
|
36
|
-
if (!name || (name + '').startsWith('_')) continue;
|
|
37
|
-
let val: unknown;
|
|
38
|
-
try { val = (obj as any).get(name); } catch { val = '<err>'; }
|
|
39
|
-
if (typeof val === 'object' && val !== null && depth > 1)
|
|
40
|
-
lines.push(` ${name}:\n${indent(dumpStr(val, depth - 1, maxProps), ' ')}`);
|
|
41
|
-
else
|
|
42
|
-
lines.push(` ${name}: ${String(val)}`);
|
|
43
|
-
count++;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
} catch { /* not a GodotVar */ }
|
|
47
|
-
return lines.join('\n');
|
|
48
|
-
}
|
|
24
|
+
export function dumpStr(obj: unknown): string {
|
|
25
|
+
if (obj == null || typeof obj !== 'object') return String(obj);
|
|
26
|
+
|
|
27
|
+
// Value types & heap containers (Vector3, Array, Dictionary, …) have no getClass() —
|
|
28
|
+
// let Godot's str() render them.
|
|
29
|
+
if (!(obj as any).getClass) return str(obj as any);
|
|
49
30
|
|
|
50
|
-
|
|
31
|
+
const dump = varToStr(obj as any);
|
|
32
|
+
let name: unknown;
|
|
33
|
+
try { name = (obj as any).name; } catch { name = undefined; }
|
|
34
|
+
return name === undefined || name === null || name === '' ? dump : `${name}: ${dump}`;
|
|
35
|
+
}
|
|
51
36
|
|
|
52
37
|
/**
|
|
53
38
|
* Serialize a scene subtree as a tree-formatted string.
|
|
@@ -56,34 +41,36 @@ function indent(s: string, prefix: string): string { return s.split('\n').join('
|
|
|
56
41
|
* @param depth - Max depth (default 99)
|
|
57
42
|
*/
|
|
58
43
|
export function dumpTreeStr(node: Node, depth = 99): string {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
44
|
+
const lines: string[] = [];
|
|
45
|
+
(function walk(n: Node, d: number, ind: string) {
|
|
46
|
+
if (d <= 0) return;
|
|
47
|
+
lines.push(`${ind}${n.getClass()} "${n.name}" (${n.getChildCount()} children)`);
|
|
48
|
+
for (let i = 0; i < n.getChildCount(); i++) {
|
|
49
|
+
const c = n.getChild(i);
|
|
50
|
+
if (c) walk(c as Node, d - 1, ind + ' ');
|
|
51
|
+
}
|
|
52
|
+
})(node, depth, '');
|
|
53
|
+
return lines.join('\n');
|
|
69
54
|
}
|
|
70
55
|
|
|
71
56
|
/**
|
|
72
57
|
* Return a snapshot of engine runtime stats as a formatted string.
|
|
73
58
|
*
|
|
74
59
|
* Includes process frames, FPS, physics frames, node count, time scale,
|
|
75
|
-
* and main-loop class.
|
|
60
|
+
* and main-loop class. Never throws: `Engine.getMainLoop()` can be null or
|
|
61
|
+
* throw while the engine is starting/stopping, in which case those rows show `N/A`.
|
|
76
62
|
*/
|
|
77
63
|
export function statsStr(): string {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
64
|
+
let tree: any = null;
|
|
65
|
+
try { tree = Engine.getMainLoop(); } catch { tree = null; }
|
|
66
|
+
return [
|
|
67
|
+
`Frames: ${Engine.getProcessFrames()}`,
|
|
68
|
+
`FPS: ${Engine.getFramesPerSecond()}`,
|
|
69
|
+
`Physics: ${Engine.getPhysicsFrames()}`,
|
|
70
|
+
`Nodes: ${tree?.getNodeCount?.() ?? 'N/A'}`,
|
|
71
|
+
`Time scale: ${Engine.timeScale}`,
|
|
72
|
+
`Main loop: ${tree ? tree.getClass() : 'N/A'}`,
|
|
73
|
+
].join('\n');
|
|
87
74
|
}
|
|
88
75
|
|
|
89
76
|
/**
|
|
@@ -91,16 +78,43 @@ export function statsStr(): string {
|
|
|
91
78
|
* Requires the `--expose-gc` Node.js flag.
|
|
92
79
|
*/
|
|
93
80
|
export function gc(): string {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
81
|
+
const before = globalThis.process?.memoryUsage?.().heapUsed;
|
|
82
|
+
if (globalThis.Bun)
|
|
83
|
+
globalThis.Bun.gc(true);
|
|
84
|
+
else if (globalThis.gc)
|
|
85
|
+
globalThis.gc({ type: 'major', execution: 'sync' });
|
|
86
|
+
else
|
|
87
|
+
return 'gc() unavailable — need --expose-gc';
|
|
88
|
+
|
|
89
|
+
const after = globalThis.process?.memoryUsage?.().heapUsed;
|
|
90
|
+
const freed = typeof before === 'number' && typeof after === 'number' ? (before - after) / 1024 : 0;
|
|
91
|
+
return `GC: freed ${freed.toFixed(0)} KB`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* `DOMRect` is not a global in Bun (desktop), but `getBoundingClientRect()`
|
|
96
|
+
* constructs one. Install a minimal spec-compatible polyfill on desktop so the
|
|
97
|
+
* helper works identically on desktop and web (browsers already define it).
|
|
98
|
+
*/
|
|
99
|
+
function ensureDOMRect(): void {
|
|
100
|
+
if (typeof (globalThis as any).DOMRect !== 'undefined') return;
|
|
101
|
+
(globalThis as any).DOMRect = class DOMRect {
|
|
102
|
+
x: number;
|
|
103
|
+
y: number;
|
|
104
|
+
width: number;
|
|
105
|
+
height: number;
|
|
106
|
+
constructor(x = 0, y = 0, width = 0, height = 0) {
|
|
107
|
+
this.x = x;
|
|
108
|
+
this.y = y;
|
|
109
|
+
this.width = width;
|
|
110
|
+
this.height = height;
|
|
111
|
+
}
|
|
112
|
+
get left() { return this.x; }
|
|
113
|
+
get top() { return this.y; }
|
|
114
|
+
get right() { return this.x + this.width; }
|
|
115
|
+
get bottom() { return this.y + this.height; }
|
|
116
|
+
toJSON() { return { x: this.x, y: this.y, width: this.width, height: this.height }; }
|
|
117
|
+
};
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
/**
|
|
@@ -111,109 +125,112 @@ export function gc(): string {
|
|
|
111
125
|
* breakpoints in any module.
|
|
112
126
|
* - Registers an `uncaughtException` handler that logs but keeps the process
|
|
113
127
|
* alive for interactive debugging.
|
|
128
|
+
* - Polyfills `DOMRect` (needed by `getBoundingClientRect()` on desktop).
|
|
114
129
|
*/
|
|
115
130
|
export function initDebug(): void {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
131
|
+
globalThis.process?.on?.('uncaughtException', (err, origin) => {
|
|
132
|
+
console.error(`\n✗ ${origin}:`, err.stack);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
ensureDOMRect();
|
|
136
|
+
|
|
137
|
+
Object.defineProperty(CanvasItem.prototype, 'getBoundingClientRect', {
|
|
138
|
+
configurable: true,
|
|
139
|
+
value(this: CanvasItem): DOMRect | null {
|
|
140
|
+
const scale = DisplayServer.screenGetScale();
|
|
141
|
+
if (this instanceof Control) {
|
|
142
|
+
const rc = (this as Control).getGlobalRect();
|
|
143
|
+
return new DOMRect(
|
|
144
|
+
rc.position.x / scale, rc.position.y / scale,
|
|
145
|
+
rc.size.x / scale, rc.size.y / scale,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof (this as any).getRect !== 'function') return null;
|
|
150
|
+
|
|
151
|
+
const rect = (this as any).getRect() as Rect2;
|
|
152
|
+
const t = this.getScreenTransform();
|
|
153
|
+
|
|
154
|
+
const pos = rect.position;
|
|
155
|
+
const sz = rect.size;
|
|
156
|
+
const tx = t.x;
|
|
157
|
+
const ty = t.y;
|
|
158
|
+
const to = t.origin;
|
|
159
|
+
|
|
160
|
+
const x0 = pos.x, y0 = pos.y;
|
|
161
|
+
const x1 = x0 + sz.x, y1 = y0 + sz.y;
|
|
162
|
+
const txx = tx.x, txy = tx.y;
|
|
163
|
+
const tyx = ty.x, tyy = ty.y;
|
|
164
|
+
const tox = to.x, toy = to.y;
|
|
165
|
+
|
|
166
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
167
|
+
const accum = (px: number, py: number) => {
|
|
168
|
+
if (px < minX) minX = px; if (py < minY) minY = py;
|
|
169
|
+
if (px > maxX) maxX = px; if (py > maxY) maxY = py;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
accum(txx * x0 + tyx * y0 + tox, txy * x0 + tyy * y0 + toy);
|
|
173
|
+
accum(txx * x1 + tyx * y0 + tox, txy * x1 + tyy * y0 + toy);
|
|
174
|
+
accum(txx * x0 + tyx * y1 + tox, txy * x0 + tyy * y1 + toy);
|
|
175
|
+
accum(txx * x1 + tyx * y1 + tox, txy * x1 + tyy * y1 + toy);
|
|
176
|
+
|
|
177
|
+
return new DOMRect(
|
|
178
|
+
minX / scale, minY / scale,
|
|
179
|
+
(maxX - minX) / scale, (maxY - minY) / scale,
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
Object.defineProperty(Node3D.prototype, 'getBoundingClientRect', {
|
|
185
|
+
configurable: true,
|
|
186
|
+
value(this: Node3D): DOMRect | null {
|
|
187
|
+
if (typeof (this as any).getAabb !== 'function') return null;
|
|
188
|
+
|
|
189
|
+
const aabb = (this as any).getAabb();
|
|
190
|
+
const gt = this.globalTransform;
|
|
191
|
+
const viewport = this.getViewport();
|
|
192
|
+
if (!viewport) return null;
|
|
193
|
+
const camera = viewport.getCamera3d();
|
|
194
|
+
if (!camera) return null;
|
|
195
|
+
const scale = DisplayServer.screenGetScale();
|
|
196
|
+
|
|
197
|
+
const b = gt.basis;
|
|
198
|
+
const bx = b.x, by = b.y, bz = b.z;
|
|
199
|
+
const o = gt.origin;
|
|
200
|
+
const p = aabb.position, e = aabb.end;
|
|
201
|
+
|
|
202
|
+
const [px, py, pz] = [p.x, p.y, p.z];
|
|
203
|
+
const [ex, ey, ez] = [e.x, e.y, e.z];
|
|
204
|
+
const [bxx, bxy, bxz] = [bx.x, bx.y, bx.z];
|
|
205
|
+
const [byx, byy, byz] = [by.x, by.y, by.z];
|
|
206
|
+
const [bzx, bzy, bzz] = [bz.x, bz.y, bz.z];
|
|
207
|
+
const [ox, oy, oz] = [o.x, o.y, o.z];
|
|
208
|
+
|
|
209
|
+
const corners = [
|
|
210
|
+
[px, py, pz], [ex, py, pz], [px, ey, pz], [ex, ey, pz],
|
|
211
|
+
[px, py, ez], [ex, py, ez], [px, ey, ez], [ex, ey, ez],
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
215
|
+
|
|
216
|
+
for (const [cx, cy, cz] of corners) {
|
|
217
|
+
const wx = bxx * cx + byx * cy + bzx * cz + ox;
|
|
218
|
+
const wy = bxy * cx + byy * cy + bzy * cz + oy;
|
|
219
|
+
const wz = bxz * cx + byz * cy + bzz * cz + oz;
|
|
220
|
+
const s = camera.unprojectPosition(new Vector3(wx, wy, wz));
|
|
221
|
+
if (s.x < minX) minX = s.x; if (s.y < minY) minY = s.y;
|
|
222
|
+
if (s.x > maxX) maxX = s.x; if (s.y > maxY) maxY = s.y;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return new DOMRect(
|
|
226
|
+
minX / scale, minY / scale,
|
|
227
|
+
(maxX - minX) / scale, (maxY - minY) / scale,
|
|
228
|
+
);
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
(globalThis as any).$ = {
|
|
233
|
+
Engine, OS, ClassDB, Time,
|
|
234
|
+
dumpStr, dumpTreeStr, statsStr, gc,
|
|
235
|
+
};
|
|
219
236
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
***********************************************************************/
|
|
4
4
|
|
|
5
5
|
export * from '../gen/index.ts';
|
|
6
|
+
import * as ValueTypes from '../gen/value-types/index.ts';
|
|
7
|
+
import * as HeapTypes from '../gen/heap-types/index.ts';
|
|
6
8
|
import { Engine } from '../gen/classes/Engine.ts';
|
|
7
9
|
import { GodotInstance } from '../gen/classes/GodotInstance.ts';
|
|
8
10
|
import { SceneTree } from '../gen/classes/SceneTree.ts';
|
|
@@ -11,6 +13,8 @@ import { gc } from './debug.ts';
|
|
|
11
13
|
import { cancelAnimationFrame, getGodot, requestAnimationFrame } from './runtime.ts';
|
|
12
14
|
|
|
13
15
|
// ---- make sure these classes are not tree-shaked ----
|
|
16
|
+
void ValueTypes;
|
|
17
|
+
void HeapTypes;
|
|
14
18
|
void GodotInstance;
|
|
15
19
|
void SceneTree;
|
|
16
20
|
void Window;
|
|
@@ -22,7 +26,7 @@ Object.assign(globalThis as any, { requestAnimationFrame, cancelAnimationFrame }
|
|
|
22
26
|
export async function runGodot(signal?: AbortSignal, unmount?: () => PromiseLike<void>) {
|
|
23
27
|
signal?.throwIfAborted();
|
|
24
28
|
signal?.addEventListener('abort', () => {
|
|
25
|
-
(Engine.getMainLoop() as SceneTree)
|
|
29
|
+
(Engine.getMainLoop() as SceneTree)?.quit();
|
|
26
30
|
}, { once: true });
|
|
27
31
|
|
|
28
32
|
const godot = getGodot();
|
package/src/load.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
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 { stageFile } from './runtime.ts';
|
|
12
|
+
import { decodeCtex } from './web-image.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A Godot `Resource` subclass constructor: its `.name` is the registered class
|
|
16
|
+
* name (used as the `ResourceLoader` type hint), and `InstanceType<C>` is the
|
|
17
|
+
* loaded resource type.
|
|
18
|
+
*/
|
|
19
|
+
type ResourceConstructor = { new(...args: any[]): Resource } & Function;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A file to stage before loading, keyed by `res://` path: sidecars (`.import`)
|
|
23
|
+
* arrive as `content` (text); imported products (`.scn`/`.ctex`) arrive as
|
|
24
|
+
* `path` (fetched).
|
|
25
|
+
*/
|
|
26
|
+
interface AssetFile {
|
|
27
|
+
content?: string;
|
|
28
|
+
path?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type AssetFiles = Record<string, AssetFile>;
|
|
32
|
+
|
|
33
|
+
const UID_RE = /uid="(uid:\/\/[\w.]+)"/;
|
|
34
|
+
|
|
35
|
+
// Godot encodes `uid://` numbers in base 34 over `a-y` then `0-8`
|
|
36
|
+
// (core/io/resource_uid.cpp). Decode in JS with BigInt: `ResourceUID.textToId`
|
|
37
|
+
// returns a JS number, and these ids exceed `Number.MAX_SAFE_INTEGER`, so the
|
|
38
|
+
// low digits would be lost. BigInt flows through `_C` losslessly (int64).
|
|
39
|
+
const UID_CHARS = 'abcdefghijklmnopqrstuvwxy012345678';
|
|
40
|
+
|
|
41
|
+
function uidToId(uid: string): bigint {
|
|
42
|
+
let id = 0n;
|
|
43
|
+
for (let i = 6; i < uid.length; i++) {
|
|
44
|
+
id = id * 34n + BigInt(UID_CHARS.indexOf(uid[i]));
|
|
45
|
+
}
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Registers an asset's `uid://` → `res://` path so scene ext_resource references
|
|
51
|
+
* resolve it (Godot's `ResourceUID` map) instead of warning and falling back to
|
|
52
|
+
* the stored text path. Idempotent; skips unknown/malformed uids.
|
|
53
|
+
*/
|
|
54
|
+
function registerAssetUid(uid: string, resPath: string): void {
|
|
55
|
+
const id = uidToId(uid);
|
|
56
|
+
if (id !== 0n && !ResourceUID.hasId(id as unknown as number)) {
|
|
57
|
+
ResourceUID.addId(id as unknown as number, resPath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function materialize([resPath, entry]: [string, AssetFile]): Promise<void> {
|
|
62
|
+
if (FileAccess.fileExists(resPath)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let bytes: Uint8Array;
|
|
66
|
+
if (entry.content !== undefined) {
|
|
67
|
+
// Sidecars (`.import`) and native text sources carry the asset's `uid=`;
|
|
68
|
+
// register it so scenes resolve by uid. `.import` maps to the source path
|
|
69
|
+
// (resPath minus the suffix); native files map to themselves.
|
|
70
|
+
const uid = entry.content.match(UID_RE)?.[1];
|
|
71
|
+
if (uid) {
|
|
72
|
+
registerAssetUid(uid, resPath.endsWith('.import') ? resPath.slice(0, -'.import'.length) : resPath);
|
|
73
|
+
}
|
|
74
|
+
bytes = new TextEncoder().encode(entry.content);
|
|
75
|
+
} else if (entry.path) {
|
|
76
|
+
const res = await fetch(entry.path);
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
bytes = new Uint8Array(await res.arrayBuffer());
|
|
81
|
+
if (resPath.endsWith('.ctex')) {
|
|
82
|
+
// On web, decode embedded PNG/WebP blobs with the browser before
|
|
83
|
+
// staging, so the engine never runs an image codec (see web-image.ts).
|
|
84
|
+
bytes = await decodeCtex(bytes);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
stageFile?.(ProjectSettings.globalizePath(resPath), bytes);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const nextTick = () => new Promise(requestAnimationFrame);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* An asset module as generated by `@ringozz/godot/preload`: `default` is the
|
|
96
|
+
* load promise, `materialize` resolves once its bundled files (plus its deps'
|
|
97
|
+
* files) are staged on the engine's filesystem.
|
|
98
|
+
*/
|
|
99
|
+
interface AssetModule {
|
|
100
|
+
default: Promise<unknown>;
|
|
101
|
+
materialize: Promise<unknown>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Stages the asset's bundled files before loading: `.import` sidecars as text
|
|
106
|
+
* and imported products (`.scn`/`.ctex`) as fetched paths. On web the bytes are
|
|
107
|
+
* written straight into Emscripten's MEMFS via Godot's `copyToFS` (JS-heap only,
|
|
108
|
+
* no wasm copy); a no-op on desktop where the files already exist. `deps`
|
|
109
|
+
* supplies referenced asset modules whose file-staging is awaited (so Godot can
|
|
110
|
+
* resolve them) and whose load failures are logged.
|
|
111
|
+
*/
|
|
112
|
+
function materializeFiles(files: AssetFiles, deps: AssetModule[] = []): Promise<unknown> {
|
|
113
|
+
for (const dep of deps) {
|
|
114
|
+
dep.default.catch((err) => console.error('[godot] dependency load failed:', err));
|
|
115
|
+
}
|
|
116
|
+
if (stageFile === undefined) {
|
|
117
|
+
// Desktop: the files already exist on disk (res:// = cwd) and uids come
|
|
118
|
+
// from `.godot/uid_cache.bin`, so there is nothing to stage.
|
|
119
|
+
return Promise.resolve();
|
|
120
|
+
}
|
|
121
|
+
return Promise.all([...Object.entries(files).map(materialize), ...deps.map((dep) => dep.materialize)]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Returns a promise resolving to the resource already cached in the engine at
|
|
126
|
+
* `path` (`ResourceCache`), or `null` when it isn't loaded yet. Used by
|
|
127
|
+
* {@link loadAsset} to skip file staging and the threaded load when a
|
|
128
|
+
* re-evaluated module (e.g. web HMR) references an asset that is still cached.
|
|
129
|
+
* `getCachedRef` returns the same JS wrapper as `loadThreadedGet` (instance
|
|
130
|
+
* binding), so identity is preserved.
|
|
131
|
+
*/
|
|
132
|
+
function cachedResource<C extends ResourceConstructor>(path: string): Promise<InstanceType<C>> | null {
|
|
133
|
+
const result = ResourceLoader.getCachedRef(path) as InstanceType<C>;
|
|
134
|
+
return result ? Promise.resolve(result) : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Entry point for the generated asset modules (`@ringozz/godot/preload`): checks
|
|
139
|
+
* the engine's `ResourceCache` once (`cachedResource`), stages the bundled files
|
|
140
|
+
* via `materializeFiles` when needed, then loads via {@link loadResourceAsync},
|
|
141
|
+
* memoizing the resulting load promise on `data` (the module's
|
|
142
|
+
* `import.meta.hot.data`, carried across HMR re-evaluations; `{}` on desktop
|
|
143
|
+
* where modules evaluate once). Memoization keeps `use()` seeing the **same**
|
|
144
|
+
* fulfilled promise object across re-evaluations — no Suspense fallback flash on
|
|
145
|
+
* hot reload; a rejected load is evicted so the next evaluation retries. Returns
|
|
146
|
+
* the `materialize` promise (own + deps' files staged) and the load promise.
|
|
147
|
+
*/
|
|
148
|
+
export function loadAsset<C extends ResourceConstructor>(
|
|
149
|
+
path: string,
|
|
150
|
+
cls: C,
|
|
151
|
+
files: AssetFiles,
|
|
152
|
+
deps: AssetModule[] = [],
|
|
153
|
+
data: Record<string, unknown> = {},
|
|
154
|
+
): { materialize: Promise<unknown>; load: Promise<InstanceType<C>> } {
|
|
155
|
+
const cached = cachedResource<C>(path);
|
|
156
|
+
const materialize = cached ? Promise.resolve() : materializeFiles(files, deps);
|
|
157
|
+
const existing = data[path] as Promise<InstanceType<C>> | undefined;
|
|
158
|
+
const load = existing ?? cached ?? materialize.then(() => loadResourceAsync(path, cls, files));
|
|
159
|
+
data[path] = load;
|
|
160
|
+
load.catch(() => {
|
|
161
|
+
if (data[path] === load) delete data[path];
|
|
162
|
+
});
|
|
163
|
+
return { materialize, load };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Loads a resource in the background. `cls` supplies both the `ResourceLoader`
|
|
168
|
+
* type hint (its registered `.name`) and the return type. On web, call
|
|
169
|
+
* {@link materializeFiles} with the asset's bundled files first; on desktop the
|
|
170
|
+
* files already exist. Pass the same `files` map to have the staged files
|
|
171
|
+
* deleted from MEMFS once the resource is loaded (the resource stays cached in
|
|
172
|
+
* the engine, so the bytes are no longer needed).
|
|
173
|
+
*/
|
|
174
|
+
export async function loadResourceAsync<C extends ResourceConstructor>(
|
|
175
|
+
path: string,
|
|
176
|
+
cls: C,
|
|
177
|
+
files?: AssetFiles,
|
|
178
|
+
): Promise<InstanceType<C>> {
|
|
179
|
+
const err = ResourceLoader.loadThreadedRequest(path, cls.name);
|
|
180
|
+
if (err) {
|
|
181
|
+
throw new Error(`loadResourceAsync(${path}): loadThreadedRequest failed (${err})`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let result: InstanceType<C>;
|
|
185
|
+
while (true) {
|
|
186
|
+
const status = ResourceLoader.loadThreadedGetStatus(path);
|
|
187
|
+
if (status === ThreadLoadStatus.THREAD_LOAD_LOADED) {
|
|
188
|
+
result = ResourceLoader.loadThreadedGet(path) as InstanceType<C>;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
if (status === ThreadLoadStatus.THREAD_LOAD_FAILED || status === ThreadLoadStatus.THREAD_LOAD_INVALID_RESOURCE) {
|
|
192
|
+
throw new Error(`loadResourceAsync(${path}): load failed (status ${status})`);
|
|
193
|
+
}
|
|
194
|
+
await nextTick();
|
|
195
|
+
}
|
|
196
|
+
if (files && stageFile !== undefined) {
|
|
197
|
+
// Keep `.import` sidecars — they route imported source paths to their
|
|
198
|
+
// products (ResourceFormatImporter recognizes a path by its sidecar).
|
|
199
|
+
// Delete everything else: the products are read only on a cache miss, and
|
|
200
|
+
// the resource stays cached after loading, so the bytes are dead weight.
|
|
201
|
+
// Best-effort: a file may already be gone (another module's cleanup or a
|
|
202
|
+
// cache-miss re-stage).
|
|
203
|
+
for (const resPath of Object.keys(files)) {
|
|
204
|
+
if (!resPath.endsWith('.import')) {
|
|
205
|
+
DirAccess.removeAbsolute(resPath);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|