@defold-typescript/transpiler 0.29.0 → 0.31.0
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/dist/animation-sources.d.ts +6 -0
- package/dist/{sprite-animation-index.d.ts → component-animation-index.d.ts} +4 -4
- package/dist/config-key-index.d.ts +1 -0
- package/dist/gui-flipbook-index.d.ts +8 -0
- package/dist/gui-node-index.d.ts +21 -0
- package/dist/index.d.ts +21 -8
- package/dist/index.js +1317 -289
- package/dist/library-dependencies.d.ts +47 -0
- package/dist/scene-address-declaration.d.ts +14 -0
- package/dist/scene-collection-roles.d.ts +30 -0
- package/dist/scene-component-index.d.ts +20 -0
- package/dist/scene-documents.d.ts +21 -0
- package/dist/scene-naming-context.d.ts +66 -0
- package/dist/scene-object-path-index.d.ts +4 -1
- package/dist/url-address-literals.d.ts +10 -0
- package/dist/url-address-slots.d.ts +3 -1
- package/dist/url-cross-world-address.d.ts +41 -0
- package/dist/url-fragment-reachability.d.ts +3 -0
- package/package.json +2 -2
- package/src/animation-sources.ts +90 -0
- package/src/component-animation-index.ts +399 -0
- package/src/config-key-index.ts +28 -2
- package/src/gui-flipbook-index.ts +91 -0
- package/src/gui-node-index.ts +42 -4
- package/src/index.ts +66 -10
- package/src/library-dependencies.ts +337 -0
- package/src/scene-address-declaration.ts +74 -0
- package/src/scene-collection-roles.ts +267 -0
- package/src/scene-component-index.ts +74 -6
- package/src/scene-documents.ts +186 -0
- package/src/scene-naming-context.ts +130 -0
- package/src/scene-object-path-index.ts +172 -33
- package/src/url-address-literals.ts +115 -0
- package/src/url-address-slots.ts +23 -2
- package/src/url-cross-world-address.ts +109 -0
- package/src/url-fragment-reachability.ts +72 -93
- package/src/sprite-animation-index.ts +0 -241
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import {
|
|
2
|
+
animationSetEntries,
|
|
3
|
+
normalizeDocumentKey,
|
|
4
|
+
readAnimationSources,
|
|
5
|
+
resourceKey,
|
|
6
|
+
} from "./animation-sources";
|
|
7
|
+
import { parseSceneTextFormat, type SceneMessage, SceneTextFormatError } from "./scene-text-format";
|
|
8
|
+
|
|
9
|
+
// The animation ids each script may play, keyed first by the project-relative
|
|
10
|
+
// resource path of the `.script` that owns them and then by the id of the
|
|
11
|
+
// component they belong to, plus an honest record of every link that could not
|
|
12
|
+
// be settled. A missing key means nothing may be offered for it; an empty set
|
|
13
|
+
// means the component resolved and declares no animation.
|
|
14
|
+
export interface ComponentAnimationIndex {
|
|
15
|
+
readonly byScriptResource: ReadonlyMap<string, ReadonlyMap<string, ReadonlySet<string>>>;
|
|
16
|
+
// The display path of the document declaring each animation id — an atlas or
|
|
17
|
+
// tile source for a sprite, and for a model the animation set that *lists the
|
|
18
|
+
// entry* producing the id rather than the root the `.model` points at — under
|
|
19
|
+
// the same two keys plus the id itself. Its per-component key set is exactly
|
|
20
|
+
// that component's id set in `byScriptResource`, so the two can never
|
|
21
|
+
// disagree about which ids a component declares.
|
|
22
|
+
readonly sourceByScriptResource: ReadonlyMap<
|
|
23
|
+
string,
|
|
24
|
+
ReadonlyMap<string, ReadonlyMap<string, string>>
|
|
25
|
+
>;
|
|
26
|
+
readonly unresolved: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SCRIPT_SUFFIX = ".script";
|
|
30
|
+
const SPRITE_SUFFIX = ".sprite";
|
|
31
|
+
const MODEL_SUFFIX = ".model";
|
|
32
|
+
const ANIMATION_SET_SUFFIX = ".animationset";
|
|
33
|
+
|
|
34
|
+
// The only mesh containers `AnimationSetBuilder` accepts. Anything else in an
|
|
35
|
+
// entry is a build error, so it may contribute no id.
|
|
36
|
+
const MESH_SUFFIXES = [".gltf", ".glb"];
|
|
37
|
+
|
|
38
|
+
function firstField(message: SceneMessage, name: string): string | undefined {
|
|
39
|
+
return (message.fields.get(name) ?? [])[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function childrenOf(message: SceneMessage, name: string): readonly SceneMessage[] {
|
|
43
|
+
return message.messages.get(name) ?? [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The id an animation-set entry contributes, before its prefix: the entry's
|
|
47
|
+
// filename with its extension dropped. The names inside the mesh container are
|
|
48
|
+
// never read — for a set the editor stamps the owning name on every animation
|
|
49
|
+
// the file holds, so the path is the whole answer.
|
|
50
|
+
function basename(resource: string): string {
|
|
51
|
+
const file = resource.slice(resource.lastIndexOf("/") + 1);
|
|
52
|
+
const dot = file.lastIndexOf(".");
|
|
53
|
+
return dot === -1 ? file : file.slice(0, dot);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Which kind of document a component reads its ids from. The two resolve
|
|
57
|
+
// differently — a tile source declares ids outright, an animation set names
|
|
58
|
+
// files whose basenames are the ids — while sharing one key space, because
|
|
59
|
+
// component ids are unique within a game object.
|
|
60
|
+
type AnimationSourceKind = "sprite" | "model";
|
|
61
|
+
|
|
62
|
+
interface AnimationSource {
|
|
63
|
+
readonly kind: AnimationSourceKind;
|
|
64
|
+
readonly path: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface AssetIndex {
|
|
68
|
+
readonly animationsByTileSet: Map<string, Set<string>>;
|
|
69
|
+
readonly tileSetBySprite: Map<string, string>;
|
|
70
|
+
readonly modelDocuments: Set<string>;
|
|
71
|
+
readonly animationSourceByModel: Map<string, string>;
|
|
72
|
+
readonly entriesByAnimationSet: Map<string, readonly string[]>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readAssets(assets: ReadonlyMap<string, string>, unresolved: string[]): AssetIndex {
|
|
76
|
+
const animationsByTileSet = readAnimationSources(assets, unresolved);
|
|
77
|
+
const tileSetBySprite = new Map<string, string>();
|
|
78
|
+
const modelDocuments = new Set<string>();
|
|
79
|
+
const animationSourceByModel = new Map<string, string>();
|
|
80
|
+
const entriesByAnimationSet = new Map<string, readonly string[]>();
|
|
81
|
+
|
|
82
|
+
for (const [path, text] of assets) {
|
|
83
|
+
const displayPath = normalizeDocumentKey(path);
|
|
84
|
+
const isSprite = displayPath.endsWith(SPRITE_SUFFIX);
|
|
85
|
+
const isModel = displayPath.endsWith(MODEL_SUFFIX);
|
|
86
|
+
const isAnimationSet = displayPath.endsWith(ANIMATION_SET_SUFFIX);
|
|
87
|
+
if (!isSprite && !isModel && !isAnimationSet) continue;
|
|
88
|
+
let document: SceneMessage;
|
|
89
|
+
try {
|
|
90
|
+
document = parseSceneTextFormat(text);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
93
|
+
unresolved.push(`${displayPath}: could not be parsed (${error.message})`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (isSprite) {
|
|
97
|
+
const tileSet = firstField(document, "tile_set");
|
|
98
|
+
if (tileSet !== undefined) tileSetBySprite.set(displayPath, resourceKey(tileSet));
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (isModel) {
|
|
102
|
+
modelDocuments.add(displayPath);
|
|
103
|
+
const animations = firstField(document, "animations");
|
|
104
|
+
if (animations !== undefined) animationSourceByModel.set(displayPath, animations);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
entriesByAnimationSet.set(displayPath, animationSetEntries(document));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
animationsByTileSet,
|
|
112
|
+
tileSetBySprite,
|
|
113
|
+
modelDocuments,
|
|
114
|
+
animationSourceByModel,
|
|
115
|
+
entriesByAnimationSet,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The ids one animation set contributes, each mapped to the display path of the
|
|
120
|
+
// set that lists the entry producing it, or `undefined` when the chain itself
|
|
121
|
+
// could not be read. An entry in an unsupported format is refused on its own
|
|
122
|
+
// and the rest of the set still stands, because that entry is a build error the
|
|
123
|
+
// author sees; a missing document or a cycle breaks the chain instead, and a
|
|
124
|
+
// partial answer from a chain that is not understood is exactly the wrong guess
|
|
125
|
+
// this index refuses to make.
|
|
126
|
+
function resolveAnimationSet(input: {
|
|
127
|
+
path: string;
|
|
128
|
+
prefix: string;
|
|
129
|
+
stack: Set<string>;
|
|
130
|
+
assets: AssetIndex;
|
|
131
|
+
unresolved: string[];
|
|
132
|
+
}): Map<string, string> | undefined {
|
|
133
|
+
const { path, prefix, stack, assets, unresolved } = input;
|
|
134
|
+
const entries = assets.entriesByAnimationSet.get(path);
|
|
135
|
+
if (entries === undefined) return undefined;
|
|
136
|
+
|
|
137
|
+
// Branch-local, not global: a set reached from two different entries is
|
|
138
|
+
// reached twice legitimately, and only a set re-entered while still being
|
|
139
|
+
// read is a cycle.
|
|
140
|
+
stack.add(path);
|
|
141
|
+
try {
|
|
142
|
+
const ids = new Map<string, string>();
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const key = resourceKey(entry);
|
|
145
|
+
if (MESH_SUFFIXES.some((suffix) => key.endsWith(suffix))) {
|
|
146
|
+
ids.set(prefix + basename(key), path);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (!key.endsWith(ANIMATION_SET_SUFFIX)) {
|
|
150
|
+
unresolved.push(
|
|
151
|
+
`${path}: the entry ${entry} is not a .gltf, .glb or .animationset, so it declares no animation id`,
|
|
152
|
+
);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!assets.entriesByAnimationSet.has(key)) {
|
|
156
|
+
unresolved.push(
|
|
157
|
+
`${path}: the entry ${entry} is not among the project's animation set documents`,
|
|
158
|
+
);
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
if (stack.has(key)) {
|
|
162
|
+
unresolved.push(
|
|
163
|
+
`${path}: the entry ${entry} re-enters an animation set already being read, so the chain is cyclic`,
|
|
164
|
+
);
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
const nested = resolveAnimationSet({
|
|
168
|
+
path: key,
|
|
169
|
+
prefix: `${prefix + basename(key)}/`,
|
|
170
|
+
stack,
|
|
171
|
+
assets,
|
|
172
|
+
unresolved,
|
|
173
|
+
});
|
|
174
|
+
if (nested === undefined) return undefined;
|
|
175
|
+
for (const [id, declaring] of nested) if (!ids.has(id)) ids.set(id, declaring);
|
|
176
|
+
}
|
|
177
|
+
return ids;
|
|
178
|
+
} finally {
|
|
179
|
+
stack.delete(path);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The animation-declaring components of one game object, as
|
|
184
|
+
// `id -> source document`. An embedded component carries its own payload; a
|
|
185
|
+
// referenced one names a document that carries it instead. A `.model` naming a
|
|
186
|
+
// mesh source directly is refused here rather than resolved: the editor and the
|
|
187
|
+
// build pipeline disagree about which ids such a model can play, so no id is
|
|
188
|
+
// established and the model is named in `unresolved`.
|
|
189
|
+
function componentAnimationSources(
|
|
190
|
+
object: SceneMessage,
|
|
191
|
+
displayPath: string,
|
|
192
|
+
assets: AssetIndex,
|
|
193
|
+
unresolved: string[],
|
|
194
|
+
): Map<string, AnimationSource> {
|
|
195
|
+
const sources = new Map<string, AnimationSource>();
|
|
196
|
+
|
|
197
|
+
function claimModel(id: string, animations: string): void {
|
|
198
|
+
if (!animations.endsWith(ANIMATION_SET_SUFFIX)) {
|
|
199
|
+
unresolved.push(
|
|
200
|
+
`${displayPath}: the model component "${id}" names ${animations} rather than an animation set, so none of its animation ids are established`,
|
|
201
|
+
);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
sources.set(id, { kind: "model", path: resourceKey(animations) });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const embedded of childrenOf(object, "embedded_components")) {
|
|
208
|
+
const type = firstField(embedded, "type");
|
|
209
|
+
if (type !== "sprite" && type !== "model") continue;
|
|
210
|
+
const id = firstField(embedded, "id");
|
|
211
|
+
const payload = firstField(embedded, "data");
|
|
212
|
+
if (id === undefined || payload === undefined) continue;
|
|
213
|
+
let document: SceneMessage;
|
|
214
|
+
try {
|
|
215
|
+
document = parseSceneTextFormat(payload);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
218
|
+
unresolved.push(
|
|
219
|
+
`${displayPath}: the embedded ${type} "${id}" could not be read (${error.message})`,
|
|
220
|
+
);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (type === "sprite") {
|
|
224
|
+
const tileSet = firstField(document, "tile_set");
|
|
225
|
+
if (tileSet !== undefined) sources.set(id, { kind: "sprite", path: resourceKey(tileSet) });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const animations = firstField(document, "animations");
|
|
229
|
+
if (animations !== undefined) claimModel(id, animations);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const referenced of childrenOf(object, "components")) {
|
|
233
|
+
const component = firstField(referenced, "component");
|
|
234
|
+
const id = firstField(referenced, "id");
|
|
235
|
+
if (id === undefined || component === undefined) continue;
|
|
236
|
+
if (component.endsWith(SPRITE_SUFFIX)) {
|
|
237
|
+
const tileSet = assets.tileSetBySprite.get(resourceKey(component));
|
|
238
|
+
if (tileSet === undefined) {
|
|
239
|
+
unresolved.push(
|
|
240
|
+
`${displayPath}: the sprite component "${id}" names ${component}, which is not among the project's sprite documents`,
|
|
241
|
+
);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
sources.set(id, { kind: "sprite", path: tileSet });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (!component.endsWith(MODEL_SUFFIX)) continue;
|
|
248
|
+
const resource = resourceKey(component);
|
|
249
|
+
if (!assets.modelDocuments.has(resource)) {
|
|
250
|
+
unresolved.push(
|
|
251
|
+
`${displayPath}: the model component "${id}" names ${component}, which is not among the project's model documents`,
|
|
252
|
+
);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// A model that declares no animations at all is a legitimate authoring
|
|
256
|
+
// state, not a broken link: it resolves to nothing and says nothing.
|
|
257
|
+
const animations = assets.animationSourceByModel.get(resource);
|
|
258
|
+
if (animations !== undefined) claimModel(id, animations);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return sources;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function scriptResourcesOf(object: SceneMessage): string[] {
|
|
265
|
+
const resources: string[] = [];
|
|
266
|
+
for (const component of childrenOf(object, "components")) {
|
|
267
|
+
const resource = firstField(component, "component");
|
|
268
|
+
if (resource === undefined || !resource.endsWith(SCRIPT_SUFFIX)) continue;
|
|
269
|
+
const key = resourceKey(resource);
|
|
270
|
+
if (key !== SCRIPT_SUFFIX.slice(1)) resources.push(key);
|
|
271
|
+
}
|
|
272
|
+
return resources;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isGameObject(message: SceneMessage): boolean {
|
|
276
|
+
return message.messages.has("components") || message.messages.has("embedded_components");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function buildComponentAnimationIndex(input: {
|
|
280
|
+
scenes: ReadonlyMap<string, string>;
|
|
281
|
+
assets: ReadonlyMap<string, string>;
|
|
282
|
+
}): ComponentAnimationIndex {
|
|
283
|
+
const unresolved: string[] = [];
|
|
284
|
+
const assets = readAssets(input.assets, unresolved);
|
|
285
|
+
const byScriptResource = new Map<string, ReadonlyMap<string, ReadonlySet<string>>>();
|
|
286
|
+
const sourceByScriptResource = new Map<
|
|
287
|
+
string,
|
|
288
|
+
ReadonlyMap<string, ReadonlyMap<string, string>>
|
|
289
|
+
>();
|
|
290
|
+
const claimedBy = new Map<string, string>();
|
|
291
|
+
|
|
292
|
+
// A `.go` document is a game object at its root; a `.collection` carries one
|
|
293
|
+
// inside each `embedded_instances` `data:` payload, decoded exactly one level
|
|
294
|
+
// at a time. No `instances { prototype: }` edge is followed: the referenced
|
|
295
|
+
// `.go` is its own document and already claims its own script.
|
|
296
|
+
function walk(message: SceneMessage, blockName: string, displayPath: string): void {
|
|
297
|
+
if (isGameObject(message)) {
|
|
298
|
+
claim(message, displayPath);
|
|
299
|
+
}
|
|
300
|
+
if (blockName === "embedded_instances") {
|
|
301
|
+
for (const payload of message.fields.get("data") ?? []) {
|
|
302
|
+
try {
|
|
303
|
+
walk(parseSceneTextFormat(payload), "", displayPath);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
306
|
+
unresolved.push(
|
|
307
|
+
`${displayPath}: an embedded game object could not be read (${error.message})`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const [name, nested] of message.messages) {
|
|
313
|
+
for (const child of nested) {
|
|
314
|
+
walk(child, name, displayPath);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function idsOf(
|
|
320
|
+
id: string,
|
|
321
|
+
source: AnimationSource,
|
|
322
|
+
displayPath: string,
|
|
323
|
+
): Map<string, string> | undefined {
|
|
324
|
+
if (source.kind === "sprite") {
|
|
325
|
+
const declared = assets.animationsByTileSet.get(source.path);
|
|
326
|
+
if (declared !== undefined) return new Map([...declared].map((name) => [name, source.path]));
|
|
327
|
+
unresolved.push(
|
|
328
|
+
`${displayPath}: the sprite component "${id}" names the tile source /${source.path}, which is not among the project's asset documents`,
|
|
329
|
+
);
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
const resolved = resolveAnimationSet({
|
|
333
|
+
path: source.path,
|
|
334
|
+
prefix: "",
|
|
335
|
+
stack: new Set(),
|
|
336
|
+
assets,
|
|
337
|
+
unresolved,
|
|
338
|
+
});
|
|
339
|
+
if (resolved !== undefined) return resolved;
|
|
340
|
+
if (!assets.entriesByAnimationSet.has(source.path)) {
|
|
341
|
+
unresolved.push(
|
|
342
|
+
`${displayPath}: the model component "${id}" names the animation set /${source.path}, which is not among the project's asset documents`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function claim(object: SceneMessage, displayPath: string): void {
|
|
349
|
+
const scripts = scriptResourcesOf(object);
|
|
350
|
+
if (scripts.length === 0) return;
|
|
351
|
+
|
|
352
|
+
const sources = componentAnimationSources(object, displayPath, assets, unresolved);
|
|
353
|
+
const animations = new Map<string, ReadonlySet<string>>();
|
|
354
|
+
const declaringSources = new Map<string, ReadonlyMap<string, string>>();
|
|
355
|
+
for (const [id, source] of sources) {
|
|
356
|
+
const resolved = idsOf(id, source, displayPath);
|
|
357
|
+
if (resolved === undefined) continue;
|
|
358
|
+
animations.set(id, new Set(resolved.keys()));
|
|
359
|
+
declaringSources.set(id, resolved);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (const key of scripts) {
|
|
363
|
+
const owner = claimedBy.get(key);
|
|
364
|
+
if (owner !== undefined) {
|
|
365
|
+
byScriptResource.delete(key);
|
|
366
|
+
sourceByScriptResource.delete(key);
|
|
367
|
+
unresolved.push(
|
|
368
|
+
`${key}: claimed by both ${owner} and ${displayPath}, so its component animations are ambiguous`,
|
|
369
|
+
);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
claimedBy.set(key, displayPath);
|
|
373
|
+
byScriptResource.set(key, animations);
|
|
374
|
+
sourceByScriptResource.set(key, declaringSources);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const [path, text] of input.scenes) {
|
|
379
|
+
const displayPath = normalizeDocumentKey(path);
|
|
380
|
+
try {
|
|
381
|
+
walk(parseSceneTextFormat(text), "", displayPath);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
384
|
+
unresolved.push(`${displayPath}: could not be parsed (${error.message})`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { byScriptResource, sourceByScriptResource, unresolved };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// The component id a same-object `"#id"` address names, or `undefined` for
|
|
392
|
+
// every other form. A path form names a component on a *different* game
|
|
393
|
+
// object, so its id must never be resolved against this script's own.
|
|
394
|
+
export function componentIdOfSameObjectAddress(address: string): string | undefined {
|
|
395
|
+
if (!address.startsWith("#")) return undefined;
|
|
396
|
+
const id = address.slice(1);
|
|
397
|
+
if (id === "" || id.includes("/") || id.includes("#")) return undefined;
|
|
398
|
+
return id;
|
|
399
|
+
}
|
package/src/config-key-index.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Deliberately a line reader rather than a parse of the scene text format:
|
|
6
6
|
// `game.project` is a flat INI the editor writes, the same shape
|
|
7
|
-
// `
|
|
8
|
-
// (`dependencies#0`), which is part of its name here and never a
|
|
7
|
+
// `readGameProjectDependencies` (library-dependencies.ts) already walks. A key
|
|
8
|
+
// may carry a `#` (`dependencies#0`), which is part of its name here and never a
|
|
9
|
+
// fragment.
|
|
9
10
|
|
|
10
11
|
function sectionNameOf(line: string): string | undefined {
|
|
11
12
|
const match = line.trim().match(/^\[(.+)\]$/);
|
|
@@ -32,3 +33,28 @@ export function buildConfigKeyIndex(gameProjectText: string): ReadonlySet<string
|
|
|
32
33
|
}
|
|
33
34
|
return keys;
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
// One declared value, read the same way `buildConfigKeyIndex` reads the whole
|
|
38
|
+
// file. The section is matched in full rather than as a prefix, so a
|
|
39
|
+
// `main_collection_backup` beside `main_collection` answers neither.
|
|
40
|
+
export function readGameProjectSetting(
|
|
41
|
+
gameProjectText: string,
|
|
42
|
+
section: string,
|
|
43
|
+
key: string,
|
|
44
|
+
): string | undefined {
|
|
45
|
+
let current: string | undefined;
|
|
46
|
+
for (const line of gameProjectText.split("\n")) {
|
|
47
|
+
const header = sectionNameOf(line);
|
|
48
|
+
if (header !== undefined) {
|
|
49
|
+
current = header;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (current !== section) continue;
|
|
53
|
+
const separator = line.indexOf("=");
|
|
54
|
+
if (separator === -1) continue;
|
|
55
|
+
if (line.slice(0, separator).trim() !== key) continue;
|
|
56
|
+
const value = line.slice(separator + 1).trim();
|
|
57
|
+
if (value !== "") return value;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { normalizeDocumentKey, readAnimationSources, resourceKey } from "./animation-sources";
|
|
2
|
+
import { parseSceneTextFormat, type SceneMessage, SceneTextFormatError } from "./scene-text-format";
|
|
3
|
+
|
|
4
|
+
// The flipbook animation ids each gui script may play, keyed by the
|
|
5
|
+
// project-relative resource path of the `.gui_script` that owns them, and then
|
|
6
|
+
// by animation id — whose value is the display path of every named texture
|
|
7
|
+
// declaring it, so a completion reads the keys and a provenance panel reads the
|
|
8
|
+
// value. A missing key means nothing may be offered for it; an empty map means
|
|
9
|
+
// the scene resolved and its textures declare no animation.
|
|
10
|
+
export interface GuiFlipbookIndex {
|
|
11
|
+
readonly byScriptResource: ReadonlyMap<string, ReadonlyMap<string, ReadonlySet<string>>>;
|
|
12
|
+
readonly unresolved: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// The scope is the `.gui` scene naming this script, never the node the call
|
|
16
|
+
// addresses: `gui.set_texture` retargets a node at runtime, so scoping to a
|
|
17
|
+
// node's authored texture would reject calls that are valid. The scene's
|
|
18
|
+
// `textures` blocks are the honest universe.
|
|
19
|
+
const SCRIPT_SUFFIX = ".gui_script";
|
|
20
|
+
|
|
21
|
+
function scriptKeyOf(script: string): string | undefined {
|
|
22
|
+
if (!script.endsWith(SCRIPT_SUFFIX)) return undefined;
|
|
23
|
+
const key = resourceKey(script);
|
|
24
|
+
return key === SCRIPT_SUFFIX ? undefined : key;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildGuiFlipbookIndex(input: {
|
|
28
|
+
scenes: ReadonlyMap<string, string>;
|
|
29
|
+
assets: ReadonlyMap<string, string>;
|
|
30
|
+
}): GuiFlipbookIndex {
|
|
31
|
+
const unresolved: string[] = [];
|
|
32
|
+
const animationsBySource = readAnimationSources(input.assets, unresolved);
|
|
33
|
+
const byScriptResource = new Map<string, ReadonlyMap<string, ReadonlySet<string>>>();
|
|
34
|
+
const claimedBy = new Map<string, string>();
|
|
35
|
+
|
|
36
|
+
for (const [path, text] of input.scenes) {
|
|
37
|
+
const displayPath = normalizeDocumentKey(path);
|
|
38
|
+
let document: SceneMessage;
|
|
39
|
+
try {
|
|
40
|
+
document = parseSceneTextFormat(text);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
43
|
+
unresolved.push(`${displayPath}: could not be parsed (${error.message})`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [script] = document.fields.get("script") ?? [];
|
|
48
|
+
if (script === undefined) continue;
|
|
49
|
+
const key = scriptKeyOf(script);
|
|
50
|
+
if (key === undefined) continue;
|
|
51
|
+
|
|
52
|
+
const owner = claimedBy.get(key);
|
|
53
|
+
if (owner !== undefined) {
|
|
54
|
+
byScriptResource.delete(key);
|
|
55
|
+
unresolved.push(
|
|
56
|
+
`${key}: claimed by both ${owner} and ${displayPath}, so its flipbook animations are ambiguous`,
|
|
57
|
+
);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
claimedBy.set(key, displayPath);
|
|
61
|
+
|
|
62
|
+
// Top-level `textures` only, for the reason `buildGuiNodeIndex` reads only
|
|
63
|
+
// top-level `nodes`: a `layouts` or `templates` block repeats an authored
|
|
64
|
+
// surface under an override rather than naming a source of its own.
|
|
65
|
+
const declaredBy = new Map<string, Set<string>>();
|
|
66
|
+
for (const message of document.messages.get("textures") ?? []) {
|
|
67
|
+
for (const texture of message.fields.get("texture") ?? []) {
|
|
68
|
+
if (texture === "") continue;
|
|
69
|
+
const source = resourceKey(texture);
|
|
70
|
+
const ids = animationsBySource.get(source);
|
|
71
|
+
if (ids === undefined) {
|
|
72
|
+
unresolved.push(
|
|
73
|
+
`${displayPath}: the texture ${texture} is not among the project's asset documents`,
|
|
74
|
+
);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (const id of ids) {
|
|
78
|
+
const declarers = declaredBy.get(id);
|
|
79
|
+
if (declarers) {
|
|
80
|
+
declarers.add(source);
|
|
81
|
+
} else {
|
|
82
|
+
declaredBy.set(id, new Set([source]));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
byScriptResource.set(key, declaredBy);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { byScriptResource, unresolved };
|
|
91
|
+
}
|
package/src/gui-node-index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
|
|
1
|
+
import { parseSceneTextFormat, type SceneMessage, SceneTextFormatError } from "./scene-text-format";
|
|
2
2
|
|
|
3
3
|
// The node ids each script may address, keyed by the project-relative resource
|
|
4
4
|
// path of the `.gui_script` that owns them, plus an honest record of every scene
|
|
@@ -25,6 +25,46 @@ function scriptKeyOf(script: string): string | undefined {
|
|
|
25
25
|
return key === SCRIPT_SUFFIX ? undefined : key;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The gui-script resource one already-parsed `.gui` names, keyed the way this
|
|
30
|
+
* index keys it. Exported so the naming-context resolver reaches a gui script
|
|
31
|
+
* through the same rule rather than a second reader — the aggregation on top
|
|
32
|
+
* differs on purpose (this index refuses a resource two scenes claim, while two
|
|
33
|
+
* scenes driving one gui script really is two naming contexts), but the key
|
|
34
|
+
* shape has one owner.
|
|
35
|
+
*/
|
|
36
|
+
export function guiScriptResourceOf(document: SceneMessage): string | undefined {
|
|
37
|
+
const [script] = document.fields.get("script") ?? [];
|
|
38
|
+
return script === undefined ? undefined : scriptKeyOf(script);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Every `.gui` document's display path mapped to the gui-script resource it
|
|
43
|
+
* names, skipping a `.gui` that names none and one this parser cannot read.
|
|
44
|
+
*
|
|
45
|
+
* The map `buildScriptNamingContexts` consumes, built once here so the editor
|
|
46
|
+
* plugin and the CLI reach a gui script through one reader. Unlike
|
|
47
|
+
* `buildGuiNodeIndex` it records no ambiguity: two `.gui` scenes driving one gui
|
|
48
|
+
* script really are two naming contexts, while they would make that script's
|
|
49
|
+
* node ids unanswerable.
|
|
50
|
+
*/
|
|
51
|
+
export function guiScriptResourcesOf(
|
|
52
|
+
documents: ReadonlyMap<string, string>,
|
|
53
|
+
): ReadonlyMap<string, string> {
|
|
54
|
+
const scripts = new Map<string, string>();
|
|
55
|
+
for (const [displayPath, text] of documents) {
|
|
56
|
+
let resource: string | undefined;
|
|
57
|
+
try {
|
|
58
|
+
resource = guiScriptResourceOf(parseSceneTextFormat(text));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (resource !== undefined) scripts.set(displayPath, resource);
|
|
64
|
+
}
|
|
65
|
+
return scripts;
|
|
66
|
+
}
|
|
67
|
+
|
|
28
68
|
export function buildGuiNodeIndex(documents: ReadonlyMap<string, string>): GuiNodeIndex {
|
|
29
69
|
const byScriptResource = new Map<string, ReadonlySet<string>>();
|
|
30
70
|
const claimedBy = new Map<string, string>();
|
|
@@ -40,9 +80,7 @@ export function buildGuiNodeIndex(documents: ReadonlyMap<string, string>): GuiNo
|
|
|
40
80
|
continue;
|
|
41
81
|
}
|
|
42
82
|
|
|
43
|
-
const
|
|
44
|
-
if (script === undefined) continue;
|
|
45
|
-
const key = scriptKeyOf(script);
|
|
83
|
+
const key = guiScriptResourceOf(document);
|
|
46
84
|
if (key === undefined) continue;
|
|
47
85
|
|
|
48
86
|
const owner = claimedBy.get(key);
|