@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.
Files changed (37) hide show
  1. package/dist/animation-sources.d.ts +6 -0
  2. package/dist/{sprite-animation-index.d.ts → component-animation-index.d.ts} +4 -4
  3. package/dist/config-key-index.d.ts +1 -0
  4. package/dist/gui-flipbook-index.d.ts +8 -0
  5. package/dist/gui-node-index.d.ts +21 -0
  6. package/dist/index.d.ts +21 -8
  7. package/dist/index.js +1317 -289
  8. package/dist/library-dependencies.d.ts +47 -0
  9. package/dist/scene-address-declaration.d.ts +14 -0
  10. package/dist/scene-collection-roles.d.ts +30 -0
  11. package/dist/scene-component-index.d.ts +20 -0
  12. package/dist/scene-documents.d.ts +21 -0
  13. package/dist/scene-naming-context.d.ts +66 -0
  14. package/dist/scene-object-path-index.d.ts +4 -1
  15. package/dist/url-address-literals.d.ts +10 -0
  16. package/dist/url-address-slots.d.ts +3 -1
  17. package/dist/url-cross-world-address.d.ts +41 -0
  18. package/dist/url-fragment-reachability.d.ts +3 -0
  19. package/package.json +2 -2
  20. package/src/animation-sources.ts +90 -0
  21. package/src/component-animation-index.ts +399 -0
  22. package/src/config-key-index.ts +28 -2
  23. package/src/gui-flipbook-index.ts +91 -0
  24. package/src/gui-node-index.ts +42 -4
  25. package/src/index.ts +66 -10
  26. package/src/library-dependencies.ts +337 -0
  27. package/src/scene-address-declaration.ts +74 -0
  28. package/src/scene-collection-roles.ts +267 -0
  29. package/src/scene-component-index.ts +74 -6
  30. package/src/scene-documents.ts +186 -0
  31. package/src/scene-naming-context.ts +130 -0
  32. package/src/scene-object-path-index.ts +172 -33
  33. package/src/url-address-literals.ts +115 -0
  34. package/src/url-address-slots.ts +23 -2
  35. package/src/url-cross-world-address.ts +109 -0
  36. package/src/url-fragment-reachability.ts +72 -93
  37. package/src/sprite-animation-index.ts +0 -241
