gdharness 1.2.33 → 1.2.35
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/build/cli.js +134 -33
- package/build/godot/addons/gdharness_editor/animation_reads.gd +224 -0
- package/build/godot/addons/gdharness_editor/animation_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +76 -5
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +30 -143
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +4 -0
- package/build/godot/operations/animation_reads.gd +224 -0
- package/build/godot/operations/export_presets.gd +232 -0
- package/build/godot/operations/file_reads.gd +76 -5
- package/build/godot/operations/godot_operations.gd +21 -1
- package/build/godot/operations/import_pipeline.gd +4 -60
- package/build/godot/operations/input_actions.gd +51 -7
- package/build/godot/operations/project_diagnostics.gd +3 -1
- package/build/index.js +132 -31
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7847,6 +7847,11 @@ function isWithinRoot(rootPath, candidatePath) {
|
|
|
7847
7847
|
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
7848
7848
|
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
7849
7849
|
}
|
|
7850
|
+
function keptInside(given) {
|
|
7851
|
+
const at = given.indexOf("::");
|
|
7852
|
+
const inner = at === -1 ? "" : given.slice(at + 2);
|
|
7853
|
+
return KEPT_ID.test(inner) ? { file: given.slice(0, at), inner } : { file: given, inner: undefined };
|
|
7854
|
+
}
|
|
7850
7855
|
function resolveWithinProject(projectPath, candidatePath, options = {}) {
|
|
7851
7856
|
const trimmed = candidatePath.trim();
|
|
7852
7857
|
if (trimmed === "") {
|
|
@@ -7875,6 +7880,13 @@ function resolveWithinProject(projectPath, candidatePath, options = {}) {
|
|
|
7875
7880
|
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
7876
7881
|
};
|
|
7877
7882
|
}
|
|
7883
|
+
const kept = keptInside(trimmed);
|
|
7884
|
+
if (kept.inner !== undefined) {
|
|
7885
|
+
return {
|
|
7886
|
+
ok: false,
|
|
7887
|
+
reason: `Path '${trimmed}' names the resource ${kept.inner} kept inside ${kept.file}, not a file. resource_edit read reads one by that path; this argument takes a file.`
|
|
7888
|
+
};
|
|
7889
|
+
}
|
|
7878
7890
|
if (withoutScheme.includes(":")) {
|
|
7879
7891
|
return {
|
|
7880
7892
|
ok: false,
|
|
@@ -7917,9 +7929,10 @@ function exportedBuild(projectPath, candidatePath) {
|
|
|
7917
7929
|
}
|
|
7918
7930
|
return { ok: true, absolutePath: real, relativePath: found.relativePath };
|
|
7919
7931
|
}
|
|
7920
|
-
var RESOURCE_SCHEME = "res://", OTHER_SCHEME;
|
|
7932
|
+
var RESOURCE_SCHEME = "res://", OTHER_SCHEME, KEPT_ID;
|
|
7921
7933
|
var init_paths = __esm(() => {
|
|
7922
7934
|
OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
7935
|
+
KEPT_ID = /^[A-Za-z0-9_]+$/;
|
|
7923
7936
|
});
|
|
7924
7937
|
|
|
7925
7938
|
// src/runner.ts
|
|
@@ -8128,9 +8141,14 @@ class GameLog {
|
|
|
8128
8141
|
const owner = this.lastHeadline[source];
|
|
8129
8142
|
if (owner !== null && /^\s/.test(line)) {
|
|
8130
8143
|
owner.detail.push(line.trim());
|
|
8144
|
+
owner.continues = false;
|
|
8131
8145
|
return;
|
|
8132
8146
|
}
|
|
8133
8147
|
const headline = announced(line);
|
|
8148
|
+
if (owner?.continues === true && headline === null) {
|
|
8149
|
+
owner.detail.push(line.trim());
|
|
8150
|
+
return;
|
|
8151
|
+
}
|
|
8134
8152
|
const detail = [];
|
|
8135
8153
|
const entry = {
|
|
8136
8154
|
index: this.entries.length,
|
|
@@ -8143,7 +8161,7 @@ class GameLog {
|
|
|
8143
8161
|
if (headline?.byScript === true) {
|
|
8144
8162
|
this.byScript.add(entry.index);
|
|
8145
8163
|
}
|
|
8146
|
-
this.lastHeadline[source] = headline ? { detail } : null;
|
|
8164
|
+
this.lastHeadline[source] = headline ? { detail, continues: source === "stderr" && headline.text.endsWith(":") } : null;
|
|
8147
8165
|
}
|
|
8148
8166
|
raisedByScript(entry) {
|
|
8149
8167
|
return this.byScript.has(entry.index);
|
|
@@ -11308,7 +11326,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11308
11326
|
{
|
|
11309
11327
|
name: "project_settings",
|
|
11310
11328
|
title: "Project settings",
|
|
11311
|
-
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
|
|
11329
|
+
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events and deadzone, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
|
|
11312
11330
|
parameters: {
|
|
11313
11331
|
projectPath: PROJECT_PATH,
|
|
11314
11332
|
setting: {
|
|
@@ -11406,8 +11424,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
11406
11424
|
requires: ["scenePath"]
|
|
11407
11425
|
},
|
|
11408
11426
|
add_input_action: {
|
|
11409
|
-
effect: "
|
|
11410
|
-
summary: "register an input action and its events",
|
|
11427
|
+
effect: "destructive",
|
|
11428
|
+
summary: "register an input action and its events, or rebind one that exists",
|
|
11411
11429
|
requires: ["actionName", "events"]
|
|
11412
11430
|
},
|
|
11413
11431
|
remove_input_action: {
|
|
@@ -11545,20 +11563,36 @@ var init_tool_definitions = __esm(() => {
|
|
|
11545
11563
|
{
|
|
11546
11564
|
name: "project_export",
|
|
11547
11565
|
title: "Export",
|
|
11548
|
-
description: "Export presets and exports.",
|
|
11566
|
+
description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without.",
|
|
11549
11567
|
parameters: {
|
|
11550
11568
|
projectPath: PROJECT_PATH,
|
|
11551
|
-
preset: {
|
|
11569
|
+
preset: {
|
|
11570
|
+
type: "string",
|
|
11571
|
+
ops: ["run", "add_preset"],
|
|
11572
|
+
description: "Preset name from export_presets.cfg. add_preset: the name of the new one."
|
|
11573
|
+
},
|
|
11574
|
+
platform: {
|
|
11575
|
+
type: "string",
|
|
11576
|
+
ops: ["add_preset"],
|
|
11577
|
+
enum: ["Windows Desktop", "Linux", "macOS", "Android", "iOS", "Web"],
|
|
11578
|
+
description: "add_preset: the platform the preset exports for."
|
|
11579
|
+
},
|
|
11552
11580
|
outputPath: {
|
|
11553
11581
|
type: "string",
|
|
11554
|
-
|
|
11582
|
+
ops: ["run", "add_preset"],
|
|
11583
|
+
description: `Where the export is written, inside the project. The directory is created if it is not there: Godot's command-line exporter refuses a missing one with "The given export path doesn't exist", which reads as a wrong path in the preset. add_preset: the path the preset exports to from the editor, optional.`
|
|
11555
11584
|
},
|
|
11556
|
-
debug: { type: "boolean", description: "run: a debug export. Default false." }
|
|
11585
|
+
debug: { type: "boolean", ops: ["run"], description: "run: a debug export. Default false." }
|
|
11557
11586
|
},
|
|
11558
11587
|
requires: ["projectPath"],
|
|
11559
11588
|
operations: {
|
|
11560
11589
|
list: { effect: "read", summary: "the presets in export_presets.cfg", requires: [] },
|
|
11561
|
-
run: { effect: "destructive", summary: "export with a preset", requires: ["preset", "outputPath"] }
|
|
11590
|
+
run: { effect: "destructive", summary: "export with a preset", requires: ["preset", "outputPath"] },
|
|
11591
|
+
add_preset: {
|
|
11592
|
+
effect: "additive",
|
|
11593
|
+
summary: "add an export preset to export_presets.cfg",
|
|
11594
|
+
requires: ["preset", "platform"]
|
|
11595
|
+
}
|
|
11562
11596
|
}
|
|
11563
11597
|
},
|
|
11564
11598
|
{
|
|
@@ -11755,7 +11789,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11755
11789
|
{
|
|
11756
11790
|
name: "scene_animation",
|
|
11757
11791
|
title: "Animations",
|
|
11758
|
-
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another.
|
|
11792
|
+
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write need the editor connected.",
|
|
11759
11793
|
parameters: {
|
|
11760
11794
|
projectPath: PROJECT_PATH,
|
|
11761
11795
|
scenePath: SCENE_PATH,
|
|
@@ -11787,7 +11821,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
11787
11821
|
ops: ["read_states", "add_state", "connect_states"],
|
|
11788
11822
|
description: "The AnimationTree node."
|
|
11789
11823
|
},
|
|
11790
|
-
stateName: {
|
|
11824
|
+
stateName: {
|
|
11825
|
+
type: "string",
|
|
11826
|
+
ops: ["add_state"],
|
|
11827
|
+
description: "add_state: the state to add. It goes right of the rightmost state in the editor's graph, answered under position."
|
|
11828
|
+
},
|
|
11791
11829
|
stateMachinePath: {
|
|
11792
11830
|
type: "string",
|
|
11793
11831
|
ops: ["read_states", "add_state", "connect_states"],
|
|
@@ -11962,7 +12000,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11962
12000
|
projectPath: PROJECT_PATH,
|
|
11963
12001
|
resourcePath: {
|
|
11964
12002
|
...RESOURCE_PATH,
|
|
11965
|
-
description: `${RESOURCE_FILE} move: a directory too.`
|
|
12003
|
+
description: `${RESOURCE_FILE} move: a directory too. read: also a resource kept inside a scene or resource file, by the path a read answers a property holding it with, such as "res://hero.tscn::Animation_jump".`
|
|
11966
12004
|
},
|
|
11967
12005
|
resourceType: {
|
|
11968
12006
|
type: "string",
|
|
@@ -12929,7 +12967,7 @@ var init_headless_operations = __esm(() => {
|
|
|
12929
12967
|
uid: "get_uid",
|
|
12930
12968
|
refresh_classes: "refresh_class_cache"
|
|
12931
12969
|
}),
|
|
12932
|
-
project_export: dictionary({ list: "list_export_presets" }),
|
|
12970
|
+
project_export: dictionary({ list: "list_export_presets", add_preset: "add_export_preset" }),
|
|
12933
12971
|
script_edit: dictionary({ create: "create_script", modify: "modify_script" }),
|
|
12934
12972
|
script_info: dictionary({ structure: "get_script_info" }),
|
|
12935
12973
|
editor_classes: dictionary({
|
|
@@ -44875,7 +44913,7 @@ function exportPresetRefusal(projectPath, preset) {
|
|
|
44875
44913
|
try {
|
|
44876
44914
|
text = readFileSync26(join30(projectPath, "export_presets.cfg"), "utf8");
|
|
44877
44915
|
} catch {
|
|
44878
|
-
return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with.
|
|
44916
|
+
return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
|
|
44879
44917
|
}
|
|
44880
44918
|
const names = [];
|
|
44881
44919
|
let inPreset = false;
|
|
@@ -44893,9 +44931,9 @@ function exportPresetRefusal(projectPath, preset) {
|
|
|
44893
44931
|
if (names.includes(preset)) {
|
|
44894
44932
|
return null;
|
|
44895
44933
|
}
|
|
44896
|
-
return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with.
|
|
44934
|
+
return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with. project_export add_preset adds one.` : `This project has no export preset named '${preset}'. Its presets are ${names.map((name) => `'${name}'`).join(", ")}.`;
|
|
44897
44935
|
}
|
|
44898
|
-
function exportAnswer(ending, asked, before, after) {
|
|
44936
|
+
function exportAnswer(ending, asked, before, after, beside = []) {
|
|
44899
44937
|
const { log } = ending;
|
|
44900
44938
|
const written = after !== null && after !== before;
|
|
44901
44939
|
const notCopied = librariesNotCopied(log.all);
|
|
@@ -44907,6 +44945,7 @@ function exportAnswer(ending, asked, before, after) {
|
|
|
44907
44945
|
failure: ending.failure ?? undefined,
|
|
44908
44946
|
errors: log.count("error"),
|
|
44909
44947
|
warnings: log.count("warning"),
|
|
44948
|
+
...beside.length === 0 ? {} : { alsoWritten: beside },
|
|
44910
44949
|
...notCopied.length === 0 ? {} : {
|
|
44911
44950
|
librariesNotLoaded: notCopied,
|
|
44912
44951
|
extensionNote: `The export could not load ${notCopied.join(" and ")}: on Windows an engine running as the editor, which an export is, loads a GDExtension from a ~ copy beside the library, and another engine on this project held that copy: it could not be moved aside before the export, or another engine made a new one in between. Export again once that engine has loaded or exited.`
|
|
@@ -44929,6 +44968,20 @@ function exportAnswer(ending, asked, before, after) {
|
|
|
44929
44968
|
isError: true
|
|
44930
44969
|
};
|
|
44931
44970
|
}
|
|
44971
|
+
function writtenBeside(before, after, output, within) {
|
|
44972
|
+
return [...after].filter(([name, at]) => name !== output && before.get(name) !== at).map(([name]) => within === "." ? name : `${within}/${name}`).sort();
|
|
44973
|
+
}
|
|
44974
|
+
function filesIn(folder) {
|
|
44975
|
+
const found = new Map;
|
|
44976
|
+
try {
|
|
44977
|
+
for (const entry of readdirSync13(folder, { withFileTypes: true })) {
|
|
44978
|
+
if (entry.isFile()) {
|
|
44979
|
+
found.set(entry.name, statSync13(join30(folder, entry.name)).mtimeMs);
|
|
44980
|
+
}
|
|
44981
|
+
}
|
|
44982
|
+
} catch {}
|
|
44983
|
+
return found;
|
|
44984
|
+
}
|
|
44932
44985
|
function scanWaitAnswer(scanned, started = "game") {
|
|
44933
44986
|
if (scanned.waitedMs === 0) {
|
|
44934
44987
|
return {};
|
|
@@ -46163,6 +46216,9 @@ class GodotServer {
|
|
|
46163
46216
|
if (tool === "project_import" && op === "set_options") {
|
|
46164
46217
|
return await this.handleSetImportOptions(args);
|
|
46165
46218
|
}
|
|
46219
|
+
if (tool === "project_export" && op === "add_preset") {
|
|
46220
|
+
return await this.handleAddExportPreset(args);
|
|
46221
|
+
}
|
|
46166
46222
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
46167
46223
|
if (headless !== undefined) {
|
|
46168
46224
|
const { from: asked, ...answerable } = args;
|
|
@@ -46220,7 +46276,11 @@ class GodotServer {
|
|
|
46220
46276
|
case "scene_create":
|
|
46221
46277
|
return op === "create" ? await bridge("create_scene") : await bridge("save_scene");
|
|
46222
46278
|
case "scene_tree":
|
|
46223
|
-
return await this.readFromFile(args, "scenePath", "read_scene_tree",
|
|
46279
|
+
return await this.readFromFile(args, "scenePath", "read_scene_tree", (file) => ({
|
|
46280
|
+
scenePath: file,
|
|
46281
|
+
depth: readNumber(args, "depth") ?? -1,
|
|
46282
|
+
includeProperties: readBoolean(args, "includeProperties") ?? false
|
|
46283
|
+
}), async () => await bridge("list_scene_nodes"));
|
|
46224
46284
|
case "scene_node":
|
|
46225
46285
|
switch (op) {
|
|
46226
46286
|
case "add":
|
|
@@ -46250,9 +46310,17 @@ class GodotServer {
|
|
|
46250
46310
|
case "scene_animation":
|
|
46251
46311
|
switch (op) {
|
|
46252
46312
|
case "read":
|
|
46253
|
-
return await
|
|
46313
|
+
return await this.readFromFile(args, "scenePath", "read_animations", (file) => ({
|
|
46314
|
+
scenePath: file,
|
|
46315
|
+
playerNodePath: readNonEmptyString(args, "playerNodePath") ?? ".",
|
|
46316
|
+
animationName: readString(args, "animationName") ?? ""
|
|
46317
|
+
}), async () => await bridge("read_animations"));
|
|
46254
46318
|
case "read_states":
|
|
46255
|
-
return await
|
|
46319
|
+
return await this.readFromFile(args, "scenePath", "read_animation_states", (file) => ({
|
|
46320
|
+
scenePath: file,
|
|
46321
|
+
animTreePath: readString(args, "animTreePath") ?? "",
|
|
46322
|
+
stateMachinePath: readString(args, "stateMachinePath") ?? ""
|
|
46323
|
+
}), async () => await bridge("read_animation_states"));
|
|
46256
46324
|
case "create":
|
|
46257
46325
|
return await bridge("create_animation");
|
|
46258
46326
|
case "add_track":
|
|
@@ -46279,8 +46347,14 @@ class GodotServer {
|
|
|
46279
46347
|
switch (op) {
|
|
46280
46348
|
case "move":
|
|
46281
46349
|
return await this.handleMoveFile(args);
|
|
46282
|
-
case "read":
|
|
46283
|
-
|
|
46350
|
+
case "read": {
|
|
46351
|
+
const { file, inner } = keptInside(readString(args, "resourcePath") ?? "");
|
|
46352
|
+
const kept = inner === undefined ? {} : { resourcePath: file, subResource: inner };
|
|
46353
|
+
return await this.readFromFile({ ...args, ...kept }, "resourcePath", "read_resource", (found) => ({
|
|
46354
|
+
resourcePath: inner === undefined ? found : `${found}::${inner}`,
|
|
46355
|
+
includeDefaults: readBoolean(args, "includeDefaults") ?? false
|
|
46356
|
+
}), async () => await bridge("read_resource", kept));
|
|
46357
|
+
}
|
|
46284
46358
|
case "create":
|
|
46285
46359
|
return await bridge("create_resource");
|
|
46286
46360
|
case "modify":
|
|
@@ -47208,6 +47282,30 @@ class GodotServer {
|
|
|
47208
47282
|
return this.createErrorResponse(`Search failed: ${errorMessage(error)}`);
|
|
47209
47283
|
}
|
|
47210
47284
|
}
|
|
47285
|
+
async handleAddExportPreset(args) {
|
|
47286
|
+
const project = this.project(args);
|
|
47287
|
+
if (!project.ok) {
|
|
47288
|
+
return project.response;
|
|
47289
|
+
}
|
|
47290
|
+
const given = readNonEmptyString(args, "outputPath");
|
|
47291
|
+
const output = given === undefined ? null : resolveWithinProject(project.value.path, given);
|
|
47292
|
+
if (output !== null && !output.ok) {
|
|
47293
|
+
return this.createErrorResponse(output.reason, OUTPUT_PATH_SOLUTIONS);
|
|
47294
|
+
}
|
|
47295
|
+
const outcome = await this.operation("add_export_preset", {
|
|
47296
|
+
preset: readString(args, "preset") ?? "",
|
|
47297
|
+
platform: readString(args, "platform") ?? "",
|
|
47298
|
+
outputPath: output?.relativePath ?? ""
|
|
47299
|
+
}, project.value.path);
|
|
47300
|
+
if (!outcome.ok || !this.editorHasOpen(project.value.path)) {
|
|
47301
|
+
return this.answer(outcome);
|
|
47302
|
+
}
|
|
47303
|
+
return this.jsonTextResponse({
|
|
47304
|
+
...outcome.payload,
|
|
47305
|
+
editorNote: "The editor open on this project read export_presets.cfg when it started and does not list this preset, and saving presets in its Export dialog writes its own list over the file. editor_launch restart loads the file; an export through project_export run reads the file and needs no restart.",
|
|
47306
|
+
...engineExtras(outcome)
|
|
47307
|
+
});
|
|
47308
|
+
}
|
|
47211
47309
|
async handleExportProject(args) {
|
|
47212
47310
|
const project = this.project(args);
|
|
47213
47311
|
if (!project.ok) {
|
|
@@ -47216,11 +47314,14 @@ class GodotServer {
|
|
|
47216
47314
|
const preset = readNonEmptyString(args, "preset") ?? "";
|
|
47217
47315
|
const output = resolveWithinProject(project.value.path, readString(args, "outputPath") ?? "");
|
|
47218
47316
|
if (!output.ok) {
|
|
47219
|
-
return this.createErrorResponse(output.reason,
|
|
47317
|
+
return this.createErrorResponse(output.reason, OUTPUT_PATH_SOLUTIONS);
|
|
47220
47318
|
}
|
|
47221
47319
|
const noPreset = exportPresetRefusal(project.value.path, preset);
|
|
47222
47320
|
if (noPreset !== null) {
|
|
47223
|
-
return this.createErrorResponse(noPreset, [
|
|
47321
|
+
return this.createErrorResponse(noPreset, [
|
|
47322
|
+
"project_export list reads the presets the project has",
|
|
47323
|
+
"project_export add_preset adds one"
|
|
47324
|
+
]);
|
|
47224
47325
|
}
|
|
47225
47326
|
const engine = await this.engine();
|
|
47226
47327
|
if (!engine.ok) {
|
|
@@ -47247,6 +47348,7 @@ class GodotServer {
|
|
|
47247
47348
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
47248
47349
|
const writtenAt = () => existsSync23(output.absolutePath) ? statSync13(output.absolutePath).mtimeMs : null;
|
|
47249
47350
|
const before = writtenAt();
|
|
47351
|
+
const listedBefore = filesIn(folder);
|
|
47250
47352
|
const freed = freeLibraryCopies(project.value.path);
|
|
47251
47353
|
if (freed.setAside.length > 0 || freed.swept.length > 0) {
|
|
47252
47354
|
this.logDebug(`Before the export: set aside ${freed.setAside.join(", ")}; swept ${freed.swept.join(", ")}`);
|
|
@@ -47260,7 +47362,8 @@ class GodotServer {
|
|
|
47260
47362
|
if (ending.exitSignal === null && ending.exitCode === null) {
|
|
47261
47363
|
return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
|
|
47262
47364
|
}
|
|
47263
|
-
|
|
47365
|
+
const beside = writtenBeside(listedBefore, filesIn(folder), basename9(output.absolutePath), dirname22(output.relativePath).replaceAll("\\", "/"));
|
|
47366
|
+
return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt(), beside);
|
|
47264
47367
|
}
|
|
47265
47368
|
reportTestProgress(log) {
|
|
47266
47369
|
const progress = callProgress();
|
|
@@ -50670,7 +50773,7 @@ class GodotServer {
|
|
|
50670
50773
|
...leftChanged === undefined ? {} : { note: leftChanged }
|
|
50671
50774
|
});
|
|
50672
50775
|
}
|
|
50673
|
-
async readFromFile(args, pathKey, operation, throughTheEditor) {
|
|
50776
|
+
async readFromFile(args, pathKey, operation, paramsFor, throughTheEditor) {
|
|
50674
50777
|
const project = this.project(args);
|
|
50675
50778
|
if (!project.ok) {
|
|
50676
50779
|
return project.response;
|
|
@@ -50688,12 +50791,7 @@ class GodotServer {
|
|
|
50688
50791
|
if (file === null) {
|
|
50689
50792
|
return this.createErrorResponse(`${pathKey} must name a file inside the project.`);
|
|
50690
50793
|
}
|
|
50691
|
-
const
|
|
50692
|
-
scenePath: file,
|
|
50693
|
-
depth: readNumber(args, "depth") ?? -1,
|
|
50694
|
-
includeProperties: readBoolean(args, "includeProperties") ?? false
|
|
50695
|
-
} : { resourcePath: file, includeDefaults: readBoolean(args, "includeDefaults") ?? false };
|
|
50696
|
-
const outcome = await this.operation(operation, params, projectPath);
|
|
50794
|
+
const outcome = await this.operation(operation, paramsFor(file), projectPath);
|
|
50697
50795
|
if (!outcome.ok) {
|
|
50698
50796
|
return this.answer(outcome);
|
|
50699
50797
|
}
|
|
@@ -51355,7 +51453,7 @@ async function runGodotServer() {
|
|
|
51355
51453
|
const server = new GodotServer;
|
|
51356
51454
|
await server.run();
|
|
51357
51455
|
}
|
|
51358
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
51456
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
51359
51457
|
var init_server = __esm(() => {
|
|
51360
51458
|
init_dist();
|
|
51361
51459
|
init_stdio();
|
|
@@ -51437,6 +51535,9 @@ var init_server = __esm(() => {
|
|
|
51437
51535
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
51438
51536
|
"Point projectPath at the project the file belongs to"
|
|
51439
51537
|
];
|
|
51538
|
+
OUTPUT_PATH_SOLUTIONS = [
|
|
51539
|
+
'Give outputPath inside the project, relative to it, such as "build/game.exe" or "res://build/game.exe"; copy the build elsewhere once it is exported'
|
|
51540
|
+
];
|
|
51440
51541
|
PROJECT_FILE_ARGUMENTS = [
|
|
51441
51542
|
"scenePath",
|
|
51442
51543
|
"instancePath",
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Animations and state machines read off their nodes: by the editor's tools from the scene it holds,
|
|
4
|
+
## and by a headless engine with none connected from a bare node of the class given what the file
|
|
5
|
+
## stores, which runs no script of the scene's, so a read answers the same either way.
|
|
6
|
+
|
|
7
|
+
const FileReads = preload("file_reads.gd")
|
|
8
|
+
const Serialisation = preload("serialisation.gd")
|
|
9
|
+
|
|
10
|
+
const LOOP_MODE_NAMES: Dictionary[int, String] = {
|
|
11
|
+
Animation.LOOP_NONE: "none",
|
|
12
|
+
Animation.LOOP_LINEAR: "linear",
|
|
13
|
+
Animation.LOOP_PINGPONG: "pingpong",
|
|
14
|
+
}
|
|
15
|
+
const TRACK_TYPE_NAMES: Dictionary[int, String] = {
|
|
16
|
+
Animation.TYPE_VALUE: "property",
|
|
17
|
+
Animation.TYPE_POSITION_3D: "position_3d",
|
|
18
|
+
Animation.TYPE_ROTATION_3D: "rotation_3d",
|
|
19
|
+
Animation.TYPE_SCALE_3D: "scale_3d",
|
|
20
|
+
Animation.TYPE_BLEND_SHAPE: "blend_shape",
|
|
21
|
+
Animation.TYPE_METHOD: "method",
|
|
22
|
+
Animation.TYPE_BEZIER: "bezier",
|
|
23
|
+
Animation.TYPE_AUDIO: "audio",
|
|
24
|
+
Animation.TYPE_ANIMATION: "animation",
|
|
25
|
+
}
|
|
26
|
+
const SWITCH_MODE_NAMES: Dictionary[int, String] = {
|
|
27
|
+
AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE: "immediate",
|
|
28
|
+
AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC: "sync",
|
|
29
|
+
AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END: "at_end",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## [method animations] of the AnimationPlayer at [param player_path] in the file [param scene_path].
|
|
34
|
+
static func scene_animations(scene_path: String, player_path: String, animation_name: String) -> Dictionary:
|
|
35
|
+
var made: Array = _bare(scene_path, player_path, "AnimationPlayer")
|
|
36
|
+
if made[0] == null:
|
|
37
|
+
return made[1]
|
|
38
|
+
var player: AnimationPlayer = made[0]
|
|
39
|
+
var answer: Dictionary = animations(player, animation_name)
|
|
40
|
+
player.free()
|
|
41
|
+
return answer
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## [method states] of the AnimationTree at [param tree_path] in the file [param scene_path].
|
|
45
|
+
static func scene_states(scene_path: String, tree_path: String, state_machine_path: String) -> Dictionary:
|
|
46
|
+
var made: Array = _bare(scene_path, tree_path, "AnimationTree")
|
|
47
|
+
if made[0] == null:
|
|
48
|
+
return made[1]
|
|
49
|
+
var tree: AnimationTree = made[0]
|
|
50
|
+
var answer: Dictionary = states(tree, state_machine_path)
|
|
51
|
+
tree.free()
|
|
52
|
+
return answer
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## A node of the class the file gives the node at [param node_path], holding every property the
|
|
56
|
+
## files set but its script, as [node, null] or [null, refusal]. The caller frees it.
|
|
57
|
+
static func _bare(scene_path: String, node_path: String, kind: String) -> Array:
|
|
58
|
+
var read: Dictionary = FileReads.scene_node(scene_path, node_path)
|
|
59
|
+
if not read.is_empty() and not read.get("ok", false):
|
|
60
|
+
return [null, read]
|
|
61
|
+
var type: String = str(read.get("type", ""))
|
|
62
|
+
if not ClassDB.class_exists(type) or not ClassDB.is_parent_class(type, kind):
|
|
63
|
+
return [null, {"ok": false, "error": "%s not found at: %s" % [kind, node_path]}]
|
|
64
|
+
var bare: Node = ClassDB.instantiate(type)
|
|
65
|
+
bare.name = str(read["name"])
|
|
66
|
+
var given: Dictionary = read["properties"]
|
|
67
|
+
for property: Variant in given:
|
|
68
|
+
if str(property) != "script":
|
|
69
|
+
bare.set(str(property), given[property])
|
|
70
|
+
return [bare, null]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## The animations of [param player] with their lengths, or [param animation_name] whole when it is
|
|
74
|
+
## given. A track comes back in the shape add_track takes, so one read here can be written to
|
|
75
|
+
## another animation as it stands.
|
|
76
|
+
static func animations(player: AnimationPlayer, animation_name: String) -> Dictionary:
|
|
77
|
+
if animation_name.is_empty():
|
|
78
|
+
var listed: Array[Dictionary] = []
|
|
79
|
+
for called: String in player.get_animation_list():
|
|
80
|
+
var listed_one: Animation = player.get_animation(StringName(called))
|
|
81
|
+
var summary: Dictionary = _summary(listed_one)
|
|
82
|
+
summary["animationName"] = called
|
|
83
|
+
summary["tracks"] = listed_one.get_track_count()
|
|
84
|
+
listed.append(summary)
|
|
85
|
+
return {"ok": true, "animations": listed}
|
|
86
|
+
if not player.has_animation(StringName(animation_name)):
|
|
87
|
+
var names: PackedStringArray = player.get_animation_list()
|
|
88
|
+
return {
|
|
89
|
+
"ok": false,
|
|
90
|
+
"error":
|
|
91
|
+
(
|
|
92
|
+
"%s has no animation %s. It has: %s."
|
|
93
|
+
% [player.name, animation_name, ", ".join(names) if not names.is_empty() else "none"]
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
var anim: Animation = player.get_animation(StringName(animation_name))
|
|
97
|
+
var answer: Dictionary = _summary(anim)
|
|
98
|
+
answer["ok"] = true
|
|
99
|
+
answer["animationName"] = animation_name
|
|
100
|
+
var tracks: Array[Dictionary] = []
|
|
101
|
+
var values: Serialisation = Serialisation.new()
|
|
102
|
+
for track: int in anim.get_track_count():
|
|
103
|
+
tracks.append(_track(anim, track, values))
|
|
104
|
+
answer["tracks"] = tracks
|
|
105
|
+
return answer
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
static func _summary(anim: Animation) -> Dictionary:
|
|
109
|
+
return {
|
|
110
|
+
"length": anim.length,
|
|
111
|
+
"loopMode": LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
|
|
112
|
+
"step": anim.step,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
static func _track(anim: Animation, track: int, values: Serialisation) -> Dictionary:
|
|
117
|
+
var kind: int = anim.track_get_type(track)
|
|
118
|
+
var path: NodePath = anim.track_get_path(track)
|
|
119
|
+
var shaped: Dictionary = {
|
|
120
|
+
"type": TRACK_TYPE_NAMES.get(kind, str(kind)), "nodePath": str(path.get_concatenated_names())
|
|
121
|
+
}
|
|
122
|
+
var property: String = str(path.get_concatenated_subnames())
|
|
123
|
+
if not property.is_empty():
|
|
124
|
+
shaped["property"] = property
|
|
125
|
+
var keyframes: Array[Dictionary] = []
|
|
126
|
+
var methods: Dictionary = {}
|
|
127
|
+
for key: int in anim.track_get_key_count(track):
|
|
128
|
+
var keyframe: Dictionary = {"time": anim.track_get_key_time(track, key)}
|
|
129
|
+
if kind == Animation.TYPE_METHOD:
|
|
130
|
+
var method: String = str(anim.method_track_get_name(track, key))
|
|
131
|
+
methods[method] = true
|
|
132
|
+
keyframe["method"] = method
|
|
133
|
+
keyframe["args"] = values.serialize_value(anim.method_track_get_params(track, key))
|
|
134
|
+
else:
|
|
135
|
+
keyframe["value"] = values.serialize_value(anim.track_get_key_value(track, key))
|
|
136
|
+
keyframes.append(keyframe)
|
|
137
|
+
# One method across every key is the track's, as add_track takes it. Keys calling different
|
|
138
|
+
# methods, which the editor can make and add_track cannot, keep their own.
|
|
139
|
+
if methods.size() == 1:
|
|
140
|
+
shaped["method"] = methods.keys()[0]
|
|
141
|
+
for keyframe: Dictionary in keyframes:
|
|
142
|
+
var _shared: bool = keyframe.erase("method")
|
|
143
|
+
shaped["keyframes"] = keyframes
|
|
144
|
+
return shaped
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
## The state machine at a slash-separated path of nested state machine names in [param tree], or
|
|
148
|
+
## null when any step of the path is not one. Every node in a tree of state machines is a state
|
|
149
|
+
## machine itself, so the walk asks each step for the child by name and refuses anything else.
|
|
150
|
+
static func state_machine(tree: AnimationTree, state_machine_path: String) -> AnimationNodeStateMachine:
|
|
151
|
+
if tree == null:
|
|
152
|
+
return null
|
|
153
|
+
var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
|
|
154
|
+
if state_machine_path == "root":
|
|
155
|
+
return current
|
|
156
|
+
for segment: String in state_machine_path.split("/", false):
|
|
157
|
+
if current == null or not current.has_node(StringName(segment)):
|
|
158
|
+
return null
|
|
159
|
+
current = current.get_node(StringName(segment)) as AnimationNodeStateMachine
|
|
160
|
+
return current
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
## Why [method state_machine] found none at [param state_machine_path] in [param tree]: the step of
|
|
164
|
+
## the path that failed, and what is there instead.
|
|
165
|
+
static func no_machine(tree: AnimationTree, state_machine_path: String) -> String:
|
|
166
|
+
if tree == null or tree.tree_root == null:
|
|
167
|
+
return "The AnimationTree has no tree_root."
|
|
168
|
+
var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
|
|
169
|
+
if current == null:
|
|
170
|
+
return (
|
|
171
|
+
"The AnimationTree's tree_root is not a state machine: its type is %s."
|
|
172
|
+
% tree.tree_root.get_class()
|
|
173
|
+
)
|
|
174
|
+
var walked: PackedStringArray = []
|
|
175
|
+
for segment: String in state_machine_path.split("/", false):
|
|
176
|
+
var where: String = "the root state machine"
|
|
177
|
+
if not walked.is_empty():
|
|
178
|
+
where = "the state machine at " + "/".join(walked)
|
|
179
|
+
if not current.has_node(StringName(segment)):
|
|
180
|
+
var names: PackedStringArray = []
|
|
181
|
+
for state: StringName in current.get_node_list():
|
|
182
|
+
if state != &"Start" and state != &"End":
|
|
183
|
+
var _listed: bool = names.push_back(str(state))
|
|
184
|
+
var held: String = ", ".join(names) if not names.is_empty() else "none"
|
|
185
|
+
return (
|
|
186
|
+
"%s has no state %s. Its states are: %s."
|
|
187
|
+
% [where.to_upper()[0] + where.substr(1), segment, held]
|
|
188
|
+
)
|
|
189
|
+
var next: AnimationNode = current.get_node(StringName(segment))
|
|
190
|
+
if not next is AnimationNodeStateMachine:
|
|
191
|
+
return (
|
|
192
|
+
"The state %s in %s is not a state machine: its type is %s."
|
|
193
|
+
% [segment, where, next.get_class()]
|
|
194
|
+
)
|
|
195
|
+
current = next
|
|
196
|
+
var _walked: bool = walked.push_back(segment)
|
|
197
|
+
return "No state machine at " + state_machine_path
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
## The states and transitions of the state machine at [param state_machine_path] in [param tree].
|
|
201
|
+
static func states(tree: AnimationTree, state_machine_path: String) -> Dictionary:
|
|
202
|
+
var sm: AnimationNodeStateMachine = state_machine(tree, state_machine_path)
|
|
203
|
+
if sm == null:
|
|
204
|
+
return {"ok": false, "error": no_machine(tree, state_machine_path)}
|
|
205
|
+
var listed: Array[Dictionary] = []
|
|
206
|
+
for state: StringName in sm.get_node_list():
|
|
207
|
+
var node: AnimationNode = sm.get_node(state)
|
|
208
|
+
var entry: Dictionary = {"stateName": str(state), "type": node.get_class()}
|
|
209
|
+
if node is AnimationNodeAnimation:
|
|
210
|
+
var plays: AnimationNodeAnimation = node
|
|
211
|
+
entry["animationName"] = str(plays.animation)
|
|
212
|
+
listed.append(entry)
|
|
213
|
+
var transitions: Array[Dictionary] = []
|
|
214
|
+
for index: int in sm.get_transition_count():
|
|
215
|
+
var transition: AnimationNodeStateMachineTransition = sm.get_transition(index)
|
|
216
|
+
var shaped: Dictionary = {
|
|
217
|
+
"from": str(sm.get_transition_from(index)),
|
|
218
|
+
"to": str(sm.get_transition_to(index)),
|
|
219
|
+
"transitionType": SWITCH_MODE_NAMES.get(transition.switch_mode, str(transition.switch_mode)),
|
|
220
|
+
}
|
|
221
|
+
if not str(transition.advance_condition).is_empty():
|
|
222
|
+
shaped["advanceCondition"] = str(transition.advance_condition)
|
|
223
|
+
transitions.append(shaped)
|
|
224
|
+
return {"ok": true, "states": listed, "transitions": transitions}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://c3calvdgcggh9
|