@ringozz/godot 4.7.2-661 → 4.7.2-676
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 +28 -5
- package/package.json +5 -4
- package/src/assets.d.ts +10 -1
- package/src/bake.ts +587 -0
- package/src/dump-deps.gd +140 -66
- package/src/editor.ts +6 -1
- package/src/export_presets.example.cfg +3 -3
- package/src/formats.ts +78 -0
- package/src/load.ts +169 -17
- package/src/pack.ts +276 -220
- package/src/preload.ts +103 -150
- package/src/serve.ts +67 -0
package/src/pack.ts
CHANGED
|
@@ -2,259 +2,315 @@
|
|
|
2
2
|
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
3
|
***********************************************************************/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// Bakes a Godot project's assets for the runtime: imports them, then exports one native `.pck` **per
|
|
12
|
-
// level** plus a shared one, so a host fetches the shared data and only the level it is playing.
|
|
13
|
-
// `dump-deps.gd` supplies both inputs (no host-side scanning), in the dump's own two words: each
|
|
14
|
-
// source's `packed` closure — which decides what a pack *owns* — and the `loose` (WebP-backed)
|
|
15
|
-
// texture sources, which the host stages and transcodes per file.
|
|
5
|
+
// The pack protocol: the document a bake emits for every pack, the names that document and its pack are
|
|
6
|
+
// written and read under, the entries a pack declares, and the resolution that turns one entry into the
|
|
7
|
+
// packs to mount and the loose files to stage before it is loaded. Three callers speak it — the bake
|
|
8
|
+
// (`bake.ts`, which writes the documents), the preload plugin (which reads them at build time to serve
|
|
9
|
+
// a pack's module) and the runtime loader (which fetches them) — so the shapes live here once instead of
|
|
10
|
+
// being re-spelled by each of them.
|
|
16
11
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
12
|
+
// Nothing here touches the filesystem, the editor or the engine. A document arrives as text, and a
|
|
13
|
+
// resolved file names the pack that owns its product rather than a URL: how a pack's bytes are
|
|
14
|
+
// addressed — fetched over the network, bundled beside the app, or read off the disk — belongs to the
|
|
15
|
+
// caller doing the I/O, which is also the only caller that knows which of the three it is.
|
|
16
|
+
|
|
17
|
+
import { classOf } from './formats.ts';
|
|
18
|
+
|
|
19
|
+
/** One pack in a release: the version to mount, and its declared dependencies by pack name. */
|
|
20
|
+
export interface ReleasePack {
|
|
21
|
+
version: string;
|
|
22
|
+
deps: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A release: pack name → what to mount. */
|
|
26
|
+
export type Release = Record<string, ReleasePack>;
|
|
27
|
+
|
|
28
|
+
/** A `bun.lock` package entry: `[name@version, registry, { dependencies? }, "sha512-…"]`. */
|
|
29
|
+
type LockEntry = [string, string, { dependencies?: Record<string, string>; optionalDependencies?: Record<string, string> }?, ...unknown[]];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parses a release document: a `bun.lock` (whose `packages` map is flattened to one entry per name,
|
|
33
|
+
* with the dependency *names* as its edges) or an explicit `{ name: { version, deps } }` map. The
|
|
34
|
+
* edges are names, not ranges: the lockfile already chose one version per name, so nothing here has to
|
|
35
|
+
* resolve semver. Throws on anything else — a document this can't read would mount nothing and show up
|
|
36
|
+
* as missing assets.
|
|
37
|
+
*/
|
|
38
|
+
export function parseRelease(text: string): Release {
|
|
39
|
+
const doc = JSON.parse(text.replace(/,\s*([}\]])/g, '$1')) as Record<string, unknown>;
|
|
40
|
+
const packages = doc.packages as Record<string, LockEntry> | undefined;
|
|
41
|
+
if (packages) {
|
|
42
|
+
const release: Release = {};
|
|
43
|
+
for (const [name, entry] of Object.entries(packages)) {
|
|
44
|
+
if (!Array.isArray(entry) || typeof entry[0] !== 'string') continue;
|
|
45
|
+
const at = entry[0].lastIndexOf('@');
|
|
46
|
+
release[name] = { version: at > 0 ? entry[0].slice(at + 1) : entry[0], deps: depsOf(entry) };
|
|
47
|
+
}
|
|
48
|
+
if (!Object.keys(release).length) throw new Error('godot-assets: the release document has no packages.');
|
|
49
|
+
return release;
|
|
50
|
+
}
|
|
51
|
+
const values = Object.values(doc);
|
|
52
|
+
if (values.length && values.every(isPack)) {
|
|
53
|
+
return Object.fromEntries(
|
|
54
|
+
Object.entries(doc).map(([name, pack]) => [
|
|
55
|
+
name,
|
|
56
|
+
{ version: (pack as ReleasePack).version, deps: (pack as ReleasePack).deps ?? [] },
|
|
57
|
+
]),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
throw new Error('godot-assets: unrecognized release document (expected a bun.lock or a name → {version, deps} map).');
|
|
42
61
|
}
|
|
43
62
|
|
|
44
|
-
/** A
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
name: string;
|
|
48
|
-
/** The packable sources this pack owns — the lookup the plugin mounts by. */
|
|
49
|
-
owns: string[];
|
|
63
|
+
/** A value shaped like a {@link ReleasePack}. */
|
|
64
|
+
function isPack(value: unknown): boolean {
|
|
65
|
+
return !!value && typeof value === 'object' && typeof (value as ReleasePack).version === 'string';
|
|
50
66
|
}
|
|
51
67
|
|
|
52
|
-
/** A
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
pck: string;
|
|
68
|
+
/** A lock entry's dependency *names* — the lockfile already chose one version per name. */
|
|
69
|
+
function depsOf(entry: LockEntry): string[] {
|
|
70
|
+
return Object.keys(entry[2]?.dependencies ?? entry[2]?.optionalDependencies ?? {});
|
|
56
71
|
}
|
|
57
72
|
|
|
58
|
-
/** The
|
|
59
|
-
export
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
/** The packs to mount for `entry`: the pack itself, then every pack its dependencies reach (depth first). */
|
|
74
|
+
export function mountClosure(release: Release, entry: string): string[] {
|
|
75
|
+
if (!release[entry]) throw new Error(`godot-assets: the release has no pack ${entry}.`);
|
|
76
|
+
const out: string[] = [];
|
|
77
|
+
const seen = new Set<string>();
|
|
78
|
+
const visit = (name: string) => {
|
|
79
|
+
if (seen.has(name)) return;
|
|
80
|
+
const pack = release[name];
|
|
81
|
+
if (!pack) throw new Error(`godot-assets: the release has no pack ${name} (a declared dependency).`);
|
|
82
|
+
seen.add(name);
|
|
83
|
+
out.push(name);
|
|
84
|
+
for (const dep of pack.deps) visit(dep);
|
|
85
|
+
};
|
|
86
|
+
visit(entry);
|
|
87
|
+
return out;
|
|
63
88
|
}
|
|
64
89
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
90
|
+
/** The `.pck` beside a pack's document: its name's last segment (a scoped name stays flat). The one
|
|
91
|
+
* convention the bake's writer and the runtime's reader share — mounting resolves it from the release
|
|
92
|
+
* alone, so it never waits on the document. */
|
|
93
|
+
export function pckOf(name: string): string {
|
|
94
|
+
return `${name.split('/').pop()}.pck`;
|
|
95
|
+
}
|
|
67
96
|
|
|
68
|
-
|
|
69
|
-
|
|
97
|
+
// The names a pack is written and read under, shared by the bake (the writer) and its readers (the
|
|
98
|
+
// preload plugin and the runtime loader).
|
|
70
99
|
|
|
71
100
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* rides its pack, though it lives in a shared asset tree), while **a source several levels reach, or
|
|
76
|
-
* none does, is shared**. A level whose own pack would be empty is dropped: every source it reaches
|
|
77
|
-
* is reached by another level too, so it is already in `shared`, and a pack is mounted by *owner*.
|
|
78
|
-
*
|
|
79
|
-
* When a project stops baking one scene per level — a level built from its plan at run time is a
|
|
80
|
-
* single shared scene plus the plan — the level unit becomes the `.gltfx` its plan lives in instead;
|
|
81
|
-
* until then both units exist and this keying keeps the scenes.
|
|
101
|
+
* A pack's document, beside its `.pck`. Not `package.json`: that is the pack's own manifest in its
|
|
102
|
+
* source directory — an input the bake reads to discover a pack — and one string naming both an input
|
|
103
|
+
* and an output is one string nobody can read unambiguously.
|
|
82
104
|
*/
|
|
83
|
-
export
|
|
84
|
-
const stem = (asset: string) =>
|
|
85
|
-
asset.slice(asset.lastIndexOf('/') + 1).replace(/\.(prefab|level)\.[^.]+$/i, '').replace(/\.[^.]+$/, '');
|
|
86
|
-
// A level is its scene — and, in a project that builds levels from their plans at run time, the
|
|
87
|
-
// `.gltfx` its plan lives in. A `.gltfx` a scene **reaches** is that scene's composition, not a
|
|
88
|
-
// second level: keying both would make every source of a baked level look shared.
|
|
89
|
-
const scenes = Object.keys(packed).filter((s) => s.toLowerCase().endsWith('.tscn')).sort();
|
|
90
|
-
const reachedByScene = new Set(scenes.flatMap((s) => packed[s] ?? []));
|
|
91
|
-
const levels = [
|
|
92
|
-
...scenes,
|
|
93
|
-
...Object.keys(packed).filter((s) => s.toLowerCase().endsWith('.gltfx') && !reachedByScene.has(s)),
|
|
94
|
-
].sort();
|
|
95
|
-
const closure = new Map(levels.map((s) => [s, new Set([s, ...(packed[s] ?? [])])]));
|
|
96
|
-
|
|
97
|
-
// One pack per level, named after its file stem (`shared` is reserved; a repeated stem —
|
|
98
|
-
// two `level.tscn` in different directories — gets a suffix).
|
|
99
|
-
const used = new Set(['shared']);
|
|
100
|
-
const packOf = new Map<string, Pack>();
|
|
101
|
-
for (const level of levels) {
|
|
102
|
-
let name = stem(level);
|
|
103
|
-
for (let n = 2; used.has(name); n++) name = `${stem(level)}-${n}`;
|
|
104
|
-
used.add(name);
|
|
105
|
-
packOf.set(level, { name, owns: [] });
|
|
106
|
-
}
|
|
105
|
+
export const PACK_DOC = 'pack.json';
|
|
107
106
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
107
|
+
/** The release document: which version of which pack a client mounts. */
|
|
108
|
+
export const RELEASE = 'release.json';
|
|
109
|
+
|
|
110
|
+
/** A pack's directory of loose products, beside its document. */
|
|
111
|
+
export const FILES_DIR = 'files';
|
|
112
|
+
|
|
113
|
+
/** The bake's output directory, project-relative, unless {@link PACK_DIR_ENV} names another. */
|
|
114
|
+
export const PACK_DIR = '.godot/packs';
|
|
115
|
+
|
|
116
|
+
/** The environment variable that overrides {@link PACK_DIR}. */
|
|
117
|
+
export const PACK_DIR_ENV = 'GODOT_PACKS';
|
|
118
|
+
|
|
119
|
+
/** The boot pack's `.pck`, in the bake's output directory. */
|
|
120
|
+
export const BOOT_PCK = 'boot.pck';
|
|
121
|
+
|
|
122
|
+
/** The boot pack's document — the `needs`/`stage` every document carries, for the bake's own pack. */
|
|
123
|
+
export const BOOT_MANIFEST = 'packs.json';
|
|
124
|
+
|
|
125
|
+
/** The generated module an app's bundle imports for the boot pack (`{ mounting }` + its config). */
|
|
126
|
+
export const BOOT_MODULE = 'boot-pack.js';
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* One pack, as a document: what the pack **declares** (`name`/`version`/`dependencies`/`exports` — its
|
|
130
|
+
* own `package.json`) plus the `needs`/`stage` a client stages from. Both kinds of pack carry the
|
|
131
|
+
* *same* document: a content pack's is emitted beside its `.pck`, the boot pack's is `<out>/packs.json`.
|
|
132
|
+
* So the bake's writer, the preload plugin's build-time read and the runtime's fetch all read one
|
|
133
|
+
* shape.
|
|
134
|
+
*/
|
|
135
|
+
export interface PackDoc {
|
|
136
|
+
/** Declared package name (the boot pack is the app's own, and declares none). */
|
|
137
|
+
name?: string;
|
|
138
|
+
/** Declared version (the release pins the one a client mounts). */
|
|
139
|
+
version?: string;
|
|
140
|
+
/** Declared dependencies: package name → version range. */
|
|
141
|
+
dependencies?: Record<string, string>;
|
|
142
|
+
/** Declared entries verbatim — a subpath → its conditions, one of which is `godot`. */
|
|
143
|
+
exports?: Exports;
|
|
144
|
+
/** Packed source → the loose files it needs (paths, whoever owns them). */
|
|
145
|
+
needs: Record<string, string[]>;
|
|
146
|
+
/** A loose source this pack owns → the sidecar text and the product it remaps to. */
|
|
147
|
+
stage: Record<string, { path: string; import: string }>;
|
|
121
148
|
}
|
|
122
149
|
|
|
150
|
+
/** A package manifest's `exports`, as far as this project reads it: a subpath → its conditions. */
|
|
151
|
+
export type Exports = Record<string, string | { [condition: string]: string | undefined }> | undefined;
|
|
152
|
+
|
|
123
153
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* `shared.pck` and the same manifest.
|
|
154
|
+
* A pack document, validated: its manifest half (`needs`/`stage`) must be there — a document without
|
|
155
|
+
* one would stage nothing and show up much later as missing assets. `null` when it isn't a pack.
|
|
127
156
|
*/
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const roots = opts.roots ?? ['res://src'];
|
|
132
|
-
const out = opts.out ?? '.godot/packs';
|
|
133
|
-
const base = opts.preset ?? 'Pack';
|
|
134
|
-
const prefix = `${base}: `;
|
|
135
|
-
const presets = resolve(cwd, 'export_presets.cfg');
|
|
136
|
-
const cfg = readFileSync(presets, 'utf8');
|
|
137
|
-
const basePreset = presetBlock(cfg, base);
|
|
138
|
-
mkdirSync(resolve(cwd, out), { recursive: true });
|
|
139
|
-
|
|
140
|
-
const run = runner(editor, cwd);
|
|
141
|
-
run(['--headless', '--path', '.', '--import']);
|
|
142
|
-
// `dump-deps.gd` reads a level's plan for the refs no resource edge exposes, so its `loose` lists
|
|
143
|
-
// carry the level's whole closure with or without a baked scene.
|
|
144
|
-
const { packed, loose } = dump(run, cwd, roots);
|
|
145
|
-
const packs = splitPacks(packed);
|
|
146
|
-
|
|
147
|
-
// One preset per pack, all written from the project's own base preset before any export: each
|
|
148
|
-
// excludes exactly the sources the other packs own (see the module comment).
|
|
149
|
-
const original = pruneGenerated(cfg, prefix);
|
|
150
|
-
const first = (original.match(/^\[preset\.\d+\]/gm) ?? []).length;
|
|
151
|
-
const baseExcludes = excludeFilter(basePreset);
|
|
152
|
-
const generated = packs.map((pack, i) =>
|
|
153
|
-
presetFor(basePreset, first + i, `${prefix}${pack.name}`, excludesFor(baseExcludes, packs, pack)),
|
|
154
|
-
);
|
|
155
|
-
try {
|
|
156
|
-
writeFileSync(presets, `${original.trimEnd()}\n\n${generated.join('\n\n')}\n`);
|
|
157
|
-
const manifest: PackFile[] = [];
|
|
158
|
-
for (const { name, owns } of packs) {
|
|
159
|
-
const pck = `${out}/${name}.pck`;
|
|
160
|
-
run(['--headless', '--path', '.', '--export-pack', `${prefix}${name}`, pck]);
|
|
161
|
-
manifest.push({ name, pck: resPath(pck), owns });
|
|
162
|
-
}
|
|
163
|
-
writeFileSync(
|
|
164
|
-
resolve(cwd, out, 'packs.json'),
|
|
165
|
-
`${JSON.stringify({ packs: manifest, loose } satisfies PackManifest, null, '\t')}\n`,
|
|
166
|
-
);
|
|
167
|
-
const sources = manifest.reduce((n, pack) => n + pack.owns.length, 0);
|
|
168
|
-
console.log(`[pack] ${manifest.length} packs (${manifest.map((p) => `${p.name}:${p.owns.length}`).join(', ')}) over ${sources} sources`);
|
|
169
|
-
} finally {
|
|
170
|
-
// Derived — leave the project's own file as it was, so a bake does not churn a tracked,
|
|
171
|
-
// editor-managed file.
|
|
172
|
-
writeFileSync(presets, original);
|
|
173
|
-
}
|
|
157
|
+
function packDoc(document: unknown): PackDoc | null {
|
|
158
|
+
const doc = document as PackDoc | null;
|
|
159
|
+
return doc && doc.needs && doc.stage ? doc : null;
|
|
174
160
|
}
|
|
175
161
|
|
|
176
|
-
/** A
|
|
177
|
-
function
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
throw new Error(`godot-assets: \`${editor} ${args.join(' ')}\` failed (${r.status})`);
|
|
182
|
-
}
|
|
183
|
-
};
|
|
162
|
+
/** A document's text, as the shape it is: throws when the text is not a pack document. */
|
|
163
|
+
export function parseDoc(text: string): PackDoc {
|
|
164
|
+
const doc = packDoc(JSON.parse(text) as unknown);
|
|
165
|
+
if (!doc) throw new Error('godot-assets: the document carries no manifest half (`needs`/`stage`).');
|
|
166
|
+
return doc;
|
|
184
167
|
}
|
|
185
168
|
|
|
186
|
-
/**
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
run(['--headless', '--path', '.', '--script', 'res://.godot/pack-dump-deps.gd'], {
|
|
190
|
-
GODOT_DUMP_OUT: resPath(DUMP),
|
|
191
|
-
GODOT_DUMP_ROOTS: roots.join(','),
|
|
192
|
-
});
|
|
193
|
-
const abs = resolve(cwd, DUMP);
|
|
194
|
-
if (!existsSync(abs)) throw new Error(`godot-assets: the dump wrote no ${DUMP} (see dump-deps.gd).`);
|
|
195
|
-
const sources = JSON.parse(readFileSync(abs, 'utf8')) as Record<string, { packed: string[]; loose: string[] }>;
|
|
196
|
-
const entries = Object.entries(sources);
|
|
197
|
-
return {
|
|
198
|
-
packed: Object.fromEntries(entries.map(([source, s]) => [source, s.packed])),
|
|
199
|
-
loose: Object.fromEntries(entries.map(([source, s]) => [source, s.loose])),
|
|
200
|
-
};
|
|
169
|
+
/** A document as it is written: one tab-indented JSON document, with a trailing newline. */
|
|
170
|
+
export function serializeDoc(value: PackDoc | Release): string {
|
|
171
|
+
return `${JSON.stringify(value, null, '\t')}\n`;
|
|
201
172
|
}
|
|
202
173
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
.filter((source) => !own.has(source))
|
|
213
|
-
.map((source) => source.replace(/^res:\/\//, '')),
|
|
214
|
-
];
|
|
174
|
+
/**
|
|
175
|
+
* The `res://` path one declared entry names — the subpath's `godot` condition. A pack declares its
|
|
176
|
+
* entries once, in its `package.json`; the bake copies that declaration into the document it emits, so
|
|
177
|
+
* an entry resolves against the same statement the pack was built from. Internal: `resolveEntry` is
|
|
178
|
+
* how a client reaches it, and `packEntries` is the entry vocabulary's public face.
|
|
179
|
+
*/
|
|
180
|
+
function godotEntry(exports: Exports, entry: string): string | undefined {
|
|
181
|
+
const value = exports?.[`./${entry.replace(/^\.\//, '')}`];
|
|
182
|
+
return typeof value === 'object' ? value.godot : undefined;
|
|
215
183
|
}
|
|
216
184
|
|
|
217
|
-
/**
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
// The next *preset* header — not this one's `[preset.N.options]`.
|
|
223
|
-
const end = rest.search(/\n\[preset\.\d+\]/);
|
|
224
|
-
return (end < 0 ? rest : rest.slice(0, end)).trimEnd();
|
|
185
|
+
/** One declared entry, resolved: its name, the `res://` path it names, and that path's class. */
|
|
186
|
+
export interface PackEntry {
|
|
187
|
+
entry: string;
|
|
188
|
+
res: string;
|
|
189
|
+
cls: string;
|
|
225
190
|
}
|
|
226
191
|
|
|
227
|
-
/**
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
192
|
+
/**
|
|
193
|
+
* A pack's declared entries, each with the class its path loads as — the one rule a declared entry,
|
|
194
|
+
* the types emitted for it and the module the plugin serves all read (`FORMATS` by extension, so a
|
|
195
|
+
* `.tscn` is a `PackedScene` everywhere). Entries that name no asset are skipped: they exist to
|
|
196
|
+
* declare something else.
|
|
197
|
+
*/
|
|
198
|
+
export function packEntries(exports: Exports): PackEntry[] {
|
|
199
|
+
const entries: PackEntry[] = [];
|
|
200
|
+
for (const [subpath, declared] of Object.entries(exports ?? {})) {
|
|
201
|
+
const res = typeof declared === 'object' ? declared.godot : undefined;
|
|
202
|
+
const cls = res ? classOf(res) : undefined;
|
|
203
|
+
if (res && cls) entries.push({ entry: subpath.replace(/^\.\//, ''), res, cls });
|
|
204
|
+
}
|
|
205
|
+
return entries;
|
|
231
206
|
}
|
|
232
207
|
|
|
233
|
-
/**
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
208
|
+
/** An entry name as the identifier both the served module and its emitted types use (`my-level` → `my_level`). */
|
|
209
|
+
export const entryId = (entry: string) => entry.replace(/[^A-Za-z0-9_$]/g, '_');
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* A loose source's staging descriptor, read from its `.import` sidecar: the product it serves — the
|
|
213
|
+
* first `deps/dest_files` entry, or the single `[remap] path` an importer that writes only that serves
|
|
214
|
+
* it as — and the sidecar text itself. A client has no source tree to read either from, so the pack
|
|
215
|
+
* carries them. The bake and the preload plugin both walk a sidecar with this one rule, so a source
|
|
216
|
+
* can never stage in one place and not the other.
|
|
217
|
+
*/
|
|
218
|
+
export function descriptorOf(sidecar: string): { path: string; import: string } | null {
|
|
219
|
+
const dests = /^dest_files=\[([^\]]*)\]/m.exec(sidecar)?.[1];
|
|
220
|
+
const path = dests ? /"([^"]+)"/.exec(dests)?.[1] : /^path="([^"]+)"/m.exec(sidecar)?.[1];
|
|
221
|
+
return path ? { path, import: sidecar } : null;
|
|
240
222
|
}
|
|
241
223
|
|
|
242
|
-
/**
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
224
|
+
/**
|
|
225
|
+
* The two files one loose source stages: the sidecar the engine looks for, and the product path it
|
|
226
|
+
* remaps to. Stated once because the bake (which emits a module's source) and the runtime (which builds
|
|
227
|
+
* the files a loader stages) both derive them from the same descriptor, and a rule told twice is a rule
|
|
228
|
+
* that can be told differently.
|
|
229
|
+
*/
|
|
230
|
+
export function stagedKeys(loose: string, descriptor: { path: string }): { sidecar: string; product: string } {
|
|
231
|
+
return { sidecar: `${loose}.import`, product: descriptor.path };
|
|
246
232
|
}
|
|
247
233
|
|
|
248
|
-
/**
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
234
|
+
/**
|
|
235
|
+
* A file to stage before loading, keyed by `res://` path: a sidecar (`.import`) arrives as `content`
|
|
236
|
+
* (text), an imported product (`.ctex`/`.scn`) as `path` (fetched).
|
|
237
|
+
*/
|
|
238
|
+
export interface StagedFile {
|
|
239
|
+
content?: string;
|
|
240
|
+
path?: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The files one load stages, keyed by `res://` path. */
|
|
244
|
+
export type StagedFiles = Record<string, StagedFile>;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* A declared entry, resolved: the packs to mount, the `res://` path it names, and the loose files to
|
|
248
|
+
* stage before loading it. Nothing here is a URL — addressing is the caller's.
|
|
249
|
+
*/
|
|
250
|
+
export interface ResolvedEntry {
|
|
251
|
+
/** The packs to mount, in mount order; version-addressed, so each is immutable and cacheable. */
|
|
252
|
+
mounts: Array<{ name: string; version: string }>;
|
|
253
|
+
/** The `res://` path the entry names. */
|
|
254
|
+
res: string;
|
|
255
|
+
/** What to stage before loading `res`: one entry per loose file the entry needs. */
|
|
256
|
+
files: Array<{
|
|
257
|
+
/** The sidecar's key — where the engine looks for it (`<loose>.import`) — and its text. */
|
|
258
|
+
sidecar: string;
|
|
259
|
+
content: string;
|
|
260
|
+
/** The product's key: the `res://` path it is looked up by. */
|
|
261
|
+
product: string;
|
|
262
|
+
/** The pack the product's bytes come from — its name, or `null` for the app's own pack, which declares none. */
|
|
263
|
+
pack: string | null;
|
|
264
|
+
/** The product's file name inside that pack. */
|
|
265
|
+
file: string;
|
|
266
|
+
}>;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Resolves one declared entry to what must happen before it loads: the release's closure with the
|
|
271
|
+
* version of each pack, the `res://` path the entry names, and the loose files that path needs, each
|
|
272
|
+
* owned by whichever mounted pack carries its product — the descriptor is looked up across the stated
|
|
273
|
+
* documents, so a pack borrows another's textures without carrying them. `boot` is the app's own pack,
|
|
274
|
+
* which declares no name and is staged from like any other.
|
|
275
|
+
*/
|
|
276
|
+
export function resolveEntry(
|
|
277
|
+
release: Release,
|
|
278
|
+
docs: Record<string, PackDoc>,
|
|
279
|
+
pack: string,
|
|
280
|
+
entry: string,
|
|
281
|
+
boot?: PackDoc,
|
|
282
|
+
): ResolvedEntry {
|
|
283
|
+
const names = mountClosure(release, pack);
|
|
284
|
+
const own = docs[pack];
|
|
285
|
+
if (!own) throw new Error(`godot-assets: no document for ${pack}, which declares the entry.`);
|
|
286
|
+
|
|
287
|
+
// Ownership over the whole mounted set: the app's own pack last, so it wins where both own a file.
|
|
288
|
+
const owned = new Map<string, { path: string; import: string; pack: string | null; file: string }>();
|
|
289
|
+
const claim = (doc: PackDoc, owner: string | null) => {
|
|
290
|
+
for (const [loose, descriptor] of Object.entries(doc.stage)) {
|
|
291
|
+
owned.set(loose, { ...descriptor, pack: owner, file: fileOf(descriptor.path) });
|
|
292
|
+
}
|
|
259
293
|
};
|
|
294
|
+
for (const name of names) {
|
|
295
|
+
const doc = docs[name];
|
|
296
|
+
if (!doc) throw new Error(`godot-assets: no document for ${name}, which the release mounts.`);
|
|
297
|
+
claim(doc, name);
|
|
298
|
+
}
|
|
299
|
+
if (boot) claim(boot, null);
|
|
300
|
+
|
|
301
|
+
const res = godotEntry(own.exports, entry);
|
|
302
|
+
if (!res) throw new Error(`godot-assets: ${pack} declares no entry ${entry}.`);
|
|
303
|
+
const files = (own.needs[res] ?? []).flatMap((loose) => {
|
|
304
|
+
const descriptor = owned.get(loose);
|
|
305
|
+
// A loose file no mounted pack owns is skipped, not staged as a dangling reference.
|
|
306
|
+
if (!descriptor) return [];
|
|
307
|
+
const { sidecar, product } = stagedKeys(loose, descriptor);
|
|
308
|
+
return [{ sidecar, content: descriptor.import, product, pack: descriptor.pack, file: descriptor.file }];
|
|
309
|
+
});
|
|
310
|
+
return { mounts: names.map((name) => ({ name, version: release[name].version })), res, files };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The file name a product path ends in — what a pack stages it under in its {@link FILES_DIR}. */
|
|
314
|
+
function fileOf(path: string): string {
|
|
315
|
+
return path.split('/').pop()!;
|
|
260
316
|
}
|