@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/dump-deps.gd
CHANGED
|
@@ -1,38 +1,52 @@
|
|
|
1
1
|
@tool
|
|
2
2
|
extends SceneTree
|
|
3
|
-
## Dumps, for every packable (`.import`-less) source under the given roots, two
|
|
4
|
-
##
|
|
5
|
-
##
|
|
6
|
-
##
|
|
7
|
-
##
|
|
8
|
-
##
|
|
9
|
-
## `splitPacks` uses to decide which pack owns a source (the ones only one scene reaches
|
|
10
|
-
## are that scene's).
|
|
3
|
+
## Dumps, for every packable (`.import`-less) source under the given roots, two halves. The **loose**
|
|
4
|
+
## (WebP-backed) texture sources it needs: those must stay out of the pack and be staged/transcoded by
|
|
5
|
+
## the host, and the preload plugin (`@ringozz/godot/preload`) reads them from the manifest beside the
|
|
6
|
+
## pack, so it never scans source text for references. And the **external** refs the walk reached
|
|
7
|
+
## outside the roots: a pack may reference the boot sources and its declared dependencies, never a
|
|
8
|
+
## sibling, so the bake checks those instead of trusting the tree.
|
|
11
9
|
##
|
|
12
10
|
## Sources of both (union): the engine's `ResourceLoader.get_dependencies` (for imported
|
|
13
|
-
## sources the real refs live on the imported product
|
|
14
|
-
## `dest_files`
|
|
15
|
-
##
|
|
16
|
-
##
|
|
17
|
-
##
|
|
11
|
+
## sources the real refs live on the imported *product*, reached through the `.import` sidecar —
|
|
12
|
+
## `deps/dest_files`, or the `[remap] path` an importer that writes only that serves the source as,
|
|
13
|
+
## exactly as `@ringozz/godot/preload` reads it), plus a text scan for string-valued paths
|
|
14
|
+
## (`albedo_array = "res://.../*.webparray"`, `load("res://...")`) that no static resource edge
|
|
15
|
+
## exposes — the engine reports *no* dependency for a script at all (`GDScriptParser::get_dependencies`
|
|
16
|
+
## is a stub), so a `.gd`'s literal loads are only ever found here. A dep is *loose* when it has an
|
|
17
|
+
## `.import` sidecar but is not a packed resource type — the set is derived, not listed.
|
|
18
18
|
##
|
|
19
|
-
## A third source, because neither above can see it: a **
|
|
19
|
+
## A third source, because neither above can see it: a **scene built from its plan**
|
|
20
20
|
## (`extras.gltfx.plan`, which a host script reads at run time). It names its terrain by
|
|
21
21
|
## *directory* and its ground/environment maps by relative route — no resource edge and no `res://`
|
|
22
22
|
## literal — so `_plan_refs` reads the plan off the `.gltfx` and classifies its refs. A scene that
|
|
23
|
-
## instances the `.gltfx` inherits them through `_collect`'s recursion.
|
|
23
|
+
## instances the `.gltfx` inherits them through `_collect`'s recursion. It reads the plan's own role
|
|
24
|
+
## names (`terrain.dir`, a patch's `material.{diffuse,normal,orm}` / `water.normal`, the authored
|
|
25
|
+
## stage's `env.map`), which no engine check can validate — so a plan that resolves *nothing*, or one
|
|
26
|
+
## stamped under another `extras` namespace, is warned about rather than silently staging nothing.
|
|
24
27
|
##
|
|
25
|
-
## Driven by `@ringozz/godot/
|
|
26
|
-
## GODOT_DUMP_OUT
|
|
27
|
-
##
|
|
28
|
-
## GODOT_DUMP_ROOTS
|
|
28
|
+
## Driven by `@ringozz/godot/bake`'s `buildAssetPacks`, which sets:
|
|
29
|
+
## GODOT_DUMP_OUT – output res:// path (default res://.godot/pack-dump.json) — a build
|
|
30
|
+
## input: the bake merges it into the document beside the packs.
|
|
31
|
+
## GODOT_DUMP_ROOTS – comma-separated res:// roots to scan (default res://)
|
|
32
|
+
## GODOT_DUMP_PACKED – comma-separated extensions whose *product* the pack carries: such a source is
|
|
33
|
+
## walked (an imported source's own deps are empty — its product carries the
|
|
34
|
+
## refs) where any other extension with a sidecar is a loose texture to stage
|
|
35
|
+
## GODOT_DUMP_SCANNED – comma-separated extensions whose text names `res://` paths the engine cannot
|
|
36
|
+
## report, so it is scanned for literals
|
|
37
|
+
##
|
|
38
|
+
## The two extension lists are required rather than defaulted: they are the format table's
|
|
39
|
+
## (`formats.ts`), and an empty one would classify every texture as packed — staging the wrong set
|
|
40
|
+
## instead of failing.
|
|
41
|
+
|
|
42
|
+
var _packed := PackedStringArray()
|
|
43
|
+
var _scanned := PackedStringArray()
|
|
29
44
|
|
|
30
|
-
|
|
31
|
-
##
|
|
32
|
-
##
|
|
33
|
-
##
|
|
34
|
-
|
|
35
|
-
const SCANNED := ["tscn", "tres", "gd", "gdshader", "gdshaderinc", "gltf", "obj"]
|
|
45
|
+
|
|
46
|
+
## A plan's routes are relative to the converter's output root (`--out`), *not* to the scan root — a
|
|
47
|
+
## pack is scanned on its own, so the two differ. `GODOT_DUMP_PLAN_BASE` names that root; the first
|
|
48
|
+
## scan root stands in when unset (a project whose content prefix is where it bakes).
|
|
49
|
+
var _plan_base := ""
|
|
36
50
|
|
|
37
51
|
|
|
38
52
|
func _initialize() -> void:
|
|
@@ -42,6 +56,16 @@ func _initialize() -> void:
|
|
|
42
56
|
var roots := OS.get_environment("GODOT_DUMP_ROOTS").split(",", false)
|
|
43
57
|
if roots.is_empty():
|
|
44
58
|
roots = ["res://"]
|
|
59
|
+
_packed = _extensions("GODOT_DUMP_PACKED")
|
|
60
|
+
_scanned = _extensions("GODOT_DUMP_SCANNED")
|
|
61
|
+
if _packed.is_empty() or _scanned.is_empty():
|
|
62
|
+
push_error("[dump-deps] GODOT_DUMP_PACKED and GODOT_DUMP_SCANNED are required — the dump is driven by `godot pack`.")
|
|
63
|
+
quit(1)
|
|
64
|
+
return
|
|
65
|
+
_plan_base = OS.get_environment("GODOT_DUMP_PLAN_BASE")
|
|
66
|
+
if _plan_base.is_empty():
|
|
67
|
+
_plan_base = String(roots[0])
|
|
68
|
+
_plan_base = _plan_base.rstrip("/") + "/"
|
|
45
69
|
var sources := {}
|
|
46
70
|
for r in roots:
|
|
47
71
|
var root := String(r).rstrip("/") + "/"
|
|
@@ -54,6 +78,11 @@ func _initialize() -> void:
|
|
|
54
78
|
quit()
|
|
55
79
|
|
|
56
80
|
|
|
81
|
+
## A comma-separated `GODOT_DUMP_*` extension list.
|
|
82
|
+
func _extensions(name: String) -> PackedStringArray:
|
|
83
|
+
return PackedStringArray(OS.get_environment(name).split(",", false))
|
|
84
|
+
|
|
85
|
+
|
|
57
86
|
## `root` is the scan root `path` lives under (a plan's refs are relative to it), carried down the walk.
|
|
58
87
|
func _scan(root: String, path: String, sources: Dictionary) -> void:
|
|
59
88
|
var d := DirAccess.open(path)
|
|
@@ -65,34 +94,40 @@ func _scan(root: String, path: String, sources: Dictionary) -> void:
|
|
|
65
94
|
_scan(root, path.path_join(dir) + "/", sources)
|
|
66
95
|
for file in d.get_files():
|
|
67
96
|
var p := path.path_join(file)
|
|
68
|
-
if p.get_extension().to_lower() in
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
sources[p] = {"packed": packed, "loose": loose}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
## The loose texture sources reachable from `path` (transitive), sorted. `seen` collects every
|
|
80
|
-
## packed source the walk touches — the graph — including `path` itself.
|
|
81
|
-
func _loose(root: String, path: String, seen: Dictionary) -> Array:
|
|
97
|
+
if p.get_extension().to_lower() in _packed:
|
|
98
|
+
sources[p] = _loose(root, p, {})
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
## The loose texture sources reachable from `path` (transitive), sorted, and the refs the walk reached
|
|
102
|
+
## outside the scan root. `seen` is the walk's visited set, so a cycle (or a diamond) costs one visit;
|
|
103
|
+
## the engine's own products (`res://.godot/…`) are carried by their source, so they are not refs.
|
|
104
|
+
func _loose(root: String, path: String, seen: Dictionary) -> Dictionary:
|
|
82
105
|
var loose := {}
|
|
83
106
|
_collect(root, path, loose, seen)
|
|
84
107
|
var arr := loose.keys()
|
|
85
108
|
arr.sort()
|
|
86
|
-
|
|
109
|
+
var external := []
|
|
110
|
+
for s in seen:
|
|
111
|
+
var ref := String(s)
|
|
112
|
+
if not ref.begins_with(root) and not ref.begins_with("res://.godot/"):
|
|
113
|
+
external.append(ref)
|
|
114
|
+
external.sort()
|
|
115
|
+
return {"loose": arr, "external": external}
|
|
87
116
|
|
|
88
117
|
|
|
89
|
-
|
|
118
|
+
## Classifies one dep: a packed type is walked, anything else with an `.import` sidecar is a loose
|
|
119
|
+
## texture to stage. Returns whether the dep named anything real — a plan's routes are probed in
|
|
120
|
+
## several spellings, so that is what tells `_plan_refs` its shape still resolves.
|
|
121
|
+
func _classify(root: String, dep: String, loose: Dictionary, seen: Dictionary) -> bool:
|
|
90
122
|
if dep.is_empty():
|
|
91
|
-
return
|
|
92
|
-
if dep.get_extension().to_lower() in
|
|
123
|
+
return false
|
|
124
|
+
if dep.get_extension().to_lower() in _packed:
|
|
93
125
|
_collect(root, dep, loose, seen)
|
|
94
|
-
|
|
126
|
+
return true
|
|
127
|
+
if FileAccess.file_exists(dep + ".import"):
|
|
95
128
|
loose[dep] = true
|
|
129
|
+
return true
|
|
130
|
+
return false
|
|
96
131
|
|
|
97
132
|
|
|
98
133
|
func _collect(root: String, path: String, loose: Dictionary, seen: Dictionary) -> void:
|
|
@@ -105,18 +140,29 @@ func _collect(root: String, path: String, loose: Dictionary, seen: Dictionary) -
|
|
|
105
140
|
if FileAccess.file_exists(imp):
|
|
106
141
|
var cfg := ConfigFile.new()
|
|
107
142
|
if cfg.load(imp) == OK:
|
|
108
|
-
for dst in cfg
|
|
143
|
+
for dst in _dest_files(cfg):
|
|
109
144
|
_collect(root, String(dst), loose, seen)
|
|
110
145
|
for dep in ResourceLoader.get_dependencies(path):
|
|
111
146
|
_classify(root, _res_path(String(dep)), loose, seen)
|
|
112
147
|
# String-valued resource paths are invisible to dependency analysis; scan the
|
|
113
148
|
# text for loose refs the engine cannot report.
|
|
114
|
-
if path.get_extension().to_lower() in
|
|
149
|
+
if path.get_extension().to_lower() in _scanned:
|
|
115
150
|
for p in _text_refs(path):
|
|
116
151
|
_classify(root, p, loose, seen)
|
|
117
|
-
# A
|
|
152
|
+
# A scene built from its plan names nothing above: its refs come from the plan itself.
|
|
118
153
|
if path.get_extension().to_lower() == "gltfx":
|
|
119
|
-
_plan_refs(
|
|
154
|
+
_plan_refs(path, loose, seen)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
## An imported source's product(s), read exactly as `@ringozz/godot/preload` reads them: every
|
|
158
|
+
## `deps/dest_files` entry (the real refs live on the product), or — for an importer that writes only
|
|
159
|
+
## that — the single `[remap] path` the source is served as.
|
|
160
|
+
func _dest_files(cfg: ConfigFile) -> Array:
|
|
161
|
+
var dests: Variant = cfg.get_value("deps", "dest_files", [])
|
|
162
|
+
if dests is Array and not dests.is_empty():
|
|
163
|
+
return dests
|
|
164
|
+
var served: Variant = cfg.get_value("remap", "path", "")
|
|
165
|
+
return [served] if served is String and not served.is_empty() else []
|
|
120
166
|
|
|
121
167
|
|
|
122
168
|
## `res://` path from a dependency entry (`uid://…::::res://…` or plain).
|
|
@@ -137,23 +183,39 @@ func _text_refs(path: String) -> Array:
|
|
|
137
183
|
return refs
|
|
138
184
|
|
|
139
185
|
|
|
140
|
-
## A
|
|
141
|
-
##
|
|
142
|
-
##
|
|
143
|
-
##
|
|
144
|
-
##
|
|
145
|
-
|
|
186
|
+
## A plan's refs, read from the `.gltfx` that carries it (`extras.gltfx.plan`). They are invisible to
|
|
187
|
+
## every source above (a *directory* rather than a route, relative routes, and a host script that
|
|
188
|
+
## reads the JSON instead of a resource edge), so the plan is their only statement. Each ref goes
|
|
189
|
+
## through `_classify`: what has an `.import` sidecar is staged, the header JSONs (which the packs
|
|
190
|
+
## carry) are not.
|
|
191
|
+
##
|
|
192
|
+
## The role names are the plan's own (`core/ground.ts` + `core/heapsMaterial.ts`): an overlay patch's
|
|
193
|
+
## `material.{diffuse,normal,orm}`, a lake/ocean's `water.normal`, and the authored stage's `env.map`,
|
|
194
|
+
## every one served as `<route>.webp` (the host's `_texture_route`). Nothing static can check that
|
|
195
|
+
## reading, so a plan that resolves no ref at all is warned about: that is a schema change, and it
|
|
196
|
+
## would otherwise show up as a scene with no terrain on a host that stages.
|
|
197
|
+
func _plan_refs(path: String, loose: Dictionary, seen: Dictionary) -> void:
|
|
146
198
|
var doc = JSON.parse_string(FileAccess.get_file_as_string(path))
|
|
147
199
|
var extras: Variant = doc.get("extras") if doc is Dictionary else null
|
|
148
|
-
|
|
200
|
+
if not extras is Dictionary:
|
|
201
|
+
return
|
|
202
|
+
var stamp: Variant = extras.get("gltfx")
|
|
149
203
|
var plan: Variant = stamp.get("plan") if stamp is Dictionary else null
|
|
150
204
|
if not plan is Dictionary:
|
|
205
|
+
# A `.gltfx` with no plan is normal (a plain composition). A plan under *another* namespace
|
|
206
|
+
# is not: nothing here would stage, and the host would build a scene whose textures were
|
|
207
|
+
# never shipped — so name the namespace that was stamped instead.
|
|
208
|
+
for ns in extras:
|
|
209
|
+
var other: Variant = extras[ns]
|
|
210
|
+
if ns != "gltfx" and other is Dictionary and other.get("plan") is Dictionary:
|
|
211
|
+
push_warning("[dump-deps] %s stamps a plan under extras.%s, not extras.gltfx — its refs will not be staged." % [path, ns])
|
|
151
212
|
return
|
|
152
213
|
|
|
214
|
+
var refs := 0
|
|
153
215
|
# The terrain: the plan names its directory, and the directory is the atlas list.
|
|
154
216
|
var terrain: Variant = plan.get("terrain")
|
|
155
217
|
if terrain is Dictionary and terrain.get("dir") is String:
|
|
156
|
-
_terrain_refs(
|
|
218
|
+
refs += _terrain_refs(_plan_base, _plan_base.path_join(String(terrain["dir"])), loose, seen)
|
|
157
219
|
|
|
158
220
|
# The rest are texture routes — the ground's patches / the water's maps, and the sky.
|
|
159
221
|
var ground: Variant = plan.get("ground")
|
|
@@ -164,7 +226,8 @@ func _plan_refs(root: String, path: String, loose: Dictionary, seen: Dictionary)
|
|
|
164
226
|
var fields: Variant = g.get("material") if g.get("source") == "overlay" else g.get("water")
|
|
165
227
|
if fields is Dictionary:
|
|
166
228
|
for key in ["diffuse", "normal", "orm"]:
|
|
167
|
-
_classify_route(
|
|
229
|
+
if _classify_route(_plan_base, fields.get(key), loose, seen):
|
|
230
|
+
refs += 1
|
|
168
231
|
var lighting: Variant = plan.get("lighting")
|
|
169
232
|
if lighting is Dictionary:
|
|
170
233
|
# A host renders one time of day: the authored stage's env, which the plan mirrors as `env`.
|
|
@@ -172,13 +235,17 @@ func _plan_refs(root: String, path: String, loose: Dictionary, seen: Dictionary)
|
|
|
172
235
|
var env: Variant = stages[0].get("env") if stages is Array and stages.size() > 0 and stages[0] is Dictionary else null
|
|
173
236
|
if not env is Dictionary:
|
|
174
237
|
env = lighting.get("env")
|
|
175
|
-
_classify_route(
|
|
238
|
+
if _classify_route(_plan_base, env.get("map") if env is Dictionary else null, loose, seen):
|
|
239
|
+
refs += 1
|
|
240
|
+
|
|
241
|
+
if refs == 0:
|
|
242
|
+
push_warning("[dump-deps] %s carries a plan, but no ref in it resolved — its shape (terrain.dir / a patch's material / a stage's env) may have changed." % path)
|
|
176
243
|
|
|
177
244
|
|
|
178
245
|
## A terrain directory **is** its atlas list (every atlas name is top-level in it), so each file is a
|
|
179
246
|
## ref — except a layered container's *layers*: the manifest names them and their bytes live in its
|
|
180
247
|
## imported product, so staging them would ship megabytes nothing reads.
|
|
181
|
-
func _terrain_refs(root: String, dir: String, loose: Dictionary, seen: Dictionary) ->
|
|
248
|
+
func _terrain_refs(root: String, dir: String, loose: Dictionary, seen: Dictionary) -> int:
|
|
182
249
|
var files := DirAccess.get_files_at(dir)
|
|
183
250
|
var layers := {}
|
|
184
251
|
for name in files:
|
|
@@ -187,13 +254,20 @@ func _terrain_refs(root: String, dir: String, loose: Dictionary, seen: Dictionar
|
|
|
187
254
|
var ref := line.strip_edges()
|
|
188
255
|
if not ref.is_empty() and not ref.begins_with("#"):
|
|
189
256
|
layers[ref.get_file()] = true
|
|
257
|
+
var refs := 0
|
|
190
258
|
for name in files:
|
|
191
|
-
if not layers.has(name.get_file()):
|
|
192
|
-
|
|
259
|
+
if not layers.has(name.get_file()) and _classify(root, dir.path_join(name), loose, seen):
|
|
260
|
+
refs += 1
|
|
261
|
+
return refs
|
|
193
262
|
|
|
194
263
|
|
|
195
|
-
## Classifies a plan's route: relative and unsuffixed, so the served `.webp` spelling is tried too
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
264
|
+
## Classifies a plan's route: relative and unsuffixed, so the served `.webp` spelling is tried too
|
|
265
|
+
## (the host's `_texture_route`). Returns whether it resolved.
|
|
266
|
+
func _classify_route(root: String, route: Variant, loose: Dictionary, seen: Dictionary) -> bool:
|
|
267
|
+
if not route is String:
|
|
268
|
+
return false
|
|
269
|
+
var found := false
|
|
270
|
+
for suffix in ["", ".webp"]:
|
|
271
|
+
if _classify(root, root.path_join(route + suffix), loose, seen):
|
|
272
|
+
found = true
|
|
273
|
+
return found
|
package/src/editor.ts
CHANGED
|
@@ -107,10 +107,15 @@ export async function resolveEditor(): Promise<string> {
|
|
|
107
107
|
if (import.meta.main) {
|
|
108
108
|
const command = process.argv[2];
|
|
109
109
|
if (command === 'pack') {
|
|
110
|
-
const { buildAssetPacks, parsePackArgs } = await import('./
|
|
110
|
+
const { buildAssetPacks, parsePackArgs } = await import('./bake.ts');
|
|
111
111
|
await buildAssetPacks(parsePackArgs(process.argv.slice(3)));
|
|
112
112
|
process.exit(0);
|
|
113
113
|
}
|
|
114
|
+
if (command === 'clean') {
|
|
115
|
+
const { cleanProject } = await import('./bake.ts');
|
|
116
|
+
cleanProject();
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
114
119
|
const exe = await resolveEditor();
|
|
115
120
|
const result = spawnSync(exe, process.argv.slice(2), { stdio: 'inherit', env: { ...process.env, GODOT_NODE_WRAPPER: '1' } });
|
|
116
121
|
process.exit(result.status ?? 1);
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
; (see `dump-deps.gd`), not read from the pack. `export_filter="all_resources"`
|
|
8
8
|
; ignores `include_filter` — scope with `exclude_filter` only.
|
|
9
9
|
;
|
|
10
|
-
; The bake
|
|
11
|
-
;
|
|
12
|
-
;
|
|
10
|
+
; The bake rewrites this preset's `exclude_filter` once per pack (the boot pack, then one per
|
|
11
|
+
; content pack) and puts the file back when it finishes, so the export carries exactly one scope.
|
|
12
|
+
; Don't write those scopes by hand.
|
|
13
13
|
|
|
14
14
|
[preset.0]
|
|
15
15
|
|
package/src/formats.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**********************************************************************
|
|
2
|
+
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
+
***********************************************************************/
|
|
4
|
+
|
|
5
|
+
// One row per importable source extension, holding everything its four readers need — so adding an
|
|
6
|
+
// extension is one edit here rather than an edit per list, and the lists cannot disagree:
|
|
7
|
+
//
|
|
8
|
+
// - `cls` — the class the module the preload plugin serves loads it as, and the class the ambient
|
|
9
|
+
// types for that import resolve to (`assets.d.ts`, the one hand-kept projection, which
|
|
10
|
+
// `dev/tests/formats.test.ts` compares against these rows). A row with no class is a source nothing
|
|
11
|
+
// imports: `.gltfxmesh`, which a `.gltfx`'s product references.
|
|
12
|
+
// - `packed` — its *product* travels in the `.pck`, so the dump recurses through it (an imported
|
|
13
|
+
// source's own `get_dependencies` is empty — the product carries the refs) instead of staging it
|
|
14
|
+
// loose. That is why the texture formats are rows here and *not* `packed`: the wasm build decodes no
|
|
15
|
+
// WebP, so their bytes must stay out of the pack and be staged and transcoded per file.
|
|
16
|
+
// - `scanned` — its *text* names `res://` paths no resource edge exposes (the engine reports no
|
|
17
|
+
// dependencies for a script at all), so the dump scans its text for literals.
|
|
18
|
+
//
|
|
19
|
+
// `moduleExtensions` / `packedExtensions` / `scannedExtensions` are those rows projected, which is what
|
|
20
|
+
// the plugin's filter and the dump's two environment lists are built from.
|
|
21
|
+
|
|
22
|
+
/** One source extension, and what that says about it. */
|
|
23
|
+
export interface Format {
|
|
24
|
+
/** The class its module loads as; absent when nothing imports one. */
|
|
25
|
+
cls?: string;
|
|
26
|
+
/** Its product travels in the `.pck`: walk it, rather than staging it loose. */
|
|
27
|
+
packed?: boolean;
|
|
28
|
+
/** Its text names `res://` paths worth a literal scan. */
|
|
29
|
+
scanned?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Imported source extension → what it is, and how it is carried. */
|
|
33
|
+
export const FORMATS: Record<string, Format> = {
|
|
34
|
+
gltf: { cls: 'PackedScene', packed: true, scanned: true },
|
|
35
|
+
// A scene-composition document (a glTF External Reference): an imported source whose product is a
|
|
36
|
+
// `PackedScene`, like a `.gltf`'s — and whose product is where its meshes/textures are referenced.
|
|
37
|
+
gltfx: { cls: 'PackedScene', packed: true },
|
|
38
|
+
// A shared-mesh document: what one or more `.gltfx` compositions reference. Nothing imports one —
|
|
39
|
+
// a `.gltfx`'s product is what names it — so it has no class, and is carried by a pack all the same.
|
|
40
|
+
gltfxmesh: { packed: true },
|
|
41
|
+
tscn: { cls: 'PackedScene', packed: true, scanned: true },
|
|
42
|
+
obj: { cls: 'PackedScene', packed: true, scanned: true },
|
|
43
|
+
gd: { cls: 'GDScript', packed: true, scanned: true },
|
|
44
|
+
gdshader: { cls: 'Shader', packed: true, scanned: true },
|
|
45
|
+
gdshaderinc: { cls: 'ShaderInclude', packed: true, scanned: true },
|
|
46
|
+
tres: { cls: 'Resource', packed: true, scanned: true },
|
|
47
|
+
po: { cls: 'Translation', packed: true },
|
|
48
|
+
ttf: { cls: 'FontFile', packed: true },
|
|
49
|
+
otf: { cls: 'FontFile', packed: true },
|
|
50
|
+
jpg: { cls: 'CompressedTexture2D' },
|
|
51
|
+
jpeg: { cls: 'CompressedTexture2D' },
|
|
52
|
+
png: { cls: 'CompressedTexture2D' },
|
|
53
|
+
webp: { cls: 'CompressedTexture2D' },
|
|
54
|
+
svg: { cls: 'CompressedTexture2D' },
|
|
55
|
+
// The layer manifest `godot/addons/sourcewebp` imports into a `.ctexarray` (a texture array whose
|
|
56
|
+
// layers are source WebP bytes). It has no runtime deps of its own; its product is what is staged.
|
|
57
|
+
webparray: { cls: 'CompressedTexture2DArray' },
|
|
58
|
+
exr: { cls: 'TextureLayered' },
|
|
59
|
+
hdr: { cls: 'TextureLayered' },
|
|
60
|
+
wav: { cls: 'AudioStreamWAV' },
|
|
61
|
+
ogg: { cls: 'AudioStreamOggVorbis' },
|
|
62
|
+
mp3: { cls: 'AudioStreamMP3' },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** A path's extension, lowercased (`res://…/Map.TSCN` -> `tscn`). */
|
|
66
|
+
const extOf = (path: string) => path.slice(path.lastIndexOf('.') + 1).toLowerCase();
|
|
67
|
+
|
|
68
|
+
/** The class an asset path loads as, or `undefined` when it is not an importable asset. */
|
|
69
|
+
export const classOf = (path: string) => FORMATS[extOf(path)]?.cls;
|
|
70
|
+
|
|
71
|
+
/** The extensions the preload plugin serves a module for: the rows with a class. */
|
|
72
|
+
export const moduleExtensions = Object.keys(FORMATS).filter((ext) => FORMATS[ext].cls);
|
|
73
|
+
|
|
74
|
+
/** The extensions whose product the `.pck` carries — what the dump walks (`GODOT_DUMP_PACKED`). */
|
|
75
|
+
export const packedExtensions = Object.keys(FORMATS).filter((ext) => FORMATS[ext].packed);
|
|
76
|
+
|
|
77
|
+
/** The extensions whose text the dump scans for `res://` literals (`GODOT_DUMP_SCANNED`). */
|
|
78
|
+
export const scannedExtensions = Object.keys(FORMATS).filter((ext) => FORMATS[ext].scanned);
|