@ringozz/godot 4.7.1-8 → 4.7.1-9
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 +123 -123
- package/package.json +5 -2
- package/src/assets.d.ts +116 -116
- package/src/boot.browser.ts +39 -0
- package/src/boot.ts +15 -26
- package/src/debug.ts +236 -236
- package/src/index.ts +40 -40
- package/src/load.ts +210 -210
- package/src/preload.ts +198 -198
- package/src/runtime.ts +71 -72
- package/src/web-image.ts +118 -118
package/src/preload.ts
CHANGED
|
@@ -1,198 +1,198 @@
|
|
|
1
|
-
/**********************************************************************
|
|
2
|
-
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
-
***********************************************************************/
|
|
4
|
-
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
-
import { dirname, resolve } from 'node:path';
|
|
7
|
-
import { plugin } from 'bun';
|
|
8
|
-
import type { BunPlugin } from 'bun';
|
|
9
|
-
|
|
10
|
-
// Registers the godot-assets plugin: imports of Godot source assets become
|
|
11
|
-
// virtual modules (namespace `godot`) that call `loadAsset` (`@ringozz/godot/load`)
|
|
12
|
-
// and export the resulting `materialize` (own + deps' files) and load promise.
|
|
13
|
-
// `loadAsset` checks the engine's `ResourceCache` once; when the asset is
|
|
14
|
-
// already loaded (e.g. a re-evaluated module under web HMR) the load resolves to
|
|
15
|
-
// the cached instance and `materialize` short-circuits, skipping file staging
|
|
16
|
-
// and the threaded load. The load promise persists on the module's
|
|
17
|
-
// `import.meta.hot.data` (per-module state carried across hot replacements), so
|
|
18
|
-
// HMR re-evaluations return the *same* fulfilled promise object — React's `use()`
|
|
19
|
-
// keeps its tracked status and never re-suspends (no Suspense fallback flash); a
|
|
20
|
-
// rejected load is evicted from `data` so the next evaluation retries; on
|
|
21
|
-
// desktop `data` is `{}` and modules evaluate once. Dependencies are generated
|
|
22
|
-
// modules too, so a dep's load starts at module evaluation while the parent only
|
|
23
|
-
// waits for dep *files*. The default export is the single plugin — registered
|
|
24
|
-
// globally via bunfig preload and loaded by `[serve.static] plugins` for
|
|
25
|
-
// `Bun.serve` HTML routes.
|
|
26
|
-
|
|
27
|
-
const ROOT = process.cwd();
|
|
28
|
-
const NS = 'godot';
|
|
29
|
-
|
|
30
|
-
/** Format metadata: `cls` = Godot class (importable), `scan` = scan text refs, `native` = no `.import` sidecar. */
|
|
31
|
-
const FORMATS: Record<string, { cls?: string; scan?: boolean; native?: boolean }> = {
|
|
32
|
-
gltf: { cls: 'PackedScene', scan: true },
|
|
33
|
-
tscn: { cls: 'PackedScene', scan: true, native: true },
|
|
34
|
-
obj: { cls: 'PackedScene', scan: true },
|
|
35
|
-
jpg: { cls: 'CompressedTexture2D' },
|
|
36
|
-
jpeg: { cls: 'CompressedTexture2D' },
|
|
37
|
-
png: { cls: 'CompressedTexture2D' },
|
|
38
|
-
webp: { cls: 'CompressedTexture2D' },
|
|
39
|
-
svg: { cls: 'Texture2D' },
|
|
40
|
-
exr: { cls: 'TextureLayered' },
|
|
41
|
-
hdr: { cls: 'TextureLayered' },
|
|
42
|
-
wav: { cls: 'AudioStreamWAV' },
|
|
43
|
-
ogg: { cls: 'AudioStreamOggVorbis' },
|
|
44
|
-
mp3: { cls: 'AudioStreamMP3' },
|
|
45
|
-
tres: { cls: 'Resource', scan: true, native: true },
|
|
46
|
-
po: { cls: 'Translation', native: true },
|
|
47
|
-
mtl: { scan: true },
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const ASSET_RE = new RegExp(
|
|
51
|
-
`\\.(${Object.entries(FORMATS).filter(([, f]) => f.cls).map(([k]) => k).join('|')})$`,
|
|
52
|
-
'i',
|
|
53
|
-
);
|
|
54
|
-
const CLASS_BY_EXT: Record<string, string> = Object.fromEntries(
|
|
55
|
-
Object.entries(FORMATS).filter(([, f]) => f.cls).map(([k, f]) => [k, f.cls!]),
|
|
56
|
-
);
|
|
57
|
-
const SCANNED = new Set(Object.entries(FORMATS).filter(([, f]) => f.scan).map(([k]) => k));
|
|
58
|
-
const NATIVE = new Set(Object.entries(FORMATS).filter(([, f]) => f.native).map(([k]) => k));
|
|
59
|
-
|
|
60
|
-
const extOf = (abs: string) => abs.slice(abs.lastIndexOf('.') + 1).toLowerCase();
|
|
61
|
-
const resOf = (abs: string) => 'res://' + abs.slice(ROOT.length + 1).replaceAll('\\', '/');
|
|
62
|
-
const isScannableText = (abs: string) => SCANNED.has(extOf(abs));
|
|
63
|
-
const isNativeText = (abs: string) => NATIVE.has(extOf(abs));
|
|
64
|
-
const isSourceAsset = (abs: string) => existsSync(abs + '.import') || isNativeText(abs);
|
|
65
|
-
|
|
66
|
-
/** Reads the imported destination (`dest_files`) out of an `.import` sidecar. */
|
|
67
|
-
function parseSidecarDest(text: string): string | null {
|
|
68
|
-
const deps = text.match(/^dest_files=\[([^\]]*)\]/m);
|
|
69
|
-
if (deps) {
|
|
70
|
-
const first = deps[1].match(/"([^"]+)"/);
|
|
71
|
-
if (first) {
|
|
72
|
-
return first[1];
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const remap = text.match(/^path="([^"]+)"/m);
|
|
76
|
-
return remap ? remap[1] : null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Generalized reference extraction for text-based source assets: absolute
|
|
80
|
-
// `res://` paths (Godot scenes/resources) and relative path tokens ending in a
|
|
81
|
-
// known asset extension (glTF uris, OBJ/MTL textures). The literal dot before
|
|
82
|
-
// the extension avoids matching MIME types inside `data:` URIs.
|
|
83
|
-
const RES_RE = /res:\/\/[\w./\\-]+/g;
|
|
84
|
-
const REL_RE = new RegExp(`([\\w./\\\\-]+\\.(?:${Object.keys(FORMATS).join('|')}))`, 'g');
|
|
85
|
-
|
|
86
|
-
function extractRefs(text: string): string[] {
|
|
87
|
-
const refs: string[] = [];
|
|
88
|
-
for (const m of text.matchAll(RES_RE)) refs.push(m[0]);
|
|
89
|
-
for (const m of text.matchAll(REL_RE)) refs.push(m[1]);
|
|
90
|
-
return refs;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Adds a source asset's own files to `files`: imported assets contribute their
|
|
95
|
-
* `.import` sidecar (text) + imported product (file); native text formats
|
|
96
|
-
* (tscn/tres/po) contribute the file itself (text).
|
|
97
|
-
*/
|
|
98
|
-
function addSource(srcAbs: string, files: Map<string, 'text' | 'file'>): void {
|
|
99
|
-
const impAbs = srcAbs + '.import';
|
|
100
|
-
if (existsSync(impAbs)) {
|
|
101
|
-
files.set(resOf(srcAbs) + '.import', 'text');
|
|
102
|
-
const dest = parseSidecarDest(readFileSync(impAbs, 'utf8'));
|
|
103
|
-
if (dest) {
|
|
104
|
-
files.set(dest, 'file');
|
|
105
|
-
}
|
|
106
|
-
} else if (isNativeText(srcAbs)) {
|
|
107
|
-
files.set(resOf(srcAbs), 'text');
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Collects the referenced source assets (deps) of a text source: source assets
|
|
113
|
-
* become dep modules (each handles its own transitive deps); raw text formats
|
|
114
|
-
* without a sidecar (`.mtl`) are recursed into. Buffers (`.bin`) are skipped.
|
|
115
|
-
*/
|
|
116
|
-
function collectDeps(abs: string, visited: Set<string>, deps: string[]): void {
|
|
117
|
-
if (visited.has(abs)) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
visited.add(abs);
|
|
121
|
-
if (!isScannableText(abs)) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
for (const ref of extractRefs(readFileSync(abs, 'utf8'))) {
|
|
125
|
-
const refAbs = ref.startsWith('res://')
|
|
126
|
-
? resolve(ROOT, ref.slice('res://'.length))
|
|
127
|
-
: resolve(dirname(abs), ref);
|
|
128
|
-
if (visited.has(refAbs)) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (isSourceAsset(refAbs)) {
|
|
132
|
-
deps.push(refAbs);
|
|
133
|
-
} else if (isScannableText(refAbs)) {
|
|
134
|
-
collectDeps(refAbs, visited, deps);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function analyze(abs: string): { files: Map<string, 'text' | 'file'>; deps: string[] } {
|
|
140
|
-
const files = new Map<string, 'text' | 'file'>();
|
|
141
|
-
const deps: string[] = [];
|
|
142
|
-
addSource(abs, files);
|
|
143
|
-
collectDeps(abs, new Set(), deps);
|
|
144
|
-
return { files, deps };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function ensureModule(abs: string): string {
|
|
148
|
-
const res = resOf(abs);
|
|
149
|
-
const cls = CLASS_BY_EXT[extOf(abs)] ?? 'Resource';
|
|
150
|
-
const { files, deps } = analyze(abs);
|
|
151
|
-
|
|
152
|
-
const imports = [
|
|
153
|
-
`import { loadAsset } from '@ringozz/godot/load';`,
|
|
154
|
-
`import { ${cls} } from '@ringozz/godot/${cls}';`,
|
|
155
|
-
];
|
|
156
|
-
const map: string[] = [];
|
|
157
|
-
let ti = 0;
|
|
158
|
-
let fi = 0;
|
|
159
|
-
for (const [resPath, kind] of files) {
|
|
160
|
-
const absP = resolve(ROOT, resPath.slice('res://'.length));
|
|
161
|
-
const v = kind === 'text' ? `c${ti++}` : `f${fi++}`;
|
|
162
|
-
const type = kind === 'text' ? 'text' : 'file';
|
|
163
|
-
const field = kind === 'text' ? 'content' : 'path';
|
|
164
|
-
imports.push(`import ${v} from ${JSON.stringify(absP)} with { type: '${type}' };`);
|
|
165
|
-
map.push(` ${JSON.stringify(resPath)}: { ${field}: ${v} },`);
|
|
166
|
-
}
|
|
167
|
-
deps.forEach((dep, i) => imports.push(`import * as dep${i} from ${JSON.stringify(dep)};`));
|
|
168
|
-
|
|
169
|
-
const filesVar = files.size ? `const files = {\n${map.join('\n')}\n};` : `const files = {};`;
|
|
170
|
-
const loadAsset = `const { materialize, load } = loadAsset(${JSON.stringify(res)}, ${cls}, files, [${deps.map((_, i) => `dep${i}`).join(', ')}], import.meta.hot.data);`;
|
|
171
|
-
return imports.join('\n') + '\n' + filesVar + '\n' + loadAsset + '\n' + `export { materialize, load as default };` + '\n';
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const assetPlugin: BunPlugin = {
|
|
175
|
-
name: 'godot-assets',
|
|
176
|
-
setup(build) {
|
|
177
|
-
build.onResolve({ filter: ASSET_RE }, (args) => {
|
|
178
|
-
const importerAbs = args.importer?.startsWith(`${NS}:`)
|
|
179
|
-
? args.importer.slice(NS.length + 1)
|
|
180
|
-
: args.importer;
|
|
181
|
-
const base = importerAbs ? dirname(importerAbs) : (args.resolveDir ?? ROOT);
|
|
182
|
-
const abs = resolve(base, args.path);
|
|
183
|
-
// A module's own native-text source (e.g. `.tscn`) self-imports the
|
|
184
|
-
// same path — let it load as a real file.
|
|
185
|
-
if (abs === importerAbs) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
return { path: abs, namespace: NS };
|
|
189
|
-
});
|
|
190
|
-
build.onLoad({ filter: /.*/, namespace: NS }, (args) => ({
|
|
191
|
-
contents: ensureModule(args.path),
|
|
192
|
-
loader: 'js',
|
|
193
|
-
}));
|
|
194
|
-
},
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
export default assetPlugin;
|
|
198
|
-
plugin(assetPlugin);
|
|
1
|
+
/**********************************************************************
|
|
2
|
+
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
+
***********************************************************************/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, resolve } from 'node:path';
|
|
7
|
+
import { plugin } from 'bun';
|
|
8
|
+
import type { BunPlugin } from 'bun';
|
|
9
|
+
|
|
10
|
+
// Registers the godot-assets plugin: imports of Godot source assets become
|
|
11
|
+
// virtual modules (namespace `godot`) that call `loadAsset` (`@ringozz/godot/load`)
|
|
12
|
+
// and export the resulting `materialize` (own + deps' files) and load promise.
|
|
13
|
+
// `loadAsset` checks the engine's `ResourceCache` once; when the asset is
|
|
14
|
+
// already loaded (e.g. a re-evaluated module under web HMR) the load resolves to
|
|
15
|
+
// the cached instance and `materialize` short-circuits, skipping file staging
|
|
16
|
+
// and the threaded load. The load promise persists on the module's
|
|
17
|
+
// `import.meta.hot.data` (per-module state carried across hot replacements), so
|
|
18
|
+
// HMR re-evaluations return the *same* fulfilled promise object — React's `use()`
|
|
19
|
+
// keeps its tracked status and never re-suspends (no Suspense fallback flash); a
|
|
20
|
+
// rejected load is evicted from `data` so the next evaluation retries; on
|
|
21
|
+
// desktop `data` is `{}` and modules evaluate once. Dependencies are generated
|
|
22
|
+
// modules too, so a dep's load starts at module evaluation while the parent only
|
|
23
|
+
// waits for dep *files*. The default export is the single plugin — registered
|
|
24
|
+
// globally via bunfig preload and loaded by `[serve.static] plugins` for
|
|
25
|
+
// `Bun.serve` HTML routes.
|
|
26
|
+
|
|
27
|
+
const ROOT = process.cwd();
|
|
28
|
+
const NS = 'godot';
|
|
29
|
+
|
|
30
|
+
/** Format metadata: `cls` = Godot class (importable), `scan` = scan text refs, `native` = no `.import` sidecar. */
|
|
31
|
+
const FORMATS: Record<string, { cls?: string; scan?: boolean; native?: boolean }> = {
|
|
32
|
+
gltf: { cls: 'PackedScene', scan: true },
|
|
33
|
+
tscn: { cls: 'PackedScene', scan: true, native: true },
|
|
34
|
+
obj: { cls: 'PackedScene', scan: true },
|
|
35
|
+
jpg: { cls: 'CompressedTexture2D' },
|
|
36
|
+
jpeg: { cls: 'CompressedTexture2D' },
|
|
37
|
+
png: { cls: 'CompressedTexture2D' },
|
|
38
|
+
webp: { cls: 'CompressedTexture2D' },
|
|
39
|
+
svg: { cls: 'Texture2D' },
|
|
40
|
+
exr: { cls: 'TextureLayered' },
|
|
41
|
+
hdr: { cls: 'TextureLayered' },
|
|
42
|
+
wav: { cls: 'AudioStreamWAV' },
|
|
43
|
+
ogg: { cls: 'AudioStreamOggVorbis' },
|
|
44
|
+
mp3: { cls: 'AudioStreamMP3' },
|
|
45
|
+
tres: { cls: 'Resource', scan: true, native: true },
|
|
46
|
+
po: { cls: 'Translation', native: true },
|
|
47
|
+
mtl: { scan: true },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ASSET_RE = new RegExp(
|
|
51
|
+
`\\.(${Object.entries(FORMATS).filter(([, f]) => f.cls).map(([k]) => k).join('|')})$`,
|
|
52
|
+
'i',
|
|
53
|
+
);
|
|
54
|
+
const CLASS_BY_EXT: Record<string, string> = Object.fromEntries(
|
|
55
|
+
Object.entries(FORMATS).filter(([, f]) => f.cls).map(([k, f]) => [k, f.cls!]),
|
|
56
|
+
);
|
|
57
|
+
const SCANNED = new Set(Object.entries(FORMATS).filter(([, f]) => f.scan).map(([k]) => k));
|
|
58
|
+
const NATIVE = new Set(Object.entries(FORMATS).filter(([, f]) => f.native).map(([k]) => k));
|
|
59
|
+
|
|
60
|
+
const extOf = (abs: string) => abs.slice(abs.lastIndexOf('.') + 1).toLowerCase();
|
|
61
|
+
const resOf = (abs: string) => 'res://' + abs.slice(ROOT.length + 1).replaceAll('\\', '/');
|
|
62
|
+
const isScannableText = (abs: string) => SCANNED.has(extOf(abs));
|
|
63
|
+
const isNativeText = (abs: string) => NATIVE.has(extOf(abs));
|
|
64
|
+
const isSourceAsset = (abs: string) => existsSync(abs + '.import') || isNativeText(abs);
|
|
65
|
+
|
|
66
|
+
/** Reads the imported destination (`dest_files`) out of an `.import` sidecar. */
|
|
67
|
+
function parseSidecarDest(text: string): string | null {
|
|
68
|
+
const deps = text.match(/^dest_files=\[([^\]]*)\]/m);
|
|
69
|
+
if (deps) {
|
|
70
|
+
const first = deps[1].match(/"([^"]+)"/);
|
|
71
|
+
if (first) {
|
|
72
|
+
return first[1];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const remap = text.match(/^path="([^"]+)"/m);
|
|
76
|
+
return remap ? remap[1] : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Generalized reference extraction for text-based source assets: absolute
|
|
80
|
+
// `res://` paths (Godot scenes/resources) and relative path tokens ending in a
|
|
81
|
+
// known asset extension (glTF uris, OBJ/MTL textures). The literal dot before
|
|
82
|
+
// the extension avoids matching MIME types inside `data:` URIs.
|
|
83
|
+
const RES_RE = /res:\/\/[\w./\\-]+/g;
|
|
84
|
+
const REL_RE = new RegExp(`([\\w./\\\\-]+\\.(?:${Object.keys(FORMATS).join('|')}))`, 'g');
|
|
85
|
+
|
|
86
|
+
function extractRefs(text: string): string[] {
|
|
87
|
+
const refs: string[] = [];
|
|
88
|
+
for (const m of text.matchAll(RES_RE)) refs.push(m[0]);
|
|
89
|
+
for (const m of text.matchAll(REL_RE)) refs.push(m[1]);
|
|
90
|
+
return refs;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Adds a source asset's own files to `files`: imported assets contribute their
|
|
95
|
+
* `.import` sidecar (text) + imported product (file); native text formats
|
|
96
|
+
* (tscn/tres/po) contribute the file itself (text).
|
|
97
|
+
*/
|
|
98
|
+
function addSource(srcAbs: string, files: Map<string, 'text' | 'file'>): void {
|
|
99
|
+
const impAbs = srcAbs + '.import';
|
|
100
|
+
if (existsSync(impAbs)) {
|
|
101
|
+
files.set(resOf(srcAbs) + '.import', 'text');
|
|
102
|
+
const dest = parseSidecarDest(readFileSync(impAbs, 'utf8'));
|
|
103
|
+
if (dest) {
|
|
104
|
+
files.set(dest, 'file');
|
|
105
|
+
}
|
|
106
|
+
} else if (isNativeText(srcAbs)) {
|
|
107
|
+
files.set(resOf(srcAbs), 'text');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Collects the referenced source assets (deps) of a text source: source assets
|
|
113
|
+
* become dep modules (each handles its own transitive deps); raw text formats
|
|
114
|
+
* without a sidecar (`.mtl`) are recursed into. Buffers (`.bin`) are skipped.
|
|
115
|
+
*/
|
|
116
|
+
function collectDeps(abs: string, visited: Set<string>, deps: string[]): void {
|
|
117
|
+
if (visited.has(abs)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
visited.add(abs);
|
|
121
|
+
if (!isScannableText(abs)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const ref of extractRefs(readFileSync(abs, 'utf8'))) {
|
|
125
|
+
const refAbs = ref.startsWith('res://')
|
|
126
|
+
? resolve(ROOT, ref.slice('res://'.length))
|
|
127
|
+
: resolve(dirname(abs), ref);
|
|
128
|
+
if (visited.has(refAbs)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (isSourceAsset(refAbs)) {
|
|
132
|
+
deps.push(refAbs);
|
|
133
|
+
} else if (isScannableText(refAbs)) {
|
|
134
|
+
collectDeps(refAbs, visited, deps);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function analyze(abs: string): { files: Map<string, 'text' | 'file'>; deps: string[] } {
|
|
140
|
+
const files = new Map<string, 'text' | 'file'>();
|
|
141
|
+
const deps: string[] = [];
|
|
142
|
+
addSource(abs, files);
|
|
143
|
+
collectDeps(abs, new Set(), deps);
|
|
144
|
+
return { files, deps };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ensureModule(abs: string): string {
|
|
148
|
+
const res = resOf(abs);
|
|
149
|
+
const cls = CLASS_BY_EXT[extOf(abs)] ?? 'Resource';
|
|
150
|
+
const { files, deps } = analyze(abs);
|
|
151
|
+
|
|
152
|
+
const imports = [
|
|
153
|
+
`import { loadAsset } from '@ringozz/godot/load';`,
|
|
154
|
+
`import { ${cls} } from '@ringozz/godot/${cls}';`,
|
|
155
|
+
];
|
|
156
|
+
const map: string[] = [];
|
|
157
|
+
let ti = 0;
|
|
158
|
+
let fi = 0;
|
|
159
|
+
for (const [resPath, kind] of files) {
|
|
160
|
+
const absP = resolve(ROOT, resPath.slice('res://'.length));
|
|
161
|
+
const v = kind === 'text' ? `c${ti++}` : `f${fi++}`;
|
|
162
|
+
const type = kind === 'text' ? 'text' : 'file';
|
|
163
|
+
const field = kind === 'text' ? 'content' : 'path';
|
|
164
|
+
imports.push(`import ${v} from ${JSON.stringify(absP)} with { type: '${type}' };`);
|
|
165
|
+
map.push(` ${JSON.stringify(resPath)}: { ${field}: ${v} },`);
|
|
166
|
+
}
|
|
167
|
+
deps.forEach((dep, i) => imports.push(`import * as dep${i} from ${JSON.stringify(dep)};`));
|
|
168
|
+
|
|
169
|
+
const filesVar = files.size ? `const files = {\n${map.join('\n')}\n};` : `const files = {};`;
|
|
170
|
+
const loadAsset = `const { materialize, load } = loadAsset(${JSON.stringify(res)}, ${cls}, files, [${deps.map((_, i) => `dep${i}`).join(', ')}], import.meta.hot.data);`;
|
|
171
|
+
return imports.join('\n') + '\n' + filesVar + '\n' + loadAsset + '\n' + `export { materialize, load as default };` + '\n';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const assetPlugin: BunPlugin = {
|
|
175
|
+
name: 'godot-assets',
|
|
176
|
+
setup(build) {
|
|
177
|
+
build.onResolve({ filter: ASSET_RE }, (args) => {
|
|
178
|
+
const importerAbs = args.importer?.startsWith(`${NS}:`)
|
|
179
|
+
? args.importer.slice(NS.length + 1)
|
|
180
|
+
: args.importer;
|
|
181
|
+
const base = importerAbs ? dirname(importerAbs) : (args.resolveDir ?? ROOT);
|
|
182
|
+
const abs = resolve(base, args.path);
|
|
183
|
+
// A module's own native-text source (e.g. `.tscn`) self-imports the
|
|
184
|
+
// same path — let it load as a real file.
|
|
185
|
+
if (abs === importerAbs) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
return { path: abs, namespace: NS };
|
|
189
|
+
});
|
|
190
|
+
build.onLoad({ filter: /.*/, namespace: NS }, (args) => ({
|
|
191
|
+
contents: ensureModule(args.path),
|
|
192
|
+
loader: 'js',
|
|
193
|
+
}));
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export default assetPlugin;
|
|
198
|
+
plugin(assetPlugin);
|
package/src/runtime.ts
CHANGED
|
@@ -1,72 +1,71 @@
|
|
|
1
|
-
/**********************************************************************
|
|
2
|
-
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
-
***********************************************************************/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* - _C(
|
|
28
|
-
* - _C(
|
|
29
|
-
* - _C(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
1
|
+
/**********************************************************************
|
|
2
|
+
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
+
***********************************************************************/
|
|
4
|
+
|
|
5
|
+
/** Shared runtime: platform-specific native-module selection lives in
|
|
6
|
+
* `boot.ts` (desktop) / `boot.browser.ts` (web, via the package's
|
|
7
|
+
* `browser` field). `_mod` is the native module — the desktop addon's
|
|
8
|
+
* `module.exports` or the wasm module's `.default`.
|
|
9
|
+
*/
|
|
10
|
+
import { getNativeModule } from './boot.ts';
|
|
11
|
+
const { default: _mod } = getNativeModule() as any;
|
|
12
|
+
|
|
13
|
+
/** Godot instance */
|
|
14
|
+
import type { GodotInstance } from '../gen/classes/GodotInstance.ts';
|
|
15
|
+
export const getGodot = _mod.getGodot as () => GodotInstance;
|
|
16
|
+
|
|
17
|
+
/** Base constructor for all Godot object wrappers. */
|
|
18
|
+
export interface GodotVar {
|
|
19
|
+
free(): void;
|
|
20
|
+
}
|
|
21
|
+
export const GodotVar = _mod.GodotVar as (new (...args: unknown[]) => GodotVar) & {
|
|
22
|
+
assign<T extends GodotVar>(target: T, props: Record<string, unknown>): T;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Unified entry point:
|
|
26
|
+
* - _C(this, methodId, ...args) → instance method
|
|
27
|
+
* - _C(classId, methodId, ...args) → static method
|
|
28
|
+
* - _C(null, methodId, ...args) → singleton lookup (_getInstance) or internal
|
|
29
|
+
* - _C(0xHASHn, methodId, ...args) → utility function (BigInt carries hash + type metadata)
|
|
30
|
+
*/
|
|
31
|
+
export const _C = _mod._C as (...args: unknown[]) => unknown;
|
|
32
|
+
|
|
33
|
+
/** Resolve a string name to its integer handle. */
|
|
34
|
+
export const _S = _mod._S as (name: string) => number;
|
|
35
|
+
|
|
36
|
+
/** Low-level property getter for value type fields. */
|
|
37
|
+
export const _get = _mod._get as (target: GodotVar, nameId: number) => unknown;
|
|
38
|
+
|
|
39
|
+
/** Low-level property setter for value type fields. */
|
|
40
|
+
export const _set = _mod._set as (target: GodotVar, nameId: number, val: unknown) => void;
|
|
41
|
+
|
|
42
|
+
/** Register Godot object wrapper. */
|
|
43
|
+
export const _R = _mod._R as (typeId: number, ctor: Function) => string;
|
|
44
|
+
|
|
45
|
+
/** Stage bytes into the web MEMFS (Godot's `copyToFS`); undefined on desktop. */
|
|
46
|
+
export const stageFile = (_mod as any).stageFile as ((path: string, bytes: Uint8Array) => void) | undefined;
|
|
47
|
+
|
|
48
|
+
/** Get a Signal value from an Object by signal name, or connect a callback when 3rd arg given. */
|
|
49
|
+
export const _G = _mod._G as (target: GodotVar, signalNameId: number, callback?: unknown) => unknown;
|
|
50
|
+
|
|
51
|
+
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DedicatedWorkerGlobalScope/requestAnimationFrame) */
|
|
52
|
+
export const requestAnimationFrame = _mod.requestAnimationFrame as typeof globalThis.requestAnimationFrame ?? globalThis.requestAnimationFrame;
|
|
53
|
+
|
|
54
|
+
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DedicatedWorkerGlobalScope/cancelAnimationFrame) */
|
|
55
|
+
export const cancelAnimationFrame = _mod.cancelAnimationFrame as typeof globalThis.cancelAnimationFrame ?? globalThis.cancelAnimationFrame;
|
|
56
|
+
|
|
57
|
+
/** Make a Signal PromiseLike: connects a one-shot callback that resolves with the args tuple. */
|
|
58
|
+
export function _P(signal: any, onfulfilled?: any, onrejected?: any) {
|
|
59
|
+
return new Promise<any>((resolve, reject) => {
|
|
60
|
+
if (signal.isNull()) {
|
|
61
|
+
reject(new Error('Cannot await a null Signal'));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const cb = (...args: any[]) => {
|
|
65
|
+
signal.disconnect(cb);
|
|
66
|
+
resolve(args);
|
|
67
|
+
};
|
|
68
|
+
const res = signal.connect(cb);
|
|
69
|
+
if (res !== 0) reject(new Error('Failed to connect signal: error code ' + res));
|
|
70
|
+
}).then(onfulfilled, onrejected);
|
|
71
|
+
}
|