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 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: "additive",
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. Needs the editor connected.",
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: { type: "string", ops: ["add_state"], description: "add_state: the state to add." },
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", async () => await bridge("list_scene_nodes"));
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 bridge("read_animations");
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 bridge("read_animation_states");
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
- return await this.readFromFile(args, "resourcePath", "read_resource", async () => await bridge("read_resource"));
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 params = operation === "read_scene_tree" ? {
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, "tree": _entry(nodes, ".", include_properties, depth, 0)}
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
- if not FileAccess.file_exists(res_path):
197
- return {"ok": false, "error": "No resource at " + res_path}
198
- var loaded: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
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." % res_path}
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