gdharness 1.2.32 → 1.2.34
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 +49 -20
- 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/file_reads.gd +76 -5
- package/build/godot/operations/godot_operations.gd +17 -0
- package/build/godot/operations/input_actions.gd +51 -7
- package/build/index.js +48 -19
- 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
|
|
@@ -11308,7 +11321,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11308
11321
|
{
|
|
11309
11322
|
name: "project_settings",
|
|
11310
11323
|
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.",
|
|
11324
|
+
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
11325
|
parameters: {
|
|
11313
11326
|
projectPath: PROJECT_PATH,
|
|
11314
11327
|
setting: {
|
|
@@ -11406,8 +11419,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
11406
11419
|
requires: ["scenePath"]
|
|
11407
11420
|
},
|
|
11408
11421
|
add_input_action: {
|
|
11409
|
-
effect: "
|
|
11410
|
-
summary: "register an input action and its events",
|
|
11422
|
+
effect: "destructive",
|
|
11423
|
+
summary: "register an input action and its events, or rebind one that exists",
|
|
11411
11424
|
requires: ["actionName", "events"]
|
|
11412
11425
|
},
|
|
11413
11426
|
remove_input_action: {
|
|
@@ -11755,7 +11768,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11755
11768
|
{
|
|
11756
11769
|
name: "scene_animation",
|
|
11757
11770
|
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.
|
|
11771
|
+
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
11772
|
parameters: {
|
|
11760
11773
|
projectPath: PROJECT_PATH,
|
|
11761
11774
|
scenePath: SCENE_PATH,
|
|
@@ -11787,7 +11800,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
11787
11800
|
ops: ["read_states", "add_state", "connect_states"],
|
|
11788
11801
|
description: "The AnimationTree node."
|
|
11789
11802
|
},
|
|
11790
|
-
stateName: {
|
|
11803
|
+
stateName: {
|
|
11804
|
+
type: "string",
|
|
11805
|
+
ops: ["add_state"],
|
|
11806
|
+
description: "add_state: the state to add. It goes right of the rightmost state in the editor's graph, answered under position."
|
|
11807
|
+
},
|
|
11791
11808
|
stateMachinePath: {
|
|
11792
11809
|
type: "string",
|
|
11793
11810
|
ops: ["read_states", "add_state", "connect_states"],
|
|
@@ -11962,7 +11979,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11962
11979
|
projectPath: PROJECT_PATH,
|
|
11963
11980
|
resourcePath: {
|
|
11964
11981
|
...RESOURCE_PATH,
|
|
11965
|
-
description: `${RESOURCE_FILE} move: a directory too.`
|
|
11982
|
+
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
11983
|
},
|
|
11967
11984
|
resourceType: {
|
|
11968
11985
|
type: "string",
|
|
@@ -46220,7 +46237,11 @@ class GodotServer {
|
|
|
46220
46237
|
case "scene_create":
|
|
46221
46238
|
return op === "create" ? await bridge("create_scene") : await bridge("save_scene");
|
|
46222
46239
|
case "scene_tree":
|
|
46223
|
-
return await this.readFromFile(args, "scenePath", "read_scene_tree",
|
|
46240
|
+
return await this.readFromFile(args, "scenePath", "read_scene_tree", (file) => ({
|
|
46241
|
+
scenePath: file,
|
|
46242
|
+
depth: readNumber(args, "depth") ?? -1,
|
|
46243
|
+
includeProperties: readBoolean(args, "includeProperties") ?? false
|
|
46244
|
+
}), async () => await bridge("list_scene_nodes"));
|
|
46224
46245
|
case "scene_node":
|
|
46225
46246
|
switch (op) {
|
|
46226
46247
|
case "add":
|
|
@@ -46250,9 +46271,17 @@ class GodotServer {
|
|
|
46250
46271
|
case "scene_animation":
|
|
46251
46272
|
switch (op) {
|
|
46252
46273
|
case "read":
|
|
46253
|
-
return await
|
|
46274
|
+
return await this.readFromFile(args, "scenePath", "read_animations", (file) => ({
|
|
46275
|
+
scenePath: file,
|
|
46276
|
+
playerNodePath: readNonEmptyString(args, "playerNodePath") ?? ".",
|
|
46277
|
+
animationName: readString(args, "animationName") ?? ""
|
|
46278
|
+
}), async () => await bridge("read_animations"));
|
|
46254
46279
|
case "read_states":
|
|
46255
|
-
return await
|
|
46280
|
+
return await this.readFromFile(args, "scenePath", "read_animation_states", (file) => ({
|
|
46281
|
+
scenePath: file,
|
|
46282
|
+
animTreePath: readString(args, "animTreePath") ?? "",
|
|
46283
|
+
stateMachinePath: readString(args, "stateMachinePath") ?? ""
|
|
46284
|
+
}), async () => await bridge("read_animation_states"));
|
|
46256
46285
|
case "create":
|
|
46257
46286
|
return await bridge("create_animation");
|
|
46258
46287
|
case "add_track":
|
|
@@ -46279,8 +46308,14 @@ class GodotServer {
|
|
|
46279
46308
|
switch (op) {
|
|
46280
46309
|
case "move":
|
|
46281
46310
|
return await this.handleMoveFile(args);
|
|
46282
|
-
case "read":
|
|
46283
|
-
|
|
46311
|
+
case "read": {
|
|
46312
|
+
const { file, inner } = keptInside(readString(args, "resourcePath") ?? "");
|
|
46313
|
+
const kept = inner === undefined ? {} : { resourcePath: file, subResource: inner };
|
|
46314
|
+
return await this.readFromFile({ ...args, ...kept }, "resourcePath", "read_resource", (found) => ({
|
|
46315
|
+
resourcePath: inner === undefined ? found : `${found}::${inner}`,
|
|
46316
|
+
includeDefaults: readBoolean(args, "includeDefaults") ?? false
|
|
46317
|
+
}), async () => await bridge("read_resource", kept));
|
|
46318
|
+
}
|
|
46284
46319
|
case "create":
|
|
46285
46320
|
return await bridge("create_resource");
|
|
46286
46321
|
case "modify":
|
|
@@ -50670,7 +50705,7 @@ class GodotServer {
|
|
|
50670
50705
|
...leftChanged === undefined ? {} : { note: leftChanged }
|
|
50671
50706
|
});
|
|
50672
50707
|
}
|
|
50673
|
-
async readFromFile(args, pathKey, operation, throughTheEditor) {
|
|
50708
|
+
async readFromFile(args, pathKey, operation, paramsFor, throughTheEditor) {
|
|
50674
50709
|
const project = this.project(args);
|
|
50675
50710
|
if (!project.ok) {
|
|
50676
50711
|
return project.response;
|
|
@@ -50688,17 +50723,11 @@ class GodotServer {
|
|
|
50688
50723
|
if (file === null) {
|
|
50689
50724
|
return this.createErrorResponse(`${pathKey} must name a file inside the project.`);
|
|
50690
50725
|
}
|
|
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);
|
|
50726
|
+
const outcome = await this.operation(operation, paramsFor(file), projectPath);
|
|
50697
50727
|
if (!outcome.ok) {
|
|
50698
50728
|
return this.answer(outcome);
|
|
50699
50729
|
}
|
|
50700
50730
|
return this.jsonTextResponse({
|
|
50701
|
-
ok: true,
|
|
50702
50731
|
...outcome.payload,
|
|
50703
50732
|
readBy: "a headless engine, since no editor serves this project; unsaved changes in an editor are not seen",
|
|
50704
50733
|
...engineExtras(outcome)
|
|
@@ -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
|
|
@@ -19,6 +19,30 @@ const Serialisation = preload("serialisation.gd")
|
|
|
19
19
|
## instances another scene and every stored property when [param include_properties] is set, the
|
|
20
20
|
## file's value or the default where it sets none. [param depth] -1 reads every level.
|
|
21
21
|
static func scene_tree(scene_path: String, depth: int, include_properties: bool) -> Dictionary:
|
|
22
|
+
var laid: Dictionary = _laid(scene_path)
|
|
23
|
+
if not laid.get("ok", false):
|
|
24
|
+
return laid
|
|
25
|
+
var nodes: Dictionary = laid["nodes"]
|
|
26
|
+
return {"ok": true, "tree": _entry(nodes, ".", include_properties, depth, 0)}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## The node at [param node_path] in [param scene_path] as {ok, name, type, properties}, laid as
|
|
30
|
+
## [method scene_tree] lays it, with only the properties the files set; {} when the scene has no
|
|
31
|
+
## node there, and a refusal when the file does not read as a scene.
|
|
32
|
+
static func scene_node(scene_path: String, node_path: String) -> Dictionary:
|
|
33
|
+
var laid: Dictionary = _laid(scene_path)
|
|
34
|
+
if not laid.get("ok", false):
|
|
35
|
+
return laid
|
|
36
|
+
var nodes: Dictionary = laid["nodes"]
|
|
37
|
+
var path: String = _joined(".", node_path)
|
|
38
|
+
if not nodes.has(path):
|
|
39
|
+
return {}
|
|
40
|
+
var found: Dictionary = nodes[path]
|
|
41
|
+
return {"ok": true, "name": found["name"], "type": found["type"], "properties": found["properties"]}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Every node of [param scene_path] as {ok, nodes}, keyed by path, or a refusal.
|
|
45
|
+
static func _laid(scene_path: String) -> Dictionary:
|
|
22
46
|
if not FileAccess.file_exists(scene_path):
|
|
23
47
|
return {"ok": false, "error": "Scene not found: " + scene_path}
|
|
24
48
|
# Loaded without naming the type, so a resource handed in by mistake loads as what it is and the
|
|
@@ -42,7 +66,7 @@ static func scene_tree(scene_path: String, depth: int, include_properties: bool)
|
|
|
42
66
|
return {"ok": false, "error": refused}
|
|
43
67
|
if not nodes.has("."):
|
|
44
68
|
return {"ok": false, "error": "%s holds no nodes" % scene_path}
|
|
45
|
-
return {"ok": true, "
|
|
69
|
+
return {"ok": true, "nodes": nodes}
|
|
46
70
|
|
|
47
71
|
|
|
48
72
|
## Lays the nodes of [param state] into [param nodes] under [param prefix], keyed by their path
|
|
@@ -192,12 +216,22 @@ static func _stored(type: String, given: Dictionary) -> Dictionary:
|
|
|
192
216
|
## What a resource file holds, in the form modify takes, so a value read here can be written back.
|
|
193
217
|
## Read from the file rather than a cached copy, since the file is what the writes change and what
|
|
194
218
|
## the project loads.
|
|
219
|
+
##
|
|
220
|
+
## [param res_path] may name a resource kept inside a file, as "res://hero.tscn::Animation_jump",
|
|
221
|
+
## the path a property holding one is read back with.
|
|
195
222
|
static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
223
|
+
var file: String = res_path.get_slice("::", 0)
|
|
224
|
+
if not FileAccess.file_exists(file):
|
|
225
|
+
return {"ok": false, "error": "No resource at " + file}
|
|
226
|
+
var loaded: Resource = ResourceLoader.load(file, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
199
227
|
if loaded == null:
|
|
200
|
-
return {"ok": false, "error": "%s did not load as a resource." %
|
|
228
|
+
return {"ok": false, "error": "%s did not load as a resource." % file}
|
|
229
|
+
if file != res_path:
|
|
230
|
+
var id: String = res_path.trim_prefix(file + "::")
|
|
231
|
+
var inner: Resource = _kept_in(loaded, file, id, {})
|
|
232
|
+
if inner == null:
|
|
233
|
+
return {"ok": false, "error": "%s keeps no resource with the id %s." % [file, id]}
|
|
234
|
+
loaded = inner
|
|
201
235
|
# A scene's own properties are its packed bundle, which says nothing a caller can use.
|
|
202
236
|
if loaded is PackedScene:
|
|
203
237
|
return {
|
|
@@ -235,3 +269,40 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
235
269
|
if script != null:
|
|
236
270
|
answer["script"] = script.resource_path
|
|
237
271
|
return answer
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
## The resource [param file] keeps under [param id], found in [param value] or anything it holds.
|
|
275
|
+
## The engine loads such a path only while the file is cached, and a read loads the file uncached,
|
|
276
|
+
## so it is looked for among what the file holds. A resource of another file is neither taken nor
|
|
277
|
+
## walked into: ids are unique only within one file, and a scene instancing another holds that
|
|
278
|
+
## file's resources under ids of the other file's.
|
|
279
|
+
static func _kept_in(value: Variant, file: String, id: String, seen: Dictionary) -> Resource:
|
|
280
|
+
if value is Resource:
|
|
281
|
+
var held: Resource = value
|
|
282
|
+
if seen.has(held.get_instance_id()):
|
|
283
|
+
return null
|
|
284
|
+
seen[held.get_instance_id()] = true
|
|
285
|
+
var at: String = held.resource_path
|
|
286
|
+
if not at.is_empty() and at.get_slice("::", 0) != file:
|
|
287
|
+
return null
|
|
288
|
+
if held.resource_scene_unique_id == id:
|
|
289
|
+
return held
|
|
290
|
+
for p: Dictionary in held.get_property_list():
|
|
291
|
+
if Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
292
|
+
var found: Resource = _kept_in(held.get(str(p.get("name", ""))), file, id, seen)
|
|
293
|
+
if found != null:
|
|
294
|
+
return found
|
|
295
|
+
elif value is Array:
|
|
296
|
+
var items: Array = value
|
|
297
|
+
for item: Variant in items:
|
|
298
|
+
var found: Resource = _kept_in(item, file, id, seen)
|
|
299
|
+
if found != null:
|
|
300
|
+
return found
|
|
301
|
+
elif value is Dictionary:
|
|
302
|
+
var entries: Dictionary = value
|
|
303
|
+
for key: Variant in entries:
|
|
304
|
+
for side: Variant in [key, entries[key]]:
|
|
305
|
+
var found: Resource = _kept_in(side, file, id, seen)
|
|
306
|
+
if found != null:
|
|
307
|
+
return found
|
|
308
|
+
return null
|