@@ -0,0 +1,47 @@
1
+ import { type SceneReadHost } from "./scene-documents";
2
+ export interface ExtensionDependency {
3
+ readonly index: number;
4
+ readonly url: string;
5
+ }
6
+ export declare function readGameProjectDependencies(gameProjectText: string): ExtensionDependency[];
7
+ export declare function archiveWrapperOf(entries: readonly string[]): string | undefined;
8
+ export interface LibrarySharedEntry {
9
+ readonly entry: string;
10
+ readonly path: string;
11
+ }
12
+ export interface LibraryIncludedEntries {
13
+ readonly shared: LibrarySharedEntry[];
14
+ readonly reasons: string[];
15
+ readonly refused: string[];
16
+ }
17
+ export declare function isContainedResourcePath(path: string): boolean;
18
+ export declare function libraryIncludedEntries(entries: readonly string[], gameProjectText: string | undefined): LibraryIncludedEntries;
19
+ export declare const LIBRARY_DEPENDENCY_ROOT = ".defold-types/dependencies";
20
+ export interface LibrarySceneDocument {
21
+ readonly displayPath: string;
22
+ readonly hostPath: string;
23
+ readonly url: string;
24
+ readonly text: string;
25
+ }
26
+ export interface LibrarySceneDocuments {
27
+ readonly documents: LibrarySceneDocument[];
28
+ readonly unreadable: string[];
29
+ }
30
+ /**
31
+ * The scene sources the project's resolved library dependencies contribute,
32
+ * read out of the surface `resolve` materialized.
33
+ *
34
+ * Each document is keyed by its **merged** Defold resource path, not by one
35
+ * namespaced with its origin: Defold folds a library's `[library] include_dirs`
36
+ * into the consuming project's root namespace, and `scene-object-path-index.ts`
37
+ * resolves a `collection: "/druid/foo.collection"` reference by exactly that
38
+ * key — so an origin-namespaced key would strand every cross-document
39
+ * reference. The origin travels beside the document instead.
40
+ *
41
+ * Every refusal is one `unreadable` line naming the URL where one is known: an
42
+ * absent or unusable manifest, a declared dependency the last `resolve` did not
43
+ * materialize, a materialized directory `game.project` no longer declares, and
44
+ * a file that would not read. A dependency that contributes nothing silently is
45
+ * exactly the hole `readSceneDocuments` promises not to have.
46
+ */
47
+ export declare function readLibrarySceneDocuments(host: SceneReadHost, projectRoot: string, extensions?: readonly string[]): LibrarySceneDocuments;
@@ -0,0 +1,14 @@
1
+ import type { SceneCollectionRoles } from "./scene-collection-roles";
2
+ /**
3
+ * The global augmentation that fills `SceneGameObjectAddresses` and
4
+ * `SceneComponentAddresses` from already-read scene sources — keys are
5
+ * project-relative display paths, values are file text. `roles` says which
6
+ * world each collection is, so a path key carries the world it resolves in.
7
+ *
8
+ * Pure, like the two indexes it composes: the filesystem walk belongs to the
9
+ * caller. Keys are sorted so a project whose scenes did not change re-emits a
10
+ * byte-identical file, and a document map built in a different order emits the
11
+ * same bytes as well; a `Set` iteration order leaking into the output would
12
+ * make every regeneration rewrite the file and re-check the whole program.
13
+ */
14
+ export declare function buildSceneAddressDeclaration(documents: ReadonlyMap<string, string>, roles: SceneCollectionRoles): string;
@@ -0,0 +1,30 @@
1
+ export interface SceneCollectionRoles {
2
+ /** The one collection `[bootstrap] main_collection` names, whose paths are bare. */
3
+ readonly bootstrap?: string | undefined;
4
+ /** Proxied collections, mapped to the socket `CollectionDesc.name` gives them. */
5
+ readonly sockets: ReadonlyMap<string, string>;
6
+ /** Collections another collection instances, whose paths exist only under that id. */
7
+ readonly instanced: ReadonlySet<string>;
8
+ /** Collection-factory prototypes, whose objects have no static path at all. */
9
+ readonly factoryPrototypes: ReadonlySet<string>;
10
+ readonly incomplete: readonly string[];
11
+ }
12
+ export interface SceneCollectionRolesInput {
13
+ /** The `.go`/`.collection` universe, keyed by display path. */
14
+ readonly documents: ReadonlyMap<string, string>;
15
+ /** The `.collectionproxy`/`.collectionfactory` documents, keyed by display path. */
16
+ readonly references: ReadonlyMap<string, string>;
17
+ /** `game.project`'s text, or `undefined` when the walk could not read it. */
18
+ readonly gameProject: string | undefined;
19
+ }
20
+ /**
21
+ * Classify every collection the project declares into the world it is: the one
22
+ * bootstrap root, a proxy world addressed through its own socket, a collection
23
+ * instanced inside another, or a factory prototype with no static address.
24
+ *
25
+ * Pure, like the indexes it feeds — the filesystem walk belongs to the caller.
26
+ * There is deliberately no everything-else-is-a-root fallback: that fallback is
27
+ * what offers a bare `/enemy` for an object living in another world, and losing
28
+ * a completion is safe where offering a false one is not.
29
+ */
30
+ export declare function buildSceneCollectionRoles(input: SceneCollectionRolesInput): SceneCollectionRoles;
@@ -1,5 +1,25 @@
1
+ import { type SceneMessage } from "./scene-text-format";
1
2
  export interface SceneComponentIndex {
2
3
  readonly ids: ReadonlySet<string>;
3
4
  readonly incomplete: readonly string[];
4
5
  }
