@ringozz/godot 4.7.2-659 → 4.7.2-664
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 +25 -6
- package/package.json +5 -4
- package/src/assets.d.ts +10 -1
- package/src/bake.ts +469 -0
- package/src/dump-deps.gd +200 -53
- package/src/editor.ts +1 -1
- package/src/formats.ts +78 -0
- package/src/load.ts +153 -17
- package/src/pack.ts +280 -221
- package/src/preload.ts +165 -208
- package/src/serve.ts +67 -0
- package/src/planRefs.ts +0 -209
package/src/planRefs.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
/**********************************************************************
|
|
2
|
-
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
-
***********************************************************************/
|
|
4
|
-
|
|
5
|
-
// A level's refs, read from its **plan**. A `.gltfx` level scene carries the converter's derived
|
|
6
|
-
// render state under `extras.wartales.plan`: it names the level's terrain by *directory* and its
|
|
7
|
-
// ground/environment textures by resource route. A host's closure walk cannot see any of it — the
|
|
8
|
-
// routes are relative to the manifest that names them, a layered container's layers are plain text
|
|
9
|
-
// lines, and a runtime script reads the plan dynamically (so no scene names them literally any more).
|
|
10
|
-
// Reading it here keeps a `.gltfx` source's closure complete without a baked scene.
|
|
11
|
-
//
|
|
12
|
-
// Pure filesystem: a project directory plus the `res://` roots a host scans in, the refs out. No
|
|
13
|
-
// engine, no editor — so it is unit-testable and runs before the pack bake.
|
|
14
|
-
|
|
15
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
16
|
-
import { join } from 'node:path';
|
|
17
|
-
|
|
18
|
-
/** The refs a `.gltfx`'s plan implies, ready to merge into a dump. */
|
|
19
|
-
export interface PlanRefs {
|
|
20
|
-
/** The level source's own extra loose refs: its terrain data, layered-container manifests, and the
|
|
21
|
-
* ground/environment textures its plan names. */
|
|
22
|
-
loose: string[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Two of the refs a plan names are plain files rather than imported sources — the terrain's header
|
|
26
|
-
// JSONs, which a plan-built level reads at run time. They are in the level's `loose` list, so a
|
|
27
|
-
// *staging* host writes them; a *pack* needs no help either, because Godot loads a `.json` as a
|
|
28
|
-
// `JSON` resource (verified: `ResourceLoader.exists` is true and the class is `JSON`), so an
|
|
29
|
-
// `all_resources` preset packs it — and since no pack *owns* a source it is not excluded from any,
|
|
30
|
-
// every pack carries those few KB.
|
|
31
|
-
|
|
32
|
-
const isObject = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
33
|
-
|
|
34
|
-
/** The `extras.wartales.plan` of a parsed gltfx document, or null for a foreign/older document. */
|
|
35
|
-
export function planOf(doc: unknown): Record<string, unknown> | null {
|
|
36
|
-
if (!isObject(doc) || !isObject(doc.extras)) return null;
|
|
37
|
-
const w = (doc.extras as Record<string, unknown>)['wartales'];
|
|
38
|
-
if (!isObject(w) || !isObject(w.plan)) return null;
|
|
39
|
-
return w.plan as Record<string, unknown>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Every `src/<dir>`-style root as an absolute disk directory, paired with its `res://` prefix. */
|
|
43
|
-
function rootDirs(projectDir: string, roots: string[]): { disk: string; res: string }[] {
|
|
44
|
-
const out: { disk: string; res: string }[] = [];
|
|
45
|
-
for (const root of roots) {
|
|
46
|
-
const rel = root.replace(/^res:\/\//, '');
|
|
47
|
-
out.push({ disk: join(projectDir, ...rel.split('/')), res: `res://${rel}` });
|
|
48
|
-
}
|
|
49
|
-
return out;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** A path's last segment. */
|
|
53
|
-
const base = (p: string) => p.slice(p.lastIndexOf('/') + 1);
|
|
54
|
-
|
|
55
|
-
/** Every file under `dir`, as paths relative to `dir` (posix, one level of nesting per entry). */
|
|
56
|
-
function walk(dir: string, prefix = ''): string[] {
|
|
57
|
-
const out: string[] = [];
|
|
58
|
-
let entries: string[];
|
|
59
|
-
try {
|
|
60
|
-
entries = readdirSync(dir);
|
|
61
|
-
} catch {
|
|
62
|
-
return out;
|
|
63
|
-
}
|
|
64
|
-
for (const name of entries) {
|
|
65
|
-
if (name.startsWith('.')) continue;
|
|
66
|
-
const full = join(dir, name);
|
|
67
|
-
const rel = prefix ? `${prefix}/${name}` : name;
|
|
68
|
-
try {
|
|
69
|
-
if (readdirSync(full).length >= 0) {
|
|
70
|
-
out.push(...walk(full, rel));
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
} catch {
|
|
74
|
-
// not a directory
|
|
75
|
-
}
|
|
76
|
-
out.push(rel);
|
|
77
|
-
}
|
|
78
|
-
return out;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* The environment a **host renders**: the level's authored stage — `stages[0].env`, which the plan
|
|
83
|
-
* also carries as `lighting.env`. A host shows one time of day, so the other stages (a viewer's
|
|
84
|
-
* blend material) and the reflection probe (which a host does not sample) are not runtime refs.
|
|
85
|
-
*/
|
|
86
|
-
function authoredEnvMap(lighting: Record<string, unknown>): string | null {
|
|
87
|
-
const stages = Array.isArray(lighting.stages) ? lighting.stages : [];
|
|
88
|
-
const stage0 = stages.length && isObject(stages[0]) ? (stages[0] as Record<string, unknown>) : null;
|
|
89
|
-
const env = (stage0 && isObject(stage0.env) ? stage0.env : null) ?? (isObject(lighting.env) ? lighting.env : null);
|
|
90
|
-
return env && typeof env.map === 'string' ? env.map : null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The layer file names the `.webparray` manifests in `dir` name, one per line. Those layers are read
|
|
95
|
-
* by their *importer* and live inside the imported product afterwards, so nothing loads them at run
|
|
96
|
-
* time: they are import-time inputs, and the directory walk must not stage them.
|
|
97
|
-
*/
|
|
98
|
-
function layerSources(dir: string): Set<string> {
|
|
99
|
-
const names = new Set<string>();
|
|
100
|
-
for (const manifest of walk(dir)) {
|
|
101
|
-
if (!manifest.toLowerCase().endsWith(".webparray")) continue;
|
|
102
|
-
let text: string;
|
|
103
|
-
try {
|
|
104
|
-
text = readFileSync(join(dir, ...manifest.split("/")), "utf8");
|
|
105
|
-
} catch {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
for (const line of text.split("\n")) {
|
|
109
|
-
const ref = line.trim();
|
|
110
|
-
if (ref && !ref.startsWith("#")) names.add(base(ref));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return names;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** The resource route of a ref the plan names, probing the served `.webp` suffix a texture carries. */
|
|
117
|
-
function routeOf(root: { disk: string; res: string }, route: string): string | null {
|
|
118
|
-
for (const candidate of [route, `${route}.webp`]) {
|
|
119
|
-
if (existsSync(join(root.disk, ...candidate.split('/')))) return `${root.res}/${candidate}`;
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Every `.gltfx` under `roots` with its plan-derived refs. A level's terrain is discovered by
|
|
126
|
-
* *listing* its directory (the tree holds exactly the level's atlas there, so no name list to keep in
|
|
127
|
-
* step), its manifests' layers are read from the manifests, and its ground/environment textures are
|
|
128
|
-
* probed per route (a plan route may or may not carry the served `.webp` suffix).
|
|
129
|
-
*/
|
|
130
|
-
export function planRefs(projectDir: string, roots: string[]): Map<string, PlanRefs> {
|
|
131
|
-
const out = new Map<string, PlanRefs>();
|
|
132
|
-
for (const root of rootDirs(projectDir, roots)) {
|
|
133
|
-
for (const file of walk(root.disk)) {
|
|
134
|
-
if (!file.toLowerCase().endsWith('.gltfx')) continue;
|
|
135
|
-
let doc: unknown;
|
|
136
|
-
try {
|
|
137
|
-
doc = JSON.parse(readFileSync(join(root.disk, ...file.split('/')), 'utf8'));
|
|
138
|
-
} catch {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
const plan = planOf(doc);
|
|
142
|
-
if (!plan) continue;
|
|
143
|
-
const levelRoute = `${root.res}/${file}`;
|
|
144
|
-
const loose = new Set<string>();
|
|
145
|
-
|
|
146
|
-
// The terrain: the plan names its directory, and the tree holds exactly the level's own atlas
|
|
147
|
-
// there (the height and tile-normal maps, the composed weight planes, the passmap and its
|
|
148
|
-
// meta, the header JSON and the layered containers) — so the directory *is* the list. A
|
|
149
|
-
// container's *layers* are deliberately not listed: they live inside its imported product,
|
|
150
|
-
// and are import-time inputs, so nothing stages them at run time.
|
|
151
|
-
const terrain = isObject(plan.terrain) ? plan.terrain : null;
|
|
152
|
-
if (terrain && typeof terrain.dir === 'string') {
|
|
153
|
-
const dir = terrain.dir;
|
|
154
|
-
const abs = join(root.disk, ...dir.split('/'));
|
|
155
|
-
const layers = layerSources(abs);
|
|
156
|
-
for (const name of walk(abs)) {
|
|
157
|
-
if (name.endsWith(".import") || layers.has(base(name))) continue;
|
|
158
|
-
loose.add(`${root.res}/${dir}/${name}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// The ground's patches and the rig's skies: plain resource routes with the served suffix.
|
|
163
|
-
const ground = Array.isArray(plan.ground) ? plan.ground : [];
|
|
164
|
-
for (const g of ground) {
|
|
165
|
-
if (!isObject(g)) continue;
|
|
166
|
-
const fields = g.source === 'overlay' ? [isObject(g.material) ? g.material : {}] : [isObject(g.water) ? g.water : {}];
|
|
167
|
-
for (const f of fields) {
|
|
168
|
-
for (const key of ['diffuse', 'normal', 'orm']) {
|
|
169
|
-
if (typeof f[key] !== 'string') continue;
|
|
170
|
-
const route = routeOf(root, f[key] as string);
|
|
171
|
-
if (route) loose.add(route);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const lighting = isObject(plan.lighting) ? plan.lighting : {};
|
|
176
|
-
const map = authoredEnvMap(lighting);
|
|
177
|
-
const route = map ? routeOf(root, map) : null;
|
|
178
|
-
if (route) loose.add(route);
|
|
179
|
-
|
|
180
|
-
out.set(levelRoute, { loose: [...loose].sort() });
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return out;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Merge `planRefs` into a dump's `graph`/`loose` halves: a `.gltfx` source gains its plan's refs, and
|
|
188
|
-
* so does every source that reaches it (a scene that instances it, so mounting the scene stages the
|
|
189
|
-
* level's terrain and ground too — a union, and a no-op for a project with no plan).
|
|
190
|
-
*/
|
|
191
|
-
export function mergePlanRefs(
|
|
192
|
-
graph: Record<string, string[]>,
|
|
193
|
-
deps: Record<string, string[]>,
|
|
194
|
-
refs: Map<string, PlanRefs>,
|
|
195
|
-
): void {
|
|
196
|
-
for (const [level, r] of refs) {
|
|
197
|
-
if (!(level in graph)) continue;
|
|
198
|
-
deps[level] = [...new Set([...(deps[level] ?? []), ...r.loose])].sort();
|
|
199
|
-
}
|
|
200
|
-
// A scene that instances a `.gltfx` carries that plan's refs too: mounting the scene must stage the
|
|
201
|
-
// level's terrain and ground, not only its model composition. (The scene may already name some of
|
|
202
|
-
// them — a baked one does — so this is a union, and a no-op for a project with no plan.)
|
|
203
|
-
for (const source of Object.keys(graph)) {
|
|
204
|
-
const plans = graph[source].filter((dep) => refs.has(dep));
|
|
205
|
-
if (!plans.length || refs.has(source)) continue;
|
|
206
|
-
const extra = plans.flatMap((p) => refs.get(p)!.loose);
|
|
207
|
-
deps[source] = [...new Set([...(deps[source] ?? []), ...extra])].sort();
|
|
208
|
-
}
|
|
209
|
-
}
|