@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
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { readGameProjectSetting } from "./config-key-index";
|
|
2
|
+
import { parseSceneTextFormat, type SceneMessage, SceneTextFormatError } from "./scene-text-format";
|
|
3
|
+
|
|
4
|
+
// Which world each collection is, so an address can say where it resolves. A
|
|
5
|
+
// game object's address has two axes — which world, then which path inside it —
|
|
6
|
+
// and only these roles can tell them apart. Shaped like the indexes it feeds: a
|
|
7
|
+
// non-empty `incomplete` means the classification is not provably whole, so no
|
|
8
|
+
// consumer may conclude a collection has no role.
|
|
9
|
+
export interface SceneCollectionRoles {
|
|
10
|
+
/** The one collection `[bootstrap] main_collection` names, whose paths are bare. */
|
|
11
|
+
readonly bootstrap?: string | undefined;
|
|
12
|
+
/** Proxied collections, mapped to the socket `CollectionDesc.name` gives them. */
|
|
13
|
+
readonly sockets: ReadonlyMap<string, string>;
|
|
14
|
+
/** Collections another collection instances, whose paths exist only under that id. */
|
|
15
|
+
readonly instanced: ReadonlySet<string>;
|
|
16
|
+
/** Collection-factory prototypes, whose objects have no static path at all. */
|
|
17
|
+
readonly factoryPrototypes: ReadonlySet<string>;
|
|
18
|
+
readonly incomplete: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SceneCollectionRolesInput {
|
|
22
|
+
/** The `.go`/`.collection` universe, keyed by display path. */
|
|
23
|
+
readonly documents: ReadonlyMap<string, string>;
|
|
24
|
+
/** The `.collectionproxy`/`.collectionfactory` documents, keyed by display path. */
|
|
25
|
+
readonly references: ReadonlyMap<string, string>;
|
|
26
|
+
/** `game.project`'s text, or `undefined` when the walk could not read it. */
|
|
27
|
+
readonly gameProject: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The two places Defold embeds a whole escaped document in `data:`, the same
|
|
31
|
+
// rule `buildSceneComponentIndex` states — every other `data:` is a plain scalar.
|
|
32
|
+
const EMBEDDING_BLOCKS = new Set(["embedded_instances", "embedded_components"]);
|
|
33
|
+
|
|
34
|
+
// A component reference and the field its document holds the target under: a
|
|
35
|
+
// proxy names the collection it opens as a world, a factory names the prototype
|
|
36
|
+
// it spawns copies of.
|
|
37
|
+
const REFERENCE_KINDS = [
|
|
38
|
+
{ extension: ".collectionproxy", type: "collectionproxy", field: "collection" },
|
|
39
|
+
{ extension: ".collectionfactory", type: "collectionfactory", field: "prototype" },
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
type ReferenceKind = (typeof REFERENCE_KINDS)[number];
|
|
43
|
+
|
|
44
|
+
// Matches `scene-object-path-index.ts`: resource paths inside a scene are
|
|
45
|
+
// project-absolute, documents are keyed by project-relative display path.
|
|
46
|
+
function resourceKey(resource: string): string {
|
|
47
|
+
return resource.startsWith("/") ? resource.slice(1) : resource;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// `game.project` and a `.collectionproxy` name the *compiled* resource the
|
|
51
|
+
// engine loads (`/main/main.collectionc`), not the source the walk read — the
|
|
52
|
+
// same normalisation `resolveBootPathScripts` applies to its own boot walk.
|
|
53
|
+
// Without it every real project fails to find its bootstrap world.
|
|
54
|
+
function collectionKey(resource: string): string {
|
|
55
|
+
return resourceKey(resource).replace(/\.collectionc$/, ".collection");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function firstField(message: SceneMessage, name: string): string | undefined {
|
|
59
|
+
return (message.fields.get(name) ?? [])[0];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Only a `.collection` is a world. A `.go` is a prototype, so running the role
|
|
63
|
+
// rules over it would make every prototype in the project a role-less
|
|
64
|
+
// "reachable no way" entry and drown the honest ones.
|
|
65
|
+
function isCollection(displayPath: string): boolean {
|
|
66
|
+
return displayPath.endsWith(".collection");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Classify every collection the project declares into the world it is: the one
|
|
71
|
+
* bootstrap root, a proxy world addressed through its own socket, a collection
|
|
72
|
+
* instanced inside another, or a factory prototype with no static address.
|
|
73
|
+
*
|
|
74
|
+
* Pure, like the indexes it feeds — the filesystem walk belongs to the caller.
|
|
75
|
+
* There is deliberately no everything-else-is-a-root fallback: that fallback is
|
|
76
|
+
* what offers a bare `/enemy` for an object living in another world, and losing
|
|
77
|
+
* a completion is safe where offering a false one is not.
|
|
78
|
+
*/
|
|
79
|
+
export function buildSceneCollectionRoles(input: SceneCollectionRolesInput): SceneCollectionRoles {
|
|
80
|
+
const { documents, references, gameProject } = input;
|
|
81
|
+
const incomplete: string[] = [];
|
|
82
|
+
|
|
83
|
+
const parsed = new Map<string, SceneMessage>();
|
|
84
|
+
for (const [displayPath, text] of documents) {
|
|
85
|
+
try {
|
|
86
|
+
parsed.set(displayPath, parseSceneTextFormat(text));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
89
|
+
incomplete.push(`${displayPath}: could not be parsed (${error.message})`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const instanced = new Set<string>();
|
|
94
|
+
for (const document of parsed.values()) {
|
|
95
|
+
for (const block of document.messages.get("collection_instances") ?? []) {
|
|
96
|
+
const collection = firstField(block, "collection");
|
|
97
|
+
if (collection !== undefined) instanced.add(resourceKey(collection));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const proxied = new Set<string>();
|
|
102
|
+
const factoryPrototypes = new Set<string>();
|
|
103
|
+
|
|
104
|
+
function record(kind: ReferenceKind, resource: string): void {
|
|
105
|
+
(kind.type === "collectionproxy" ? proxied : factoryPrototypes).add(collectionKey(resource));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A standalone reference is a component file of its own; its target lives in
|
|
109
|
+
// the reference document rather than in the scene that points at it, so a
|
|
110
|
+
// reference the walk never read is a named gap and not a dropped edge.
|
|
111
|
+
function followReferenceDocument(displayPath: string, resource: string): void {
|
|
112
|
+
const kind = REFERENCE_KINDS.find((candidate) => resource.endsWith(candidate.extension));
|
|
113
|
+
if (kind === undefined) return;
|
|
114
|
+
const text = references.get(resourceKey(resource));
|
|
115
|
+
if (text === undefined) {
|
|
116
|
+
incomplete.push(
|
|
117
|
+
`${displayPath}: the component ${resource} is not among the project's readable ${kind.extension} documents, so the collection it names is unclassified`,
|
|
118
|
+
);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let document: SceneMessage;
|
|
122
|
+
try {
|
|
123
|
+
document = parseSceneTextFormat(text);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
126
|
+
incomplete.push(`${resourceKey(resource)}: could not be parsed (${error.message})`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const target = firstField(document, kind.field);
|
|
130
|
+
if (target === undefined) {
|
|
131
|
+
incomplete.push(
|
|
132
|
+
`${resourceKey(resource)}: declares no ${kind.field}:, so the collection it names is unclassified`,
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
record(kind, target);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function walk(
|
|
140
|
+
message: SceneMessage,
|
|
141
|
+
blockName: string,
|
|
142
|
+
displayPath: string,
|
|
143
|
+
depth: number,
|
|
144
|
+
): void {
|
|
145
|
+
if (blockName === "components") {
|
|
146
|
+
const component = firstField(message, "component");
|
|
147
|
+
if (component !== undefined) followReferenceDocument(displayPath, component);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const payloads: SceneMessage[] = [];
|
|
151
|
+
if (EMBEDDING_BLOCKS.has(blockName)) {
|
|
152
|
+
for (const payload of message.fields.get("data") ?? []) {
|
|
153
|
+
try {
|
|
154
|
+
payloads.push(parseSceneTextFormat(payload));
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
157
|
+
incomplete.push(
|
|
158
|
+
`${displayPath}: embedded payload at depth ${depth + 1} could not be parsed (${error.message})`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (blockName === "embedded_components") {
|
|
165
|
+
const type = firstField(message, "type");
|
|
166
|
+
const kind = REFERENCE_KINDS.find((candidate) => candidate.type === type);
|
|
167
|
+
if (kind !== undefined) {
|
|
168
|
+
for (const payload of payloads) {
|
|
169
|
+
const target = firstField(payload, kind.field);
|
|
170
|
+
if (target === undefined) {
|
|
171
|
+
incomplete.push(
|
|
172
|
+
`${displayPath}: the embedded ${kind.type} declares no ${kind.field}:, so the collection it names is unclassified`,
|
|
173
|
+
);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
record(kind, target);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const payload of payloads) walk(payload, "", displayPath, depth + 1);
|
|
182
|
+
for (const [name, nested] of message.messages) {
|
|
183
|
+
for (const child of nested) walk(child, name, displayPath, depth);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const [displayPath, document] of parsed) walk(document, "", displayPath, 0);
|
|
188
|
+
|
|
189
|
+
// The socket is the proxied collection's own `name:` — `CollectionDesc.name`,
|
|
190
|
+
// which the editor writes on the collection itself and which is deliberately
|
|
191
|
+
// neither the proxy component's id nor the file's basename.
|
|
192
|
+
const sockets = new Map<string, string>();
|
|
193
|
+
const byName = new Map<string, string[]>();
|
|
194
|
+
for (const displayPath of [...proxied].sort()) {
|
|
195
|
+
const document = parsed.get(displayPath);
|
|
196
|
+
if (document === undefined) {
|
|
197
|
+
incomplete.push(
|
|
198
|
+
`${displayPath}: is opened as a collection proxy but is not among the project's readable scene documents, so its world has no name`,
|
|
199
|
+
);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const name = firstField(document, "name");
|
|
203
|
+
if (name === undefined || name === "") {
|
|
204
|
+
incomplete.push(
|
|
205
|
+
`${displayPath}: is opened as a collection proxy but declares no name:, so its world cannot be addressed`,
|
|
206
|
+
);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
sockets.set(displayPath, name);
|
|
210
|
+
byName.set(name, [...(byName.get(name) ?? []), displayPath]);
|
|
211
|
+
}
|
|
212
|
+
for (const [name, holders] of byName) {
|
|
213
|
+
if (holders.length > 1) {
|
|
214
|
+
incomplete.push(
|
|
215
|
+
`${holders.join(", ")}: all declare name: "${name}", so an address under that socket does not say which world it means`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// A project the walk found no collection in has no world to name, so an
|
|
221
|
+
// absent `[bootstrap]` says nothing a reader could act on — the same posture
|
|
222
|
+
// `sceneIndexForBuild` takes towards a scene-less project. A project that does
|
|
223
|
+
// have collections is a different case: there the absence is exactly why no
|
|
224
|
+
// address is offered bare, so it is always named.
|
|
225
|
+
const hasCollections = [...parsed.keys()].some(isCollection);
|
|
226
|
+
|
|
227
|
+
let bootstrap: string | undefined;
|
|
228
|
+
if (gameProject === undefined) {
|
|
229
|
+
if (hasCollections) {
|
|
230
|
+
incomplete.push(
|
|
231
|
+
"game.project could not be read, so no collection is known to be the bootstrap world and no address is offered unqualified",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
const main = readGameProjectSetting(gameProject, "bootstrap", "main_collection");
|
|
236
|
+
if (main === undefined) {
|
|
237
|
+
if (hasCollections) {
|
|
238
|
+
incomplete.push(
|
|
239
|
+
"game.project declares no [bootstrap] main_collection, so no collection is the bootstrap world",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
} else if (!parsed.has(collectionKey(main))) {
|
|
243
|
+
incomplete.push(
|
|
244
|
+
`game.project names ${main} as [bootstrap] main_collection, which is not among the project's readable scene documents`,
|
|
245
|
+
);
|
|
246
|
+
} else {
|
|
247
|
+
bootstrap = collectionKey(main);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const displayPath of parsed.keys()) {
|
|
252
|
+
if (!isCollection(displayPath)) continue;
|
|
253
|
+
if (
|
|
254
|
+
displayPath === bootstrap ||
|
|
255
|
+
sockets.has(displayPath) ||
|
|
256
|
+
instanced.has(displayPath) ||
|
|
257
|
+
factoryPrototypes.has(displayPath)
|
|
258
|
+
) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
incomplete.push(
|
|
262
|
+
`${displayPath}: is reached by no bootstrap, proxy, instance or factory reference, so which world its objects live in is unknown`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { bootstrap, sockets, instanced, factoryPrototypes, incomplete };
|
|
267
|
+
}
|
|
@@ -20,23 +20,56 @@ const COMPONENT_BLOCKS = new Set(["components", "embedded_components"]);
|
|
|
20
20
|
// projects as unreadable and suppress the check for good.
|
|
21
21
|
const EMBEDDING_BLOCKS = new Set(["embedded_instances", "embedded_components"]);
|
|
22
22
|
|
|
23
|
+
// A component id and the resource it references, where one is referenced at all.
|
|
24
|
+
// The two axes are gathered by one pass because they are read off the same
|
|
25
|
+
// block: a second walk over the same documents is exactly how the id universe
|
|
26
|
+
// and the resource universe drift apart.
|
|
27
|
+
export interface ComponentDeclarations {
|
|
28
|
+
readonly ids: Set<string>;
|
|
29
|
+
readonly resources: Map<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Resource paths inside a scene are project-absolute; documents are keyed by
|
|
33
|
+
// project-relative display path, the same convention `resourceKey` follows in
|
|
34
|
+
// `scene-object-path-index.ts`.
|
|
35
|
+
function resourceKey(resource: string): string {
|
|
36
|
+
return resource.startsWith("/") ? resource.slice(1) : resource;
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
function collect(
|
|
24
40
|
message: SceneMessage,
|
|
25
41
|
blockName: string,
|
|
26
42
|
depth: number,
|
|
27
43
|
displayPath: string,
|
|
28
|
-
|
|
44
|
+
declarations: ComponentDeclarations,
|
|
29
45
|
incomplete: string[],
|
|
30
46
|
): void {
|
|
31
47
|
if (COMPONENT_BLOCKS.has(blockName)) {
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
const declared = message.fields.get("id") ?? [];
|
|
49
|
+
for (const id of declared) {
|
|
50
|
+
declarations.ids.add(id);
|
|
51
|
+
}
|
|
52
|
+
// `embedded_components` is skipped on purpose: its payload *is* the
|
|
53
|
+
// component, so it names no resource and must never be credited with one.
|
|
54
|
+
if (blockName === "components") {
|
|
55
|
+
const [id] = declared;
|
|
56
|
+
const [component] = message.fields.get("component") ?? [];
|
|
57
|
+
if (id !== undefined && id !== "" && component !== undefined) {
|
|
58
|
+
declarations.resources.set(id, resourceKey(component));
|
|
59
|
+
}
|
|
34
60
|
}
|
|
35
61
|
}
|
|
36
62
|
if (EMBEDDING_BLOCKS.has(blockName)) {
|
|
37
63
|
for (const payload of message.fields.get("data") ?? []) {
|
|
38
64
|
try {
|
|
39
|
-
collect(
|
|
65
|
+
collect(
|
|
66
|
+
parseSceneTextFormat(payload),
|
|
67
|
+
"",
|
|
68
|
+
depth + 1,
|
|
69
|
+
displayPath,
|
|
70
|
+
declarations,
|
|
71
|
+
incomplete,
|
|
72
|
+
);
|
|
40
73
|
} catch (error) {
|
|
41
74
|
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
42
75
|
incomplete.push(
|
|
@@ -47,11 +80,44 @@ function collect(
|
|
|
47
80
|
}
|
|
48
81
|
for (const [name, nested] of message.messages) {
|
|
49
82
|
for (const child of nested) {
|
|
50
|
-
collect(child, name, depth, displayPath,
|
|
83
|
+
collect(child, name, depth, displayPath, declarations, incomplete);
|
|
51
84
|
}
|
|
52
85
|
}
|
|
53
86
|
}
|
|
54
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Every component id one already-parsed scene message declares, plus the
|
|
90
|
+
* resource each *referenced* component names, following the two `data:`
|
|
91
|
+
* payloads Defold embeds a whole document in. Exported because the game-object
|
|
92
|
+
* path walk attributes components to the object that owns them and must read
|
|
93
|
+
* the same rule this index reads — a second collector is exactly how the two
|
|
94
|
+
* universes drift apart.
|
|
95
|
+
*
|
|
96
|
+
* `blockName` names the block `message` was taken from, because the `data:`
|
|
97
|
+
* re-parse is keyed off it: a whole document is `""`, while a single
|
|
98
|
+
* `embedded_instances` block has to say so or its payload is never opened.
|
|
99
|
+
*/
|
|
100
|
+
export function collectComponentDeclarations(
|
|
101
|
+
message: SceneMessage,
|
|
102
|
+
displayPath: string,
|
|
103
|
+
incomplete: string[],
|
|
104
|
+
blockName = "",
|
|
105
|
+
): ComponentDeclarations {
|
|
106
|
+
const declarations: ComponentDeclarations = { ids: new Set(), resources: new Map() };
|
|
107
|
+
collect(message, blockName, 0, displayPath, declarations, incomplete);
|
|
108
|
+
return declarations;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The id half of {@link collectComponentDeclarations}, for callers that join no resources. */
|
|
112
|
+
export function collectComponentIds(
|
|
113
|
+
message: SceneMessage,
|
|
114
|
+
displayPath: string,
|
|
115
|
+
incomplete: string[],
|
|
116
|
+
blockName = "",
|
|
117
|
+
): Set<string> {
|
|
118
|
+
return collectComponentDeclarations(message, displayPath, incomplete, blockName).ids;
|
|
119
|
+
}
|
|
120
|
+
|
|
55
121
|
// Build the component-id universe from already-read scene sources: keys are
|
|
56
122
|
// display paths, values are file text. Pure — the filesystem walk belongs to the
|
|
57
123
|
// caller, so a test can drive this from inline strings and a build can drive it
|
|
@@ -69,7 +135,9 @@ export function buildSceneComponentIndex(
|
|
|
69
135
|
|
|
70
136
|
for (const [displayPath, text] of documents) {
|
|
71
137
|
try {
|
|
72
|
-
|
|
138
|
+
for (const id of collectComponentIds(parseSceneTextFormat(text), displayPath, incomplete)) {
|
|
139
|
+
ids.add(id);
|
|
140
|
+
}
|
|
73
141
|
} catch (error) {
|
|
74
142
|
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
75
143
|
incomplete.push(`${displayPath}: could not be parsed (${error.message})`);
|
package/src/scene-documents.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readLibrarySceneDocuments } from "./library-dependencies";
|
|
1
2
|
import { isDefignoredPath } from "./project-resources";
|
|
2
3
|
|
|
3
4
|
// The narrow slice of `ts.server.ServerHost` this needs: the real host
|
|
@@ -18,9 +19,42 @@ export const SCENE_EXTENSIONS = [".go", ".collection"];
|
|
|
18
19
|
export const GUI_EXTENSIONS = [".gui"];
|
|
19
20
|
export const INPUT_BINDING_EXTENSIONS = [".input_binding"];
|
|
20
21
|
|
|
21
|
-
// The
|
|
22
|
-
//
|
|
23
|
-
|
|
22
|
+
// The component files that name a collection without instancing it: a
|
|
23
|
+
// `.collectionproxy` opens it as its own world, a `.collectionfactory` spawns
|
|
24
|
+
// copies of it at runtime. Deliberately not folded into `SCENE_EXTENSIONS`,
|
|
25
|
+
// which would feed non-scene text to `buildSceneComponentIndex`.
|
|
26
|
+
export const COLLECTION_REFERENCE_EXTENSIONS = [".collectionproxy", ".collectionfactory"];
|
|
27
|
+
|
|
28
|
+
// The documents an animation id is declared by, plus every hop taken to reach
|
|
29
|
+
// one: an atlas or tile source names a sprite's ids outright, a `.animationset`
|
|
30
|
+
// names the files whose basenames are a model's ids, and a `.sprite` or
|
|
31
|
+
// `.model` is the hop a referenced component takes to reach either.
|
|
32
|
+
export const ANIMATION_ASSET_EXTENSIONS = [
|
|
33
|
+
".atlas",
|
|
34
|
+
".tilesource",
|
|
35
|
+
".sprite",
|
|
36
|
+
".model",
|
|
37
|
+
".animationset",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// The file kinds a library shares into the address universe: the documents that
|
|
41
|
+
// declare ids, plus the ones that *classify* a collection into the world it is —
|
|
42
|
+
// a library's own `.collectionproxy` is what says its collection is a proxy
|
|
43
|
+
// world rather than reached by nothing. `.input_binding` and `.project` are
|
|
44
|
+
// deliberately absent: `game.project` names exactly one active binding file, and
|
|
45
|
+
// a vendored `*.project` declares keys this project's readers cannot resolve —
|
|
46
|
+
// the rule `PROJECT_EXTENSIONS` states below.
|
|
47
|
+
//
|
|
48
|
+
// It lives here rather than beside its reader for the reason the sets above do,
|
|
49
|
+
// plus one more: `library-dependencies.ts` and this file import each other, and
|
|
50
|
+
// a constant built from these arrays at that module's top level would read them
|
|
51
|
+
// before this one had run.
|
|
52
|
+
export const LIBRARY_SCENE_EXTENSIONS = [
|
|
53
|
+
...SCENE_EXTENSIONS,
|
|
54
|
+
...GUI_EXTENSIONS,
|
|
55
|
+
...ANIMATION_ASSET_EXTENSIONS,
|
|
56
|
+
...COLLECTION_REFERENCE_EXTENSIONS,
|
|
57
|
+
];
|
|
24
58
|
|
|
25
59
|
// The walk returns every `.project` the project holds, so the lookup is by
|
|
26
60
|
// display path — a vendored `*.project` declares keys this project's readers
|
|
@@ -70,18 +104,32 @@ export function isExcludedProjectPath(displayPath: string): boolean {
|
|
|
70
104
|
// silent hole cannot be reasoned about. `paths` names the host paths actually
|
|
71
105
|
// read — what a watcher must be registered on, which cannot be reconstructed
|
|
72
106
|
// from a display path for a file outside the project root.
|
|
107
|
+
//
|
|
108
|
+
// The universe is the project plus whatever `resolve` last materialized from its
|
|
109
|
+
// library dependencies: a library document joins it under the merged Defold
|
|
110
|
+
// resource path it is addressed by, with its origin recorded in `origins` rather
|
|
111
|
+
// than folded into the key, and the project's own file wins a collision. Which
|
|
112
|
+
// makes the universe only as current as the last `resolve` — so a dependency
|
|
113
|
+
// that is declared but unresolved, unreadable or shadowed is a named
|
|
114
|
+
// `unreadable` line, never a silent omission.
|
|
73
115
|
export function readSceneDocuments(
|
|
74
116
|
host: SceneReadHost,
|
|
75
117
|
projectRoot: string,
|
|
76
118
|
extensions: readonly string[] = SCENE_EXTENSIONS,
|
|
77
|
-
): {
|
|
119
|
+
): {
|
|
120
|
+
documents: Map<string, string>;
|
|
121
|
+
origins: Map<string, string>;
|
|
122
|
+
unreadable: string[];
|
|
123
|
+
paths: string[];
|
|
124
|
+
} {
|
|
78
125
|
const documents = new Map<string, string>();
|
|
126
|
+
const origins = new Map<string, string>();
|
|
79
127
|
const unreadable: string[] = [];
|
|
80
128
|
const paths: string[] = [];
|
|
81
129
|
|
|
82
130
|
if (!host.readDirectory) {
|
|
83
131
|
unreadable.push("the editor host cannot enumerate project files, so no scene source was read");
|
|
84
|
-
return { documents, unreadable, paths };
|
|
132
|
+
return { documents, origins, unreadable, paths };
|
|
85
133
|
}
|
|
86
134
|
|
|
87
135
|
for (const filePath of host.readDirectory(projectRoot, extensions)) {
|
|
@@ -96,7 +144,19 @@ export function readSceneDocuments(
|
|
|
96
144
|
paths.push(filePath);
|
|
97
145
|
}
|
|
98
146
|
|
|
99
|
-
|
|
147
|
+
const library = readLibrarySceneDocuments(host, projectRoot, extensions);
|
|
148
|
+
unreadable.push(...library.unreadable);
|
|
149
|
+
for (const { displayPath, hostPath, url, text } of library.documents) {
|
|
150
|
+
if (documents.has(displayPath)) {
|
|
151
|
+
unreadable.push(`${displayPath}: also declared by ${url}; the project's own file is used`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
documents.set(displayPath, text);
|
|
155
|
+
origins.set(displayPath, url);
|
|
156
|
+
paths.push(hostPath);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { documents, origins, unreadable, paths };
|
|
100
160
|
}
|
|
101
161
|
|
|
102
162
|
// The project's own files of the given kinds, as the `/`-prefixed
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { SceneObjectPathIndex } from "./scene-object-path-index";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One place a script runs, and the addressing frame it runs in. At runtime an
|
|
5
|
+
* instance carries `m_CollectionPathHashState` seeded from its compiled id up
|
|
6
|
+
* to and including the last `/`, and `GetAbsoluteIdentifier` resolves a
|
|
7
|
+
* relative string by continuing that hash — so a relative address written in a
|
|
8
|
+
* script means one thing from this object and nothing anywhere else.
|
|
9
|
+
*/
|
|
10
|
+
export interface NamingContext {
|
|
11
|
+
/** The world-qualified composed path of the object hosting the script. */
|
|
12
|
+
readonly object: string;
|
|
13
|
+
/** The proxy socket this object lives behind, or `undefined` in the bootstrap world. */
|
|
14
|
+
readonly socket: string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* `object` through its last `/`, inclusive — the collection path a relative
|
|
17
|
+
* address continues. World-qualified by construction, so a relative address
|
|
18
|
+
* can never resolve across a world.
|
|
19
|
+
*/
|
|
20
|
+
readonly prefix: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SCRIPT_SUFFIX = ".script";
|
|
24
|
+
const GUI_SUFFIX = ".gui";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The proxy socket an address names, or `undefined` when it names none — a
|
|
28
|
+
* bootstrap-world path, a relative path, a bare `#fragment`.
|
|
29
|
+
*
|
|
30
|
+
* Defold's address grammar is `[socket:][path][#fragment]` with both bracketed
|
|
31
|
+
* halves optional, so `mylevel:`, `mylevel:enemy` and `mylevel:#body` are as
|
|
32
|
+
* socket-qualified as `mylevel:/path`. The socket is therefore read from the
|
|
33
|
+
* `:` delimiter rather than from a following `/`: the head is the address up to
|
|
34
|
+
* the first `/` or `#` (the whole address when it carries neither), and the
|
|
35
|
+
* socket is the text before that head's first `:`. A `:` inside a path segment
|
|
36
|
+
* (`/level:1/enemy`) is not a socket, because the `/` closes the head first,
|
|
37
|
+
* and an empty socket (`:path`) is none.
|
|
38
|
+
*
|
|
39
|
+
* Exported because a written address literal and a composed object path are the
|
|
40
|
+
* same shape: the cross-world check reads a socket out of source text with this
|
|
41
|
+
* one rule rather than a second reader that could disagree with the contexts it
|
|
42
|
+
* compares against.
|
|
43
|
+
*/
|
|
44
|
+
export function socketOfAddress(address: string): string | undefined {
|
|
45
|
+
const pathStart = address.indexOf("/");
|
|
46
|
+
const fragmentStart = address.indexOf("#");
|
|
47
|
+
const ends = [pathStart, fragmentStart, address.length].filter((end) => end >= 0);
|
|
48
|
+
const headEnd = Math.min(...ends);
|
|
49
|
+
const delimiter = address.indexOf(":");
|
|
50
|
+
return delimiter > 0 && delimiter < headEnd ? address.slice(0, delimiter) : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function contextOf(object: string): NamingContext {
|
|
54
|
+
return {
|
|
55
|
+
object,
|
|
56
|
+
socket: socketOfAddress(object),
|
|
57
|
+
prefix: object.slice(0, object.lastIndexOf("/") + 1),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Every naming context each script resource runs in, keyed by that resource and
|
|
63
|
+
* sorted by object path.
|
|
64
|
+
*
|
|
65
|
+
* `guiScripts` maps a `.gui` display path to the gui-script resource it names,
|
|
66
|
+
* which the caller builds with `guiScriptResourceOf` — the walk needs no `.gui`
|
|
67
|
+
* documents of its own.
|
|
68
|
+
*
|
|
69
|
+
* A script hosted by several objects has several contexts, and that is not
|
|
70
|
+
* ambiguity: a `.go` instanced in two collections genuinely runs in both, and
|
|
71
|
+
* it is the consumer's provenance that tells an author which context an entry
|
|
72
|
+
* resolves from.
|
|
73
|
+
*/
|
|
74
|
+
export function buildScriptNamingContexts(
|
|
75
|
+
index: SceneObjectPathIndex,
|
|
76
|
+
guiScripts: ReadonlyMap<string, string>,
|
|
77
|
+
): ReadonlyMap<string, readonly NamingContext[]> {
|
|
78
|
+
const contexts = new Map<string, NamingContext[]>();
|
|
79
|
+
|
|
80
|
+
for (const [object, resources] of index.componentResourcesOf) {
|
|
81
|
+
for (const resource of resources.values()) {
|
|
82
|
+
const script = resource.endsWith(SCRIPT_SUFFIX)
|
|
83
|
+
? resource
|
|
84
|
+
: resource.endsWith(GUI_SUFFIX)
|
|
85
|
+
? guiScripts.get(resource)
|
|
86
|
+
: undefined;
|
|
87
|
+
if (script === undefined) continue;
|
|
88
|
+
const hosted = contexts.get(script);
|
|
89
|
+
if (hosted === undefined) {
|
|
90
|
+
contexts.set(script, [contextOf(object)]);
|
|
91
|
+
} else if (!hosted.some((context) => context.object === object)) {
|
|
92
|
+
hosted.push(contextOf(object));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const hosted of contexts.values()) {
|
|
98
|
+
hosted.sort((a, b) => a.object.localeCompare(b.object));
|
|
99
|
+
}
|
|
100
|
+
return contexts;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The relative forms a script may write from one naming context: every object
|
|
105
|
+
* path under the context's prefix with that prefix stripped, and each of those
|
|
106
|
+
* joined to the components its object owns.
|
|
107
|
+
*
|
|
108
|
+
* An object `componentsOf` withheld still contributes its path — the object is
|
|
109
|
+
* provably there — but no `#component` join, because which components it owns
|
|
110
|
+
* was never read.
|
|
111
|
+
*/
|
|
112
|
+
export function relativeAddressesFrom(
|
|
113
|
+
index: SceneObjectPathIndex,
|
|
114
|
+
context: NamingContext,
|
|
115
|
+
): { readonly paths: ReadonlySet<string>; readonly addresses: ReadonlySet<string> } {
|
|
116
|
+
const paths = new Set<string>();
|
|
117
|
+
const addresses = new Set<string>();
|
|
118
|
+
|
|
119
|
+
for (const key of index.paths) {
|
|
120
|
+
if (!key.startsWith(context.prefix)) continue;
|
|
121
|
+
const relative = key.slice(context.prefix.length);
|
|
122
|
+
if (relative === "") continue;
|
|
123
|
+
paths.add(relative);
|
|
124
|
+
for (const id of index.componentsOf.get(key) ?? []) {
|
|
125
|
+
addresses.add(`${relative}#${id}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { paths, addresses };
|
|
130
|
+
}
|