6
+ export interface ComponentDeclarations {
7
+ readonly ids: Set<string>;
8
+ readonly resources: Map<string, string>;
9
+ }
10
+ /**
11
+ * Every component id one already-parsed scene message declares, plus the
12
+ * resource each *referenced* component names, following the two `data:`
13
+ * payloads Defold embeds a whole document in. Exported because the game-object
14
+ * path walk attributes components to the object that owns them and must read
15
+ * the same rule this index reads — a second collector is exactly how the two
16
+ * universes drift apart.
17
+ *
18
+ * `blockName` names the block `message` was taken from, because the `data:`
19
+ * re-parse is keyed off it: a whole document is `""`, while a single
20
+ * `embedded_instances` block has to say so or its payload is never opened.
21
+ */
22
+ export declare function collectComponentDeclarations(message: SceneMessage, displayPath: string, incomplete: string[], blockName?: string): ComponentDeclarations;
23
+ /** The id half of {@link collectComponentDeclarations}, for callers that join no resources. */
24
+ export declare function collectComponentIds(message: SceneMessage, displayPath: string, incomplete: string[], blockName?: string): Set<string>;
5
25
  export declare function buildSceneComponentIndex(documents: ReadonlyMap<string, string>): SceneComponentIndex;
@@ -0,0 +1,21 @@
1
+ export interface SceneReadHost {
2
+ readDirectory?(path: string, extensions?: readonly string[]): string[];
3
+ readFile(path: string): string | undefined;
4
+ }
5
+ export declare const SCENE_EXTENSIONS: string[];
6
+ export declare const GUI_EXTENSIONS: string[];
7
+ export declare const INPUT_BINDING_EXTENSIONS: string[];
8
+ export declare const COLLECTION_REFERENCE_EXTENSIONS: string[];
9
+ export declare const ANIMATION_ASSET_EXTENSIONS: string[];
10
+ export declare const LIBRARY_SCENE_EXTENSIONS: string[];
11
+ export declare const PROJECT_EXTENSIONS: string[];
12
+ export declare const GAME_PROJECT_DOCUMENT = "game.project";
13
+ export declare function displayPathOf(projectRoot: string, filePath: string): string;
14
+ export declare function isExcludedProjectPath(displayPath: string): boolean;
15
+ export declare function readSceneDocuments(host: SceneReadHost, projectRoot: string, extensions?: readonly string[]): {
16
+ documents: Map<string, string>;
17
+ origins: Map<string, string>;
18
+ unreadable: string[];
19
+ paths: string[];
20
+ };
21
+ export declare function listProjectResourcePaths(host: SceneReadHost, projectRoot: string, extensions: readonly string[]): Set<string>;
@@ -0,0 +1,66 @@
1
+ import type { SceneObjectPathIndex } from "./scene-object-path-index";
2
+ /**
3
+ * One place a script runs, and the addressing frame it runs in. At runtime an
4
+ * instance carries `m_CollectionPathHashState` seeded from its compiled id up
5
+ * to and including the last `/`, and `GetAbsoluteIdentifier` resolves a
6
+ * relative string by continuing that hash — so a relative address written in a
7
+ * script means one thing from this object and nothing anywhere else.
8
+ */
9
+ export interface NamingContext {
10
+ /** The world-qualified composed path of the object hosting the script. */
11
+ readonly object: string;
12
+ /** The proxy socket this object lives behind, or `undefined` in the bootstrap world. */
13
+ readonly socket: string | undefined;
14
+ /**
15
+ * `object` through its last `/`, inclusive — the collection path a relative
16
+ * address continues. World-qualified by construction, so a relative address
17
+ * can never resolve across a world.
18
+ */
19
+ readonly prefix: string;
20
+ }
21
+ /**
22
+ * The proxy socket an address names, or `undefined` when it names none — a
23
+ * bootstrap-world path, a relative path, a bare `#fragment`.
24
+ *
25
+ * Defold's address grammar is `[socket:][path][#fragment]` with both bracketed
26
+ * halves optional, so `mylevel:`, `mylevel:enemy` and `mylevel:#body` are as
27
+ * socket-qualified as `mylevel:/path`. The socket is therefore read from the
28
+ * `:` delimiter rather than from a following `/`: the head is the address up to
29
+ * the first `/` or `#` (the whole address when it carries neither), and the
30
+ * socket is the text before that head's first `:`. A `:` inside a path segment
31
+ * (`/level:1/enemy`) is not a socket, because the `/` closes the head first,
32
+ * and an empty socket (`:path`) is none.
33
+ *
34
+ * Exported because a written address literal and a composed object path are the
35
+ * same shape: the cross-world check reads a socket out of source text with this
36
+ * one rule rather than a second reader that could disagree with the contexts it
37
+ * compares against.
38
+ */
39
+ export declare function socketOfAddress(address: string): string | undefined;
40
+ /**
41
+ * Every naming context each script resource runs in, keyed by that resource and
42
+ * sorted by object path.
43
+ *
44
+ * `guiScripts` maps a `.gui` display path to the gui-script resource it names,
45
+ * which the caller builds with `guiScriptResourceOf` — the walk needs no `.gui`
46
+ * documents of its own.
47
+ *
48
+ * A script hosted by several objects has several contexts, and that is not
49
+ * ambiguity: a `.go` instanced in two collections genuinely runs in both, and
50
+ * it is the consumer's provenance that tells an author which context an entry
51
+ * resolves from.
52
+ */
53
+ export declare function buildScriptNamingContexts(index: SceneObjectPathIndex, guiScripts: ReadonlyMap<string, string>): ReadonlyMap<string, readonly NamingContext[]>;
54
+ /**
55
+ * The relative forms a script may write from one naming context: every object
56
+ * path under the context's prefix with that prefix stripped, and each of those
57
+ * joined to the components its object owns.
58
+ *
59
+ * An object `componentsOf` withheld still contributes its path — the object is
60
+ * provably there — but no `#component` join, because which components it owns
61
+ * was never read.
62
+ */
63
+ export declare function relativeAddressesFrom(index: SceneObjectPathIndex, context: NamingContext): {
64
+ readonly paths: ReadonlySet<string>;
65
+ readonly addresses: ReadonlySet<string>;
66
+ };
@@ -1,6 +1,9 @@
1
+ import type { SceneCollectionRoles } from "./scene-collection-roles";
1
2
  export interface SceneObjectPathIndex {
2
3
  readonly paths: ReadonlySet<string>;
3
4
  readonly declaredIn: ReadonlyMap<string, readonly string[]>;
5
+ readonly componentsOf: ReadonlyMap<string, readonly string[]>;
6
+ readonly componentResourcesOf: ReadonlyMap<string, ReadonlyMap<string, string>>;
4
7
  readonly incomplete: readonly string[];
5
8
  }
