@defold-typescript/transpiler 0.24.0 → 0.25.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/config-key-index.d.ts +1 -0
- package/dist/editor-script-erasure.d.ts +2 -1
- package/dist/gui-node-index.d.ts +5 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +915 -27
- package/dist/input-action-index.d.ts +1 -0
- package/dist/output-paths.d.ts +11 -0
- package/dist/project-resources.d.ts +2 -0
- package/dist/scene-component-index.d.ts +5 -0
- package/dist/scene-object-path-index.d.ts +6 -0
- package/dist/scene-text-format.d.ts +9 -0
- package/dist/sprite-animation-index.d.ts +10 -0
- package/dist/url-address-slots.d.ts +19 -0
- package/dist/url-fragment-reachability.d.ts +23 -0
- package/package.json +2 -2
- package/src/config-key-index.ts +34 -0
- package/src/editor-script-erasure.ts +44 -13
- package/src/gui-node-index.ts +71 -0
- package/src/index.ts +33 -0
- package/src/input-action-index.ts +41 -0
- package/src/output-paths.ts +69 -0
- package/src/project-resources.ts +20 -0
- package/src/scene-component-index.ts +80 -0
- package/src/scene-object-path-index.ts +148 -0
- package/src/scene-text-format.ts +171 -0
- package/src/sprite-animation-index.ts +241 -0
- package/src/transpile.ts +5 -0
- package/src/url-address-slots.ts +290 -0
- package/src/url-fragment-reachability.ts +175 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { parseSceneTextFormat, type SceneMessage, SceneTextFormatError } from "./scene-text-format";
|
|
2
|
+
|
|
3
|
+
// Every game-object path the project declares, `/`-prefixed and composed the way
|
|
4
|
+
// Defold composes them, plus an honest record of everything the walk could not
|
|
5
|
+
// settle. Shaped like `SceneComponentIndex`: a non-empty `incomplete` means the
|
|
6
|
+
// universe is not provably whole, so no consumer may conclude a path is absent.
|
|
7
|
+
export interface SceneObjectPathIndex {
|
|
8
|
+
readonly paths: ReadonlySet<string>;
|
|
9
|
+
// The documents declaring each path's *leaf* segment, sorted — the files an
|
|
10
|
+
// author would open to rename that object. A composed address is attributed to
|
|
11
|
+
// the collection that names the object, never to the ones that prefixed it.
|
|
12
|
+
readonly declaredIn: ReadonlyMap<string, readonly string[]>;
|
|
13
|
+
readonly incomplete: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The two blocks whose `id` is a leaf segment. `collection_instances` is handled
|
|
17
|
+
// apart, because its id is a namespace rather than an object.
|
|
18
|
+
const LEAF_BLOCKS = ["instances", "embedded_instances"];
|
|
19
|
+
|
|
20
|
+
// Resource paths inside a scene are project-absolute; documents are keyed by
|
|
21
|
+
// project-relative display path, the same convention `resourceKey` follows in
|
|
22
|
+
// `sprite-animation-index.ts`.
|
|
23
|
+
function resourceKey(resource: string): string {
|
|
24
|
+
return resource.startsWith("/") ? resource.slice(1) : resource;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function firstField(message: SceneMessage, name: string): string | undefined {
|
|
28
|
+
return (message.fields.get(name) ?? [])[0];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function childrenOf(message: SceneMessage, name: string): readonly SceneMessage[] {
|
|
32
|
+
return message.messages.get(name) ?? [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Build the game-object path universe from already-read scene sources: keys are
|
|
36
|
+
// display paths, values are file text. Pure the same way `buildSceneComponentIndex`
|
|
37
|
+
// is — the filesystem walk belongs to the caller.
|
|
38
|
+
//
|
|
39
|
+
// A `children:` edge is deliberately not a path segment. Defold ids are unique
|
|
40
|
+
// inside one collection and a child object is still addressed `/child`, so
|
|
41
|
+
// parenting is a transform relation; nesting comes from a collection instanced
|
|
42
|
+
// inside another.
|
|
43
|
+
export function buildSceneObjectPathIndex(
|
|
44
|
+
documents: ReadonlyMap<string, string>,
|
|
45
|
+
): SceneObjectPathIndex {
|
|
46
|
+
const incomplete: string[] = [];
|
|
47
|
+
|
|
48
|
+
if (documents.size === 0) {
|
|
49
|
+
incomplete.push("no scene sources were read, so no game-object path can be proven absent");
|
|
50
|
+
return { paths: new Set(), declaredIn: new Map(), incomplete };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parsed = new Map<string, SceneMessage>();
|
|
54
|
+
for (const [displayPath, text] of documents) {
|
|
55
|
+
try {
|
|
56
|
+
parsed.set(displayPath, parseSceneTextFormat(text));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (!(error instanceof SceneTextFormatError)) throw error;
|
|
59
|
+
incomplete.push(`${displayPath}: could not be parsed (${error.message})`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A collection another document instances is not a world of its own: at
|
|
64
|
+
// runtime its objects only exist under that instance's id, so contributing its
|
|
65
|
+
// own unprefixed paths as well would offer addresses that resolve to nothing.
|
|
66
|
+
const instanced = new Set<string>();
|
|
67
|
+
for (const document of parsed.values()) {
|
|
68
|
+
for (const block of childrenOf(document, "collection_instances")) {
|
|
69
|
+
const collection = firstField(block, "collection");
|
|
70
|
+
if (collection !== undefined) instanced.add(resourceKey(collection));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Memo keyed by display path, so a collection instanced twice is read once;
|
|
75
|
+
// `inProgress` is the cycle guard, without which two collections instancing
|
|
76
|
+
// each other would recur until the stack ran out. Each entry maps a composed
|
|
77
|
+
// path to the document that declared its leaf, so the attribution is carried
|
|
78
|
+
// by the same walk that builds the path rather than recovered afterwards.
|
|
79
|
+
const composed = new Map<string, ReadonlyMap<string, string>>();
|
|
80
|
+
const inProgress = new Set<string>();
|
|
81
|
+
|
|
82
|
+
function pathsOf(displayPath: string): ReadonlyMap<string, string> {
|
|
83
|
+
const done = composed.get(displayPath);
|
|
84
|
+
if (done !== undefined) return done;
|
|
85
|
+
if (inProgress.has(displayPath)) {
|
|
86
|
+
incomplete.push(
|
|
87
|
+
`${displayPath}: is instanced inside itself through a cycle of collection references, so its paths cannot be composed`,
|
|
88
|
+
);
|
|
89
|
+
return new Map();
|
|
90
|
+
}
|
|
91
|
+
const document = parsed.get(displayPath);
|
|
92
|
+
if (document === undefined) return new Map();
|
|
93
|
+
|
|
94
|
+
inProgress.add(displayPath);
|
|
95
|
+
const paths = new Map<string, string>();
|
|
96
|
+
|
|
97
|
+
for (const blockName of LEAF_BLOCKS) {
|
|
98
|
+
for (const block of childrenOf(document, blockName)) {
|
|
99
|
+
const id = firstField(block, "id");
|
|
100
|
+
if (id !== undefined && id !== "") paths.set(`/${id}`, displayPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const block of childrenOf(document, "collection_instances")) {
|
|
105
|
+
const id = firstField(block, "id");
|
|
106
|
+
const collection = firstField(block, "collection");
|
|
107
|
+
if (id === undefined || id === "" || collection === undefined) continue;
|
|
108
|
+
const key = resourceKey(collection);
|
|
109
|
+
if (!parsed.has(key)) {
|
|
110
|
+
incomplete.push(
|
|
111
|
+
`${displayPath}: the collection instance "${id}" names ${collection}, which is not among the project's readable scene documents`,
|
|
112
|
+
);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
for (const [nested, declarer] of pathsOf(key)) {
|
|
116
|
+
paths.set(`/${id}${nested}`, declarer);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
inProgress.delete(displayPath);
|
|
121
|
+
composed.set(displayPath, paths);
|
|
122
|
+
return paths;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Composed for every document, contributed from the un-instanced ones alone: a
|
|
126
|
+
// cycle or a dangling reference reached only from inside an instanced
|
|
127
|
+
// collection is still a gap the caller has to hear about.
|
|
128
|
+
const paths = new Set<string>();
|
|
129
|
+
const declaredIn = new Map<string, string[]>();
|
|
130
|
+
for (const displayPath of parsed.keys()) {
|
|
131
|
+
const own = pathsOf(displayPath);
|
|
132
|
+
if (instanced.has(displayPath)) continue;
|
|
133
|
+
for (const [path, declarer] of own) {
|
|
134
|
+
paths.add(path);
|
|
135
|
+
const declarers = declaredIn.get(path);
|
|
136
|
+
if (declarers === undefined) {
|
|
137
|
+
declaredIn.set(path, [declarer]);
|
|
138
|
+
} else if (!declarers.includes(declarer)) {
|
|
139
|
+
// Two roots instancing one collection under the same id compose the same
|
|
140
|
+
// address from the same leaf, which is one declaring file and not two.
|
|
141
|
+
declarers.push(declarer);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const declarers of declaredIn.values()) declarers.sort();
|
|
146
|
+
|
|
147
|
+
return { paths, declaredIn, incomplete };
|
|
148
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Defold's `.go`/`.collection` sources are protobuf text format. A message is a
|
|
2
|
+
// tree of `key: <scalar>` and `key { … }` entries where any key may repeat, so
|
|
3
|
+
// scalars and sub-messages are kept in separate multi-value maps — the format
|
|
4
|
+
// never uses one name as both.
|
|
5
|
+
export interface SceneMessage {
|
|
6
|
+
readonly fields: ReadonlyMap<string, readonly string[]>;
|
|
7
|
+
readonly messages: ReadonlyMap<string, readonly SceneMessage[]>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class SceneTextFormatError extends Error {
|
|
11
|
+
readonly line: number;
|
|
12
|
+
|
|
13
|
+
constructor(message: string, line: number) {
|
|
14
|
+
super(`${message} (line ${line})`);
|
|
15
|
+
this.name = "SceneTextFormatError";
|
|
16
|
+
this.line = line;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const IDENTIFIER_PART = /[A-Za-z0-9_]/;
|
|
21
|
+
|
|
22
|
+
function append<T>(target: Map<string, T[]>, key: string, value: T): void {
|
|
23
|
+
const existing = target.get(key);
|
|
24
|
+
if (existing === undefined) {
|
|
25
|
+
target.set(key, [value]);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
existing.push(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Parses one document. Nothing here knows that `data:` holds a payload: an
|
|
32
|
+
// embedded `.go` is reached by the caller re-invoking this function on the
|
|
33
|
+
// decoded value, which keeps exactly one decode pass per nesting level. A
|
|
34
|
+
// greedy unescape would resolve the deeper level's `\\\"` too and leave the
|
|
35
|
+
// payload unparseable.
|
|
36
|
+
export function parseSceneTextFormat(text: string): SceneMessage {
|
|
37
|
+
let pos = 0;
|
|
38
|
+
let line = 1;
|
|
39
|
+
|
|
40
|
+
function fail(message: string, at: number): never {
|
|
41
|
+
throw new SceneTextFormatError(message, at);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function advance(): string {
|
|
45
|
+
const ch = text.charAt(pos);
|
|
46
|
+
pos += 1;
|
|
47
|
+
if (ch === "\n") line += 1;
|
|
48
|
+
return ch;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function skipWhitespace(): void {
|
|
52
|
+
while (pos < text.length) {
|
|
53
|
+
const ch = text.charAt(pos);
|
|
54
|
+
if (ch !== " " && ch !== "\t" && ch !== "\r" && ch !== "\n") return;
|
|
55
|
+
advance();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function skipInlineWhitespace(): void {
|
|
60
|
+
while (pos < text.length) {
|
|
61
|
+
const ch = text.charAt(pos);
|
|
62
|
+
if (ch !== " " && ch !== "\t") return;
|
|
63
|
+
pos += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readIdentifier(at: number): string {
|
|
68
|
+
const start = pos;
|
|
69
|
+
while (pos < text.length && IDENTIFIER_PART.test(text.charAt(pos))) {
|
|
70
|
+
pos += 1;
|
|
71
|
+
}
|
|
72
|
+
if (pos === start) {
|
|
73
|
+
fail(`unexpected \`${text.charAt(start)}\``, at);
|
|
74
|
+
}
|
|
75
|
+
return text.slice(start, pos);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// One or more double-quoted literals, adjacent across whitespace and
|
|
79
|
+
// newlines, concatenated with no separator. `\"`, `\\`, `\n`, `\r` and `\t`
|
|
80
|
+
// resolve; every other backslash pair survives verbatim because it is the
|
|
81
|
+
// next level's escaping.
|
|
82
|
+
function readQuotedScalar(): string {
|
|
83
|
+
let value = "";
|
|
84
|
+
for (;;) {
|
|
85
|
+
const openedAt = line;
|
|
86
|
+
advance();
|
|
87
|
+
let closed = false;
|
|
88
|
+
while (pos < text.length) {
|
|
89
|
+
const ch = advance();
|
|
90
|
+
if (ch === "\\") {
|
|
91
|
+
if (pos >= text.length) break;
|
|
92
|
+
const escaped = advance();
|
|
93
|
+
if (escaped === '"') value += '"';
|
|
94
|
+
else if (escaped === "\\") value += "\\";
|
|
95
|
+
else if (escaped === "n") value += "\n";
|
|
96
|
+
else if (escaped === "r") value += "\r";
|
|
97
|
+
else if (escaped === "t") value += "\t";
|
|
98
|
+
else value += `\\${escaped}`;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (ch === '"') {
|
|
102
|
+
closed = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
if (ch === "\n") break;
|
|
106
|
+
value += ch;
|
|
107
|
+
}
|
|
108
|
+
if (!closed) {
|
|
109
|
+
fail("unterminated string literal", openedAt);
|
|
110
|
+
}
|
|
111
|
+
const resumePos = pos;
|
|
112
|
+
const resumeLine = line;
|
|
113
|
+
skipWhitespace();
|
|
114
|
+
if (text.charAt(pos) === '"') {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
pos = resumePos;
|
|
118
|
+
line = resumeLine;
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readUnquotedScalar(): string {
|
|
124
|
+
const newline = text.indexOf("\n", pos);
|
|
125
|
+
const end = newline === -1 ? text.length : newline;
|
|
126
|
+
const raw = text.slice(pos, end);
|
|
127
|
+
pos = end;
|
|
128
|
+
return raw.trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseBody(nested: boolean, openedAt: number): SceneMessage {
|
|
132
|
+
const fields = new Map<string, string[]>();
|
|
133
|
+
const messages = new Map<string, SceneMessage[]>();
|
|
134
|
+
for (;;) {
|
|
135
|
+
skipWhitespace();
|
|
136
|
+
if (pos >= text.length) {
|
|
137
|
+
if (nested) {
|
|
138
|
+
fail("unclosed `{`", openedAt);
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
if (text.charAt(pos) === "}") {
|
|
143
|
+
if (!nested) {
|
|
144
|
+
fail("unexpected `}`", line);
|
|
145
|
+
}
|
|
146
|
+
advance();
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
const keyLine = line;
|
|
150
|
+
const key = readIdentifier(keyLine);
|
|
151
|
+
skipInlineWhitespace();
|
|
152
|
+
const delimiter = text.charAt(pos);
|
|
153
|
+
if (delimiter === "{") {
|
|
154
|
+
const braceLine = line;
|
|
155
|
+
advance();
|
|
156
|
+
append(messages, key, parseBody(true, braceLine));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (delimiter === ":") {
|
|
160
|
+
advance();
|
|
161
|
+
skipInlineWhitespace();
|
|
162
|
+
append(fields, key, text.charAt(pos) === '"' ? readQuotedScalar() : readUnquotedScalar());
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
fail(`expected \`:\` or \`{\` after \`${key}\``, keyLine);
|
|
166
|
+
}
|
|
167
|
+
return { fields, messages };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return parseBody(false, 1);
|
|
171
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
}
|
package/src/transpile.ts
CHANGED
|
@@ -158,6 +158,11 @@ function buildAmbientFiles(): Record<string, string> {
|
|
|
158
158
|
'export { defineGuiScript } from "./src/lifecycle.js";\nexport type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";\n',
|
|
159
159
|
"node_modules/@defold-typescript/types/render-script.d.ts":
|
|
160
160
|
'export { defineRenderScript } from "./src/lifecycle.js";\nexport type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";\n',
|
|
161
|
+
"node_modules/@defold-typescript/types/editor-script.d.ts": [
|
|
162
|
+
'export { defineEditorCommand, defineEditorScript } from "./src/editor.js";',
|
|
163
|
+
'export type { EditorCommandQuery, EditorNode } from "./src/editor.js";',
|
|
164
|
+
"",
|
|
165
|
+
].join("\n"),
|
|
161
166
|
};
|
|
162
167
|
// Seed the Lua 5.1 standard library (math/os/string/table/coroutine + base
|
|
163
168
|
// globals) so user code can call e.g. `math.randomseed(os.time())`.
|