@defold-typescript/transpiler 0.30.0 → 0.32.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 +19 -9
- package/dist/index.js +1326 -396
- package/dist/library-dependencies.d.ts +47 -0
- package/dist/scene-address-declaration.d.ts +4 -2
- package/dist/scene-collection-roles.d.ts +30 -0
- package/dist/scene-component-index.d.ts +20 -0
- package/dist/scene-documents.d.ts +3 -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 +52 -10
- package/src/library-dependencies.ts +337 -0
- package/src/scene-address-declaration.ts +29 -12
- package/src/scene-collection-roles.ts +267 -0
- package/src/scene-component-index.ts +74 -6
- package/src/scene-documents.ts +66 -6
- package/src/scene-naming-context.ts +130 -0
- package/src/scene-object-path-index.ts +172 -33
- package/src/transpile.ts +3 -0
- 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
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
import type { UrlParameterTable } from "@defold-typescript/types";
|
|
7
7
|
import * as ts from "typescript";
|
|
8
8
|
import type { SceneComponentIndex } from "./scene-component-index";
|
|
9
|
+
import { socketOfAddress } from "./scene-naming-context";
|
|
10
|
+
import type { SceneObjectPathIndex } from "./scene-object-path-index";
|
|
11
|
+
import { canonicalHashSymbols, isAmbient, staticAddressTextOf } from "./url-address-literals";
|
|
9
12
|
import { addressClassOfArgument, isAddressClass } from "./url-address-slots";
|
|
10
13
|
|
|
11
14
|
export interface UrlFragmentFinding {
|
|
@@ -16,103 +19,72 @@ export interface UrlFragmentFinding {
|
|
|
16
19
|
readonly message: string;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
// The half of the joined index this check reads: which objects exist, what each
|
|
23
|
+
// owns, and — the precondition rather than decoration — whether the walk placed
|
|
24
|
+
// every world it found. `incomplete` is carried because a world it could not
|
|
25
|
+
// place contributes no key at all, so nothing downstream can notice its absence.
|
|
26
|
+
export type SceneObjectComponents = Pick<
|
|
27
|
+
SceneObjectPathIndex,
|
|
28
|
+
"paths" | "componentsOf" | "incomplete"
|
|
29
|
+
>;
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
// The path portion of an index key, with any proxy socket stripped: `/enemy` for
|
|
32
|
+
// both the bootstrap `/enemy` and the proxied `mylevel:/enemy`.
|
|
33
|
+
function pathOfKey(key: string): string {
|
|
34
|
+
const socket = socketOfAddress(key);
|
|
35
|
+
return socket === undefined ? key : key.slice(socket.length + 1);
|
|
25
36
|
}
|
|
26
37
|
|
|
27
|
-
// The
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// `Core.Hash` symbol; a project's `Project.Hash` is not in global scope under
|
|
38
|
-
// the bare name and never appears, and a project cannot add a second global
|
|
39
|
-
// `Hash` type without a duplicate-identifier error — so unlike the *name*, this
|
|
40
|
-
// anchor cannot be shadowed.
|
|
41
|
-
function ambientHashAliasTargets(checker: ts.TypeChecker, anchor: ts.Node): ReadonlySet<ts.Symbol> {
|
|
42
|
-
const targets = new Set<ts.Symbol>();
|
|
43
|
-
for (const symbol of checker.getSymbolsInScope(anchor, ts.SymbolFlags.Type)) {
|
|
44
|
-
if (symbol.name !== "Hash") continue;
|
|
45
|
-
const target = referenceTargetSymbol(checker.getDeclaredTypeOfSymbol(symbol));
|
|
46
|
-
if (target) targets.add(target);
|
|
47
|
-
}
|
|
48
|
-
return targets;
|
|
49
|
-
}
|
|
38
|
+
// The components the object an address names declares, or `undefined` wherever
|
|
39
|
+
// the answer is not provable — no object index, a fragment with no path, a
|
|
40
|
+
// relative path, a path no world declares, or a path some world declares but
|
|
41
|
+
// whose prototype could not be read. Every one of those falls back to the
|
|
42
|
+
// project-wide id set, because withholding a finding is the only safe direction.
|
|
43
|
+
function declaredComponentsOf(
|
|
44
|
+
objects: SceneObjectComponents | undefined,
|
|
45
|
+
path: string,
|
|
46
|
+
): readonly string[] | undefined {
|
|
47
|
+
if (objects === undefined || path === "") return undefined;
|
|
50
48
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
// The answer is the *intersection* of the two anchors. The `hash` function is
|
|
60
|
-
// the scope-and-fail-closed anchor: its scope anchor is an ambient file so that
|
|
61
|
-
// no user module's own `hash` participates, and every call signature is walked
|
|
62
|
-
// because a project's global-script `function hash(s: string): string` *merges*
|
|
63
|
-
// into this symbol rather than replacing it — the declarations then span both
|
|
64
|
-
// files and a fully-qualified-name test would reject the real one. The global
|
|
65
|
-
// `Hash` alias is the identity anchor: the function alone is not enough, because
|
|
66
|
-
// a merged project overload contributes its own signatures, and testing what
|
|
67
|
-
// they return by *name* admits any project type called `Hash` and then reports a
|
|
68
|
-
// type argument that was never an address (bug-122). Asking the checker instead
|
|
69
|
-
// of the filesystem is the same discipline as `tableKey` in
|
|
70
|
-
// `url-address-slots.ts`.
|
|
71
|
-
function canonicalHashSymbols(
|
|
72
|
-
checker: ts.TypeChecker,
|
|
73
|
-
program: ts.Program,
|
|
74
|
-
): ReadonlySet<ts.Symbol> {
|
|
75
|
-
const files = program.getSourceFiles();
|
|
76
|
-
const anchor = files.find((file) => isAmbient(file.fileName)) ?? files[0];
|
|
77
|
-
const canonical = new Set<ts.Symbol>();
|
|
78
|
-
if (!anchor) return canonical;
|
|
49
|
+
// A world `buildSceneCollectionRoles` could not place composes no key, so it
|
|
50
|
+
// is invisible to the withheld-`componentsOf` rule below: that rule defends a
|
|
51
|
+
// key that is in `paths`, and this one never got there. Every address form
|
|
52
|
+
// withdraws together, because `incomplete` carries free prose and there is no
|
|
53
|
+
// structured way to ask whether a given hole could own a given path. The
|
|
54
|
+
// project-wide test still runs — this is a fallback, never a suppression.
|
|
55
|
+
if (objects.incomplete.length > 0) return undefined;
|
|
79
56
|
|
|
80
|
-
|
|
57
|
+
if (socketOfAddress(path) !== undefined) {
|
|
58
|
+
return objects.paths.has(path) ? objects.componentsOf.get(path) : undefined;
|
|
59
|
+
}
|
|
60
|
+
if (!path.startsWith("/")) return undefined;
|
|
81
61
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
62
|
+
// A bare absolute path names one object per world, and the script writing it
|
|
63
|
+
// could be running in any of them, so the union is the only claim that holds
|
|
64
|
+
// whichever world it is. One withheld key withholds the whole union: a
|
|
65
|
+
// readable sibling must not stand in for a prototype nobody could read.
|
|
66
|
+
const declared = new Set<string>();
|
|
67
|
+
let known = false;
|
|
68
|
+
for (const key of objects.paths) {
|
|
69
|
+
if (pathOfKey(key) !== path) continue;
|
|
70
|
+
const owned = objects.componentsOf.get(key);
|
|
71
|
+
if (owned === undefined) return undefined;
|
|
72
|
+
known = true;
|
|
73
|
+
for (const id of owned) declared.add(id);
|
|
92
74
|
}
|
|
93
|
-
return
|
|
75
|
+
return known ? [...declared].sort() : undefined;
|
|
94
76
|
}
|
|
95
77
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// text on the constant's type and no reassignment tracking is needed.
|
|
101
|
-
//
|
|
102
|
-
// The interface must be *the* `Hash`: a project's own generic type named `Hash`
|
|
103
|
-
// resolves to its own declaration, and reading a type argument off it would
|
|
104
|
-
// report a fragment that was never an address. `canonical` is what settles that.
|
|
105
|
-
function hashedSourceOfType(
|
|
106
|
-
checker: ts.TypeChecker,
|
|
107
|
-
type: ts.Type,
|
|
108
|
-
canonical: ReadonlySet<ts.Symbol>,
|
|
109
|
-
): string | undefined {
|
|
110
|
-
const target = referenceTargetSymbol(type);
|
|
111
|
-
if (!target || !canonical.has(target)) return undefined;
|
|
112
|
-
const [source] = checker.getTypeArguments(type as ts.TypeReference);
|
|
113
|
-
return source?.isStringLiteral() ? source.value : undefined;
|
|
78
|
+
function declaredPhrase(ids: readonly string[]): string {
|
|
79
|
+
return ids.length === 0
|
|
80
|
+
? "it declares no components at all"
|
|
81
|
+
: `it declares ${ids.map((id) => `"${id}"`).join(", ")}`;
|
|
114
82
|
}
|
|
115
83
|
|
|
84
|
+
export type UrlFragmentReport =
|
|
85
|
+
| { readonly kind: "suppressed"; readonly reasons: readonly string[] }
|
|
86
|
+
| { readonly kind: "checked"; readonly findings: readonly UrlFragmentFinding[] };
|
|
87
|
+
|
|
116
88
|
// Report every address-slot expression whose text is statically known — a
|
|
117
89
|
// quoted or backtick-quoted literal, or a value whose `Hash` type still carries
|
|
118
90
|
// the string it was hashed from — whose `#fragment` names a component no
|
|
@@ -126,9 +98,10 @@ export function checkUrlFragmentReachability(input: {
|
|
|
126
98
|
program: ts.Program;
|
|
127
99
|
table: UrlParameterTable;
|
|
128
100
|
index: SceneComponentIndex;
|
|
101
|
+
objects?: SceneObjectComponents;
|
|
129
102
|
sourceFiles?: readonly ts.SourceFile[];
|
|
130
103
|
}): UrlFragmentReport {
|
|
131
|
-
const { program, table, index, sourceFiles } = input;
|
|
104
|
+
const { program, table, index, objects, sourceFiles } = input;
|
|
132
105
|
if (index.incomplete.length > 0) {
|
|
133
106
|
return { kind: "suppressed", reasons: index.incomplete };
|
|
134
107
|
}
|
|
@@ -147,21 +120,27 @@ export function checkUrlFragmentReachability(input: {
|
|
|
147
120
|
|
|
148
121
|
const visit = (node: ts.Node): void => {
|
|
149
122
|
if (ts.isExpression(node) && isAddressClass(addressClassOfArgument(checker, table, node))) {
|
|
150
|
-
const text =
|
|
151
|
-
? node.text
|
|
152
|
-
: hashedSourceOfType(checker, checker.getTypeAtLocation(node), canonical);
|
|
123
|
+
const text = staticAddressTextOf(checker, node, canonical);
|
|
153
124
|
if (text !== undefined) {
|
|
154
125
|
const hash = text.indexOf("#");
|
|
155
126
|
const fragment = hash === -1 ? "" : text.slice(hash + 1);
|
|
156
|
-
|
|
127
|
+
const declared =
|
|
128
|
+
fragment === "" ? undefined : declaredComponentsOf(objects, text.slice(0, hash));
|
|
129
|
+
const reachable =
|
|
130
|
+
declared === undefined ? index.ids.has(fragment) : declared.includes(fragment);
|
|
131
|
+
if (fragment !== "" && !reachable) {
|
|
132
|
+
const path = text.slice(0, hash);
|
|
157
133
|
findings.push({
|
|
158
134
|
fileName: sourceFile.fileName,
|
|
159
135
|
start: node.getStart(sourceFile),
|
|
160
136
|
length: node.getWidth(sourceFile),
|
|
161
137
|
fragment,
|
|
162
138
|
message:
|
|
163
|
-
|
|
164
|
-
|
|
139
|
+
declared === undefined
|
|
140
|
+
? `no \`.go\` or \`.collection\` in this project declares a component with the id ` +
|
|
141
|
+
`"${fragment}", so this address cannot resolve at runtime`
|
|
142
|
+
: `the game object "${path}" declares no component with the id "${fragment}" ` +
|
|
143
|
+
`(${declaredPhrase(declared)}), so this address cannot resolve at runtime`,
|
|
165
144
|
});
|
|
166
145
|
}
|
|
167
146
|
}
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { parseSceneTextFormat, type SceneMessage, SceneTextFormatError } from "./scene-text-format";
|
|
2
|
-
|
|
3
|
-
// The animation ids each script may play, keyed first by the project-relative
|
|
4
|
-
// resource path of the `.script` that owns them and then by the id of the
|
|
5
|
-
// sprite component they belong to, plus an honest record of every link that
|
|
6
|
-
// could not be settled. A missing key means nothing may be offered for it; an
|
|
7
|
-
// empty set means the component resolved and declares no animation.
|
|
8
|
-
export interface SpriteAnimationIndex {
|
|
9
|
-
readonly byScriptResource: ReadonlyMap<string, ReadonlyMap<string, ReadonlySet<string>>>;
|
|
10
|
-
// The display path of the tile source each keyed component's ids were read
|
|
11
|
-
// from, under the same two keys. Present for exactly the components
|
|
12
|
-
// `byScriptResource` keys, so the two can never disagree about a resource.
|
|
13
|
-
readonly tileSetByScriptResource: ReadonlyMap<string, ReadonlyMap<string, string>>;
|
|
14
|
-
readonly unresolved: readonly string[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Resource paths inside a scene are project-absolute; the index is keyed the
|
|
18
|
-
// way `buildGuiNodeIndex` keys its own, with the leading `/` dropped, so a
|
|
19
|
-
// caller maps the file it holds forward through the build's output-path math
|
|
20
|
-
// and looks the result up here.
|
|
21
|
-
function resourceKey(resource: string): string {
|
|
22
|
-
return resource.startsWith("/") ? resource.slice(1) : resource;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// The keys a caller supplies name files on the host, so they may arrive with
|
|
26
|
-
// native separators; the resource paths they are matched against come out of
|
|
27
|
-
// scene content and are always `/`-separated. Normalizing here enforces that
|
|
28
|
-
// contract at the boundary instead of leaving it to whichever caller happens
|
|
29
|
-
// to build the maps.
|
|
30
|
-
function normalizeDocumentKey(path: string): string {
|
|
31
|
-
return path.replaceAll("\\", "/");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const SCRIPT_SUFFIX = ".script";
|
|
35
|
-
const SPRITE_SUFFIX = ".sprite";
|
|
36
|
-
|
|
37
|
-
function firstField(message: SceneMessage, name: string): string | undefined {
|
|
38
|
-
return (message.fields.get(name) ?? [])[0];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function childrenOf(message: SceneMessage, name: string): readonly SceneMessage[] {
|
|
42
|
-
return message.messages.get(name) ?? [];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Only `animations { id: … }` counts. Defold may also expose a bare
|
|
46
|
-
// `images { image: … }` entry as a one-frame animation named after the file,
|
|
47
|
-
// which is unverified here — and a wrong suggestion in this slot is a runtime
|
|
48
|
-
// crash rather than a no-op.
|
|
49
|
-
function declaredAnimations(document: SceneMessage): Set<string> {
|
|
50
|
-
const ids = new Set<string>();
|
|
51
|
-
for (const animation of childrenOf(document, "animations")) {
|
|
52
|
-
for (const id of animation.fields.get("id") ?? []) {
|
|
53
|
-
if (id !== "") ids.add(id);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return ids;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface AssetIndex {
|
|
60
|
-
readonly animationsByTileSet: Map<string, Set<string>>;
|
|
61
|
-
readonly tileSetBySprite: Map<string, string>;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function readAssets(assets: ReadonlyMap<string, string>, unresolved: string[]): AssetIndex {
|
|
65
|
-
const animationsByTileSet = new Map<string, Set<string>>();
|
|
66
|
-
const tileSetBySprite = new Map<string, string>();
|
|
67
|
-
|
|
68
|
-
for (const [path, text] of assets) {
|
|
69
|
-
const displayPath = normalizeDocumentKey(path);
|
|
70
|
-
let document: SceneMessage;
|
|
71
|
-
try {
|
|
72
|
-
document = parseSceneTextFormat(text);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
75
|
-
unresolved.push(`${displayPath}: could not be parsed (${error.message})`);
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (displayPath.endsWith(SPRITE_SUFFIX)) {
|
|
79
|
-
const tileSet = firstField(document, "tile_set");
|
|
80
|
-
if (tileSet !== undefined) tileSetBySprite.set(displayPath, resourceKey(tileSet));
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
animationsByTileSet.set(displayPath, declaredAnimations(document));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return { animationsByTileSet, tileSetBySprite };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// The sprite components of one game object, as `id -> tile_set resource`. An
|
|
90
|
-
// embedded sprite carries its own payload; a referenced one names a `.sprite`
|
|
91
|
-
// document that carries it instead.
|
|
92
|
-
function spriteTileSets(
|
|
93
|
-
object: SceneMessage,
|
|
94
|
-
displayPath: string,
|
|
95
|
-
assets: AssetIndex,
|
|
96
|
-
unresolved: string[],
|
|
97
|
-
): Map<string, string> {
|
|
98
|
-
const tileSets = new Map<string, string>();
|
|
99
|
-
|
|
100
|
-
for (const embedded of childrenOf(object, "embedded_components")) {
|
|
101
|
-
if (firstField(embedded, "type") !== "sprite") continue;
|
|
102
|
-
const id = firstField(embedded, "id");
|
|
103
|
-
const payload = firstField(embedded, "data");
|
|
104
|
-
if (id === undefined || payload === undefined) continue;
|
|
105
|
-
try {
|
|
106
|
-
const tileSet = firstField(parseSceneTextFormat(payload), "tile_set");
|
|
107
|
-
if (tileSet !== undefined) tileSets.set(id, resourceKey(tileSet));
|
|
108
|
-
} catch (error) {
|
|
109
|
-
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
110
|
-
unresolved.push(
|
|
111
|
-
`${displayPath}: the embedded sprite "${id}" could not be read (${error.message})`,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
for (const referenced of childrenOf(object, "components")) {
|
|
117
|
-
const component = firstField(referenced, "component");
|
|
118
|
-
const id = firstField(referenced, "id");
|
|
119
|
-
if (id === undefined || component === undefined || !component.endsWith(SPRITE_SUFFIX)) continue;
|
|
120
|
-
const tileSet = assets.tileSetBySprite.get(resourceKey(component));
|
|
121
|
-
if (tileSet === undefined) {
|
|
122
|
-
unresolved.push(
|
|
123
|
-
`${displayPath}: the sprite component "${id}" names ${component}, which is not among the project's sprite documents`,
|
|
124
|
-
);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
tileSets.set(id, tileSet);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return tileSets;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function scriptResourcesOf(object: SceneMessage): string[] {
|
|
134
|
-
const resources: string[] = [];
|
|
135
|
-
for (const component of childrenOf(object, "components")) {
|
|
136
|
-
const resource = firstField(component, "component");
|
|
137
|
-
if (resource === undefined || !resource.endsWith(SCRIPT_SUFFIX)) continue;
|
|
138
|
-
const key = resourceKey(resource);
|
|
139
|
-
if (key !== SCRIPT_SUFFIX.slice(1)) resources.push(key);
|
|
140
|
-
}
|
|
141
|
-
return resources;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function isGameObject(message: SceneMessage): boolean {
|
|
145
|
-
return message.messages.has("components") || message.messages.has("embedded_components");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function buildSpriteAnimationIndex(input: {
|
|
149
|
-
scenes: ReadonlyMap<string, string>;
|
|
150
|
-
assets: ReadonlyMap<string, string>;
|
|
151
|
-
}): SpriteAnimationIndex {
|
|
152
|
-
const unresolved: string[] = [];
|
|
153
|
-
const assets = readAssets(input.assets, unresolved);
|
|
154
|
-
const byScriptResource = new Map<string, ReadonlyMap<string, ReadonlySet<string>>>();
|
|
155
|
-
const tileSetByScriptResource = new Map<string, ReadonlyMap<string, string>>();
|
|
156
|
-
const claimedBy = new Map<string, string>();
|
|
157
|
-
|
|
158
|
-
// A `.go` document is a game object at its root; a `.collection` carries one
|
|
159
|
-
// inside each `embedded_instances` `data:` payload, decoded exactly one level
|
|
160
|
-
// at a time. No `instances { prototype: }` edge is followed: the referenced
|
|
161
|
-
// `.go` is its own document and already claims its own script.
|
|
162
|
-
function walk(message: SceneMessage, blockName: string, displayPath: string): void {
|
|
163
|
-
if (isGameObject(message)) {
|
|
164
|
-
claim(message, displayPath);
|
|
165
|
-
}
|
|
166
|
-
if (blockName === "embedded_instances") {
|
|
167
|
-
for (const payload of message.fields.get("data") ?? []) {
|
|
168
|
-
try {
|
|
169
|
-
walk(parseSceneTextFormat(payload), "", displayPath);
|
|
170
|
-
} catch (error) {
|
|
171
|
-
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
172
|
-
unresolved.push(
|
|
173
|
-
`${displayPath}: an embedded game object could not be read (${error.message})`,
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
for (const [name, nested] of message.messages) {
|
|
179
|
-
for (const child of nested) {
|
|
180
|
-
walk(child, name, displayPath);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function claim(object: SceneMessage, displayPath: string): void {
|
|
186
|
-
const scripts = scriptResourcesOf(object);
|
|
187
|
-
if (scripts.length === 0) return;
|
|
188
|
-
|
|
189
|
-
const tileSets = spriteTileSets(object, displayPath, assets, unresolved);
|
|
190
|
-
const animations = new Map<string, ReadonlySet<string>>();
|
|
191
|
-
const declaringTileSets = new Map<string, string>();
|
|
192
|
-
for (const [id, tileSet] of tileSets) {
|
|
193
|
-
const declared = assets.animationsByTileSet.get(tileSet);
|
|
194
|
-
if (declared === undefined) {
|
|
195
|
-
unresolved.push(
|
|
196
|
-
`${displayPath}: the sprite component "${id}" names the tile source /${tileSet}, which is not among the project's asset documents`,
|
|
197
|
-
);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
animations.set(id, declared);
|
|
201
|
-
declaringTileSets.set(id, tileSet);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
for (const key of scripts) {
|
|
205
|
-
const owner = claimedBy.get(key);
|
|
206
|
-
if (owner !== undefined) {
|
|
207
|
-
byScriptResource.delete(key);
|
|
208
|
-
tileSetByScriptResource.delete(key);
|
|
209
|
-
unresolved.push(
|
|
210
|
-
`${key}: claimed by both ${owner} and ${displayPath}, so its sprite animations are ambiguous`,
|
|
211
|
-
);
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
claimedBy.set(key, displayPath);
|
|
215
|
-
byScriptResource.set(key, animations);
|
|
216
|
-
tileSetByScriptResource.set(key, declaringTileSets);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
for (const [path, text] of input.scenes) {
|
|
221
|
-
const displayPath = normalizeDocumentKey(path);
|
|
222
|
-
try {
|
|
223
|
-
walk(parseSceneTextFormat(text), "", displayPath);
|
|
224
|
-
} catch (error) {
|
|
225
|
-
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
226
|
-
unresolved.push(`${displayPath}: could not be parsed (${error.message})`);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return { byScriptResource, tileSetByScriptResource, unresolved };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// The component id a same-object `"#id"` address names, or `undefined` for
|
|
234
|
-
// every other form. A path form names a component on a *different* game
|
|
235
|
-
// object, so its id must never be resolved against this script's own.
|
|
236
|
-
export function componentIdOfSameObjectAddress(address: string): string | undefined {
|
|
237
|
-
if (!address.startsWith("#")) return undefined;
|
|
238
|
-
const id = address.slice(1);
|
|
239
|
-
if (id === "" || id.includes("/") || id.includes("#")) return undefined;
|
|
240
|
-
return id;
|
|
241
|
-
}
|