6
- export declare function buildSceneObjectPathIndex(documents: ReadonlyMap<string, string>): SceneObjectPathIndex;
9
+ export declare function buildSceneObjectPathIndex(documents: ReadonlyMap<string, string>, roles: SceneCollectionRoles): SceneObjectPathIndex;
@@ -0,0 +1,10 @@
1
+ import * as ts from "typescript";
2
+ export declare function isAmbient(fileName: string): boolean;
3
+ export declare function canonicalHashSymbols(checker: ts.TypeChecker, program: ts.Program): ReadonlySet<ts.Symbol>;
4
+ /**
5
+ * The address text a slot expression is statically known to carry — a quoted or
6
+ * backtick-quoted literal's own text, or the string a `Hash`-typed value was
7
+ * hashed from — and `undefined` for everything a project only settles at
8
+ * runtime: a substituted template, a bare `Hash`, a `Url`, a `string`.
9
+ */
10
+ export declare function staticAddressTextOf(checker: ts.TypeChecker, node: ts.Expression, canonical: ReadonlySet<ts.Symbol>): string | undefined;
@@ -1,4 +1,4 @@
1
- import type { UrlParameterClass, UrlParameterTable } from "@defold-typescript/types";
1
+ import type { UrlParameterClass, UrlParameterEntry, UrlParameterTable } from "@defold-typescript/types";
2
2
  import * as ts from "typescript";
3
3
  export interface ClassifiedSlot {
4
4
  readonly class: UrlParameterClass;
@@ -6,10 +6,12 @@ export interface ClassifiedSlot {
6
6
  readonly textStart: number;
7
7
  readonly fragmentStart: number;
8
8
  readonly addressText?: string;
9
+ readonly nodeParameter?: string;
9
10
  readonly resourceExtensions?: readonly string[];
10
11
  }
11
12
  export declare function isAddressClass(parameterClass: UrlParameterClass): boolean;
12
13
  export declare function isFragmentCaret(slot: ClassifiedSlot, position: number): boolean;
14
+ export declare function addressEntryOfArgument(checker: ts.TypeChecker, table: UrlParameterTable, argument: ts.Expression): UrlParameterEntry | undefined;
13
15
  export declare function addressClassOfArgument(checker: ts.TypeChecker, table: UrlParameterTable, argument: ts.Expression): UrlParameterClass;
14
16
  export declare function resolveClassifiedSlotAtPosition(input: {
15
17
  program: ts.Program;
@@ -0,0 +1,41 @@
1
+ import type { UrlParameterEntry, UrlParameterTable } from "@defold-typescript/types";
2
+ import * as ts from "typescript";
3
+ /** One address naming a world the script writing it cannot reach. */
4
+ export interface CrossWorldAddressFinding {
5
+ readonly fileName: string;
6
+ readonly start: number;
7
+ readonly length: number;
8
+ /** The statically-known address text, as written or as hashed. */
9
+ readonly address: string;
10
+ /** The foreign proxy socket that text names. */
11
+ readonly socket: string;
12
+ readonly message: string;
13
+ }
14
+ /**
15
+ * Whether this slot refuses a socket-qualified address naming another world.
16
+ *
17
+ * The negation of `slotAcceptsForeignSocket` in `@defold-typescript/types`, and
18
+ * deliberately fails open the same way: a slot carrying no judgment refuses
19
+ * nothing, so nothing is reported on a slot nobody classified.
20
+ */
21
+ export declare function slotRejectsForeignSocket(entry: UrlParameterEntry): boolean;
22
+ /**
23
+ * Report every address-slot expression whose statically-known text names a proxy
24
+ * socket the script writing it does not run in, at a slot that resolves through
25
+ * an instance or component handle rather than the message bus.
26
+ *
27
+ * Deliberately not folded into `checkUrlFragmentReachability`: that check
28
+ * suppresses index-wide on an incomplete component universe, while this one's
29
+ * honesty rule is per file — a script whose naming contexts could not be
30
+ * resolved is reported on at all, because unknown is not wrong. Fusing them
31
+ * would put one check's early return in front of the other's findings.
32
+ *
33
+ * `worldsOf` answers with the sockets of every naming context the file's script
34
+ * runs in; an empty answer is the unknown case and yields nothing.
35
+ */
36
+ export declare function checkCrossWorldAddresses(input: {
37
+ program: ts.Program;
38
+ table: UrlParameterTable;
39
+ worldsOf: (fileName: string) => readonly (string | undefined)[];
40
+ sourceFiles?: readonly ts.SourceFile[];
41
+ }): readonly CrossWorldAddressFinding[];
@@ -1,6 +1,7 @@
1
1
  import type { UrlParameterTable } from "@defold-typescript/types";
2
2
  import * as ts from "typescript";
3
3
  import type { SceneComponentIndex } from "./scene-component-index";
4
+ import type { SceneObjectPathIndex } from "./scene-object-path-index";
4
5
  export interface UrlFragmentFinding {
5
6
  readonly fileName: string;
6
7
  readonly start: number;
@@ -8,6 +9,7 @@ export interface UrlFragmentFinding {
8
9
  readonly fragment: string;
9
10
  readonly message: string;
10
11
  }
12
+ export type SceneObjectComponents = Pick<SceneObjectPathIndex, "paths" | "componentsOf" | "incomplete">;
11
13
  export type UrlFragmentReport = {
12
14
  readonly kind: "suppressed";
13
15
  readonly reasons: readonly string[];
@@ -19,5 +21,6 @@ export declare function checkUrlFragmentReachability(input: {
19
21
  program: ts.Program;
20
22
  table: UrlParameterTable;
21
23
  index: SceneComponentIndex;
24
+ objects?: SceneObjectComponents;
22
25
  sourceFiles?: readonly ts.SourceFile[];
23
26
  }): UrlFragmentReport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "TypeScript-to-Lua build pipeline tuned for Defold's runtime.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "test": "bun test"
36
36
  },
37
37
  "dependencies": {
38
- "@defold-typescript/types": "0.29.0",
38
+ "@defold-typescript/types": "0.31.0",
39
39
  "@typescript-to-lua/language-extensions": "1.19.0",
40
40
  "typescript-to-lua": "^1.36.0"
41
41
  }
@@ -0,0 +1,90 @@
1
+ import { parseSceneTextFormat, type SceneMessage, SceneTextFormatError } from "./scene-text-format";
2
+
3
+ // Resource paths inside a scene are project-absolute; an index keyed off one
4
+ // drops the leading `/`, so a caller maps the file it holds forward through the
5
+ // build's output-path math and looks the result up.
6
+ export function resourceKey(resource: string): string {
7
+ return resource.startsWith("/") ? resource.slice(1) : resource;
8
+ }
9
+
10
+ // The keys a caller supplies name files on the host, so they may arrive with
11
+ // native separators; the resource paths they are matched against come out of
12
+ // scene content and are always `/`-separated. Normalizing here enforces that
13
+ // contract at the boundary instead of leaving it to whichever caller happens
14
+ // to build the maps.
15
+ export function normalizeDocumentKey(path: string): string {
16
+ return path.replaceAll("\\", "/");
17
+ }
18
+
19
+ const SPRITE_SUFFIX = ".sprite";
20
+ const MODEL_SUFFIX = ".model";
21
+ const ANIMATION_SET_SUFFIX = ".animationset";
22
+
23
+ // The documents that carry only a hop to the ids, never the ids themselves: a
24
+ // `.sprite` names its tile source, a `.model` names its animation set, and a
25
+ // `.animationset` names the files whose basenames are the ids. Reading any of
26
+ // them for `animations { id: … }` yields an empty set that is indistinguishable
27
+ // from a real source declaring nothing.
28
+ const HOP_ONLY_SUFFIXES = [SPRITE_SUFFIX, MODEL_SUFFIX, ANIMATION_SET_SUFFIX];
29
+
30
+ function childrenOf(message: SceneMessage, name: string): readonly SceneMessage[] {
31
+ return message.messages.get(name) ?? [];
32
+ }
33
+
34
+ // Only `animations { id: … }` counts. Defold may also expose a bare
35
+ // `images { image: … }` entry as a one-frame animation named after the file,
36
+ // which is unverified here — and a wrong suggestion in an animation slot is a
37
+ // runtime crash rather than a no-op.
38
+ export function declaredAnimations(document: SceneMessage): Set<string> {
39
+ const ids = new Set<string>();
40
+ for (const animation of childrenOf(document, "animations")) {
41
+ for (const id of animation.fields.get("id") ?? []) {
42
+ if (id !== "") ids.add(id);
43
+ }
44
+ }
45
+ return ids;
46
+ }
47
+
48
+ // The animation ids every asset document declares, keyed by display path, with
49
+ // each unreadable document named in `unresolved` instead of silently declaring
50
+ // nothing. The hop-only kinds are skipped: they declare no ids of their own,
51
+ // only the path to the document that does, which their own readers parse them
52
+ // for.
53
+ //
54
+ // One reader for both animation indexes, so a sprite component and a gui
55
+ // texture can never disagree about what an atlas declares.
56
+ export function readAnimationSources(
57
+ assets: ReadonlyMap<string, string>,
58
+ unresolved: string[],
59
+ ): Map<string, Set<string>> {
60
+ const animationsByTileSet = new Map<string, Set<string>>();
61
+ for (const [path, text] of assets) {
62
+ const displayPath = normalizeDocumentKey(path);
63
+ if (HOP_ONLY_SUFFIXES.some((suffix) => displayPath.endsWith(suffix))) continue;
64
+ let document: SceneMessage;
65
+ try {
66
+ document = parseSceneTextFormat(text);
67
+ } catch (error) {
68
+ if (!(error instanceof SceneTextFormatError)) throw error;
69
+ unresolved.push(`${displayPath}: could not be parsed (${error.message})`);
70
+ continue;
71
+ }
72
+ animationsByTileSet.set(displayPath, declaredAnimations(document));
73
+ }
74
+ return animationsByTileSet;
75
+ }
76
+
77
+ // The entry paths a `.animationset` lists, in document order. The values are
78
+ // resource paths, not ids: `AnimationSetBuilder` stamps the owning set's name
79
+ // on every animation a file contains, so the id an entry contributes is derived
80
+ // from its path rather than read out of the file it names. Empty values are
81
+ // dropped the way an empty animation id is.
82
+ export function animationSetEntries(document: SceneMessage): string[] {
83
+ const entries: string[] = [];
84
+ for (const entry of childrenOf(document, "animations")) {
85
+ for (const animation of entry.fields.get("animation") ?? []) {
86
+ if (animation !== "") entries.push(animation);
87
+ }
88
+ }
89
+ return entries;
90
+ }