gdharness 1.3.3 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/build/cli.js +105 -74
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +3 -2
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +157 -4
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
  7. package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
  8. package/build/godot/addons/gdharness_editor/property_values.gd +195 -221
  9. package/build/godot/addons/gdharness_editor/reading.gd +15 -0
  10. package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
  11. package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/scene_file.gd +97 -14
  13. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
  14. package/build/godot/addons/gdharness_editor/tool_executor.gd +9 -3
  15. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +38 -35
  16. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
  17. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
  18. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
  19. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +68 -2
  20. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +94 -333
  21. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
  22. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
  23. package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
  24. package/build/godot/addons/gdharness_runtime/runtime_input.gd +10 -26
  25. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
  26. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
  27. package/build/godot/addons/gdharness_runtime/runtime_values.gd +30 -0
  28. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
  29. package/build/godot/operations/animation_reads.gd +3 -2
  30. package/build/godot/operations/declared_values.gd +157 -4
  31. package/build/godot/operations/dependencies.gd +14 -39
  32. package/build/godot/operations/editor_tools.gd +4 -3
  33. package/build/godot/operations/file_reads.gd +15 -3
  34. package/build/godot/operations/gdscript_analysis.gd +229 -75
  35. package/build/godot/operations/gdscript_authoring.gd +142 -34
  36. package/build/godot/operations/import_pipeline.gd +17 -6
  37. package/build/godot/operations/logger.gd +7 -0
  38. package/build/godot/operations/property_paths.gd +225 -0
  39. package/build/godot/operations/property_values.gd +195 -221
  40. package/build/godot/operations/reading.gd +15 -0
  41. package/build/godot/operations/same_values.gd +177 -0
  42. package/build/godot/operations/serialisation.gd +75 -3
  43. package/package.json +1 -1
@@ -29,18 +29,6 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
29
29
  # =============================================================================
30
30
  # Shared helpers
31
31
  # =============================================================================
32
- func _ensure_res_path(path: String) -> String:
33
- if not path.begins_with("res://"):
34
- return "res://" + path
35
- return path
36
-
37
-
38
- func _find_node(root: Node, path: String) -> Node:
39
- if path == "." or path.is_empty():
40
- return root
41
- return root.get_node_or_null(path)
42
-
43
-
44
32
  ## [param scene_path] opened for writing, with the node at [param node_path] in it, which must be a
45
33
  ## [param kind] the scene owns. Answers [scene, node, refusal]; the refusal is {} when both were
46
34
  ## found.
@@ -49,7 +37,7 @@ func _open_at(scene_path: String, node_path: String, kind: String) -> Array:
49
37
  var refused: Dictionary = scene.open(scene_path, true)
50
38
  if not refused.is_empty():
51
39
  return [scene, null, refused]
52
- var node: Node = _find_node(scene.root, node_path)
40
+ var node: Node = GdhSceneFile.node_at(scene.root, node_path)
53
41
  if node == null or not node.is_class(kind):
54
42
  return [scene, null, _not_of_kind(scene, node, node_path, kind)]
55
43
  var foreign: String = scene.foreign(node)
@@ -65,7 +53,7 @@ func _read_at(scene_path: String, node_path: String, kind: String) -> Array:
65
53
  var refused: Dictionary = scene.open(scene_path, false)
66
54
  if not refused.is_empty():
67
55
  return [scene, null, refused]
68
- var node: Node = _find_node(scene.root, node_path)
56
+ var node: Node = GdhSceneFile.node_at(scene.root, node_path)
69
57
  if node == null or not node.is_class(kind):
70
58
  return [scene, null, _not_of_kind(scene, node, node_path, kind)]
71
59
  return [scene, node, {}]
@@ -233,7 +221,7 @@ func _animated(player: AnimationPlayer, track_path: String) -> Node:
233
221
  ## The animations of an AnimationPlayer, or one of them whole. A track comes back in the shape
234
222
  ## add_track takes, so one read here can be written to another animation as it stands.
235
223
  func read_animations(args: Dictionary) -> Dictionary:
236
- var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
224
+ var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
237
225
  var player_node_path: String = str(args.get("playerNodePath", "."))
238
226
  var animation_name: String = str(args.get("animationName", ""))
239
227
  if scene_path.strip_edges() == "res://":
@@ -258,7 +246,7 @@ func _read_in(scene: GdhSceneFile, answer: Dictionary) -> Dictionary:
258
246
 
259
247
  ## The states and transitions of a state machine in an AnimationTree.
260
248
  func read_animation_states(args: Dictionary) -> Dictionary:
261
- var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
249
+ var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
262
250
  var anim_tree_path: String = str(args.get("animTreePath", ""))
263
251
  var state_machine_path: String = str(args.get("stateMachinePath", ""))
264
252
  if scene_path.strip_edges() == "res://":
@@ -278,7 +266,7 @@ func read_animation_states(args: Dictionary) -> Dictionary:
278
266
  # create_animation
279
267
  # =============================================================================
280
268
  func create_animation(args: Dictionary) -> Dictionary:
281
- var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
269
+ var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
282
270
  var player_node_path: String = str(args.get("playerNodePath", "."))
283
271
  var animation_name: String = str(args.get("animationName", ""))
284
272
  var loop_mode_name: String = str(args.get("loopMode", "none"))
@@ -335,7 +323,7 @@ func create_animation(args: Dictionary) -> Dictionary:
335
323
  # add_animation_track
336
324
  # =============================================================================
337
325
  func add_animation_track(args: Dictionary) -> Dictionary:
338
- var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
326
+ var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
339
327
  var player_node_path: String = str(args.get("playerNodePath", "."))
340
328
  var animation_name: String = str(args.get("animationName", ""))
341
329
  var given_track: Variant = args.get("track", {})
@@ -553,7 +541,7 @@ func _key_values(target: Node, prop_name: String, keyframes: Array) -> Array:
553
541
  var base: String = prop_name.get_slice(":", 0)
554
542
  var declared: Dictionary = _properties.declaration(target, base)
555
543
  if declared.is_empty():
556
- return [false, "%s has no property %s" % [target.get_class(), base]]
544
+ return [false, GdhPropertyValues.no_property(target, base)]
557
545
  var values: Array = []
558
546
  for index: int in keyframes.size():
559
547
  var keyframe: Dictionary = keyframes[index]
@@ -612,7 +600,7 @@ func _animation_at(scene_path: String, player_node_path: String, animation_name:
612
600
  ## Changes the length, loop mode or step of an animation, naming any key a shorter length leaves
613
601
  ## past the end.
614
602
  func set_animation(args: Dictionary) -> Dictionary:
615
- var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
603
+ var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
616
604
  var player_node_path: String = str(args.get("playerNodePath", "."))
617
605
  var animation_name: String = str(args.get("animationName", ""))
618
606
  var loop_mode_name: String = str(args.get("loopMode", ""))
@@ -673,7 +661,7 @@ func set_animation(args: Dictionary) -> Dictionary:
673
661
  ## the scene's trees that plays it, since a state naming an animation the player lacks plays nothing
674
662
  ## and says so only in the editor's log.
675
663
  func rename_animation(args: Dictionary) -> Dictionary:
676
- var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
664
+ var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
677
665
  var player_node_path: String = str(args.get("playerNodePath", "."))
678
666
  var animation_name: String = str(args.get("animationName", ""))
679
667
  var new_name: String = str(args.get("newName", "")).strip_edges()
@@ -752,7 +740,7 @@ func rename_animation(args: Dictionary) -> Dictionary:
752
740
  ## Removes an animation, clearing the player's autoplay when it named it and naming the states of
753
741
  ## the scene's trees left playing nothing.
754
742
  func remove_animation(args: Dictionary) -> Dictionary:
755
- var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
743
+ var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
756
744
  var player_node_path: String = str(args.get("playerNodePath", "."))
757
745
  var animation_name: String = str(args.get("animationName", ""))
758
746
  if scene_path.strip_edges() == "res://":
@@ -844,7 +832,7 @@ static func _collect_playing(
844
832
  # add_animation_state
845
833
  # =============================================================================
846
834
  func add_animation_state(args: Dictionary) -> Dictionary:
847
- var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
835
+ var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
848
836
  var anim_tree_path: String = str(args.get("animTreePath", ""))
849
837
  var state_name: String = str(args.get("stateName", ""))
850
838
  var animation_name: String = str(args.get("animationName", ""))
@@ -872,15 +860,7 @@ func add_animation_state(args: Dictionary) -> Dictionary:
872
860
  if sm.has_node(StringName(state_name)):
873
861
  return scene.refuse("The state machine already has a state named %s." % state_name)
874
862
  var player: AnimationPlayer = anim_tree.get_node_or_null(anim_tree.anim_player) as AnimationPlayer
875
- if player != null and not player.has_animation(StringName(animation_name)):
876
- # By its path from the scene root, as playerNodePath takes it; anim_player is relative to the
877
- # tree ("../Anim"), which no argument takes.
878
- return scene.refuse(
879
- (
880
- "The AnimationPlayer at %s has no animation %s for the state to play."
881
- % [str(scene.root.get_path_to(player)), animation_name]
882
- )
883
- )
863
+ var found: bool = player != null and player.has_animation(StringName(animation_name))
884
864
 
885
865
  var anim_node: AnimationNodeAnimation = AnimationNodeAnimation.new()
886
866
  anim_node.animation = StringName(animation_name)
@@ -900,19 +880,42 @@ func add_animation_state(args: Dictionary) -> Dictionary:
900
880
  "ok": true,
901
881
  "stateName": state_name,
902
882
  "animationName": animation_name,
903
- # False when the tree has no player to look the animation up in.
904
- "animationFound": player != null,
883
+ # False when the tree has no player to look the animation up in, or its player has none of
884
+ # that name.
885
+ "animationFound": found,
905
886
  "position": GdhSerialisation.new().serialize_value(position),
906
887
  }
888
+ # Written all the same, as the editor writes one: a state machine is often laid out before its
889
+ # animations are made, and refusing it stopped a tree being built in that order.
890
+ if player != null and not found:
891
+ answer["animationMissing"] = _missing_animation(scene, player, animation_name)
907
892
  var files: Dictionary = machine[1]
908
893
  return _finish(scene, Callable(), files, answer)
909
894
 
910
895
 
896
+ ## Why the state playing [param animation_name] plays nothing yet. The player is named by its path
897
+ ## from the scene root, as playerNodePath takes it; anim_player is relative to the tree ("../Anim"),
898
+ ## which no argument takes.
899
+ static func _missing_animation(
900
+ scene: GdhSceneFile, player: AnimationPlayer, animation_name: String
901
+ ) -> String:
902
+ var said: String = (
903
+ "The AnimationPlayer at %s has no animation %s yet, so the state plays nothing until it has one."
904
+ % [str(scene.root.get_path_to(player)), animation_name]
905
+ )
906
+ var close: PackedStringArray = GdhRead.nearest(animation_name, player.get_animation_list())
907
+ if close.is_empty():
908
+ return said
909
+ return (
910
+ said + " The nearest %s %s." % ["it has is" if close.size() == 1 else "it has are", ", ".join(close)]
911
+ )
912
+
913
+
911
914
  # =============================================================================
912
915
  # connect_animation_states
913
916
  # =============================================================================
914
917
  func connect_animation_states(args: Dictionary) -> Dictionary:
915
- var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
918
+ var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
916
919
  var anim_tree_path: String = str(args.get("animTreePath", ""))
917
920
  var from_state: String = str(args.get("fromState", ""))
918
921
  var to_state: String = str(args.get("toState", ""))
@@ -7,6 +7,7 @@ const GdhFileReads = preload("../file_reads.gd")
7
7
  const GdhHeadlessCheck = preload("../headless_check.gd")
8
8
  const GdhPropertyValues = preload("../property_values.gd")
9
9
  const GdhRead = preload("../reading.gd")
10
+ const GdhSame = preload("../same_values.gd")
10
11
  const GdhSceneFile = preload("../scene_file.gd")
11
12
 
12
13
  # The minimal shader of a type: its declaration and the one function the engine calls for it.
@@ -136,7 +137,7 @@ func _save_checked(resource: Resource, path: String, names: Array, previous: Var
136
137
  for key: Variant in names:
137
138
  var property: String = str(key)
138
139
  var read: Variant = _properties.read_at(loaded, property)
139
- if not GdhPropertyValues.same(read, _properties.read_at(resource, property)):
140
+ if not GdhSame.same(read, _properties.read_at(resource, property)):
140
141
  lost = (
141
142
  (
142
143
  "%s.%s would load from the file as %s: it is not a property the file keeps, so "
@@ -182,7 +183,7 @@ func _unkept_headless(resource: Resource, path: String, names: Array, previous:
182
183
  var property: String = str(key)
183
184
  asked[property] = true
184
185
  asked[property.get_slice(":", 0)] = true
185
- reached.merge(GdhHeadlessCheck.reached(resource, property))
186
+ GdhHeadlessCheck.spare(reached, GdhHeadlessCheck.reached(resource, property))
186
187
  return GdhHeadlessCheck.settle_saved(
187
188
  path,
188
189
  before,
@@ -0,0 +1,91 @@
1
+ @tool
2
+ extends RefCounted
3
+
4
+ ## The groups a node of a scene is in. A scene's own file puts a node in a group and can take it
5
+ ## out of one it put it in; a group the scene that makes the node puts it in, the one it is
6
+ ## instanced from or one this scene inherits, can be changed only there, as the editor's group dock
7
+ ## shows it locked.
8
+
9
+ const GdhSceneFile = preload("../scene_file.gd")
10
+
11
+
12
+ ## The groups of [param node], sorted, as {groups, groupsHere}: groupsHere, those this scene's file
13
+ ## puts it in, is given only where another scene puts it in some of them. Empty for a node in none.
14
+ static func read(scene: GdhSceneFile, node: Node) -> Dictionary:
15
+ var all: Array = _sorted(node.get_groups())
16
+ if all.is_empty():
17
+ return {}
18
+ var answer: Dictionary = {"groups": all}
19
+ var here: Array = _sorted(scene.groups_at(scene.path_of(node)))
20
+ if here.size() != all.size():
21
+ answer["groupsHere"] = here
22
+ return answer
23
+
24
+
25
+ ## The group names under [param key] of [param args], which the server has checked are strings, or
26
+ ## none when not given.
27
+ static func asked(args: Dictionary, key: String) -> Array:
28
+ var given: Variant = args.get(key)
29
+ return given if given is Array else []
30
+
31
+
32
+ ## Puts [param node] in each group of [param adding] and takes it out of each of [param removing],
33
+ ## or answers why not with nothing changed.
34
+ static func change(scene: GdhSceneFile, node: Node, adding: Array, removing: Array) -> String:
35
+ for group: Variant in adding + removing:
36
+ if str(group).strip_edges().is_empty():
37
+ return (
38
+ "%s names no group: a group's name has something in it besides spaces."
39
+ % JSON.stringify(group)
40
+ )
41
+ for group: Variant in adding:
42
+ if removing.has(group):
43
+ return "%s is in both addGroups and removeGroups." % str(group)
44
+ var at: String = scene.path_of(node)
45
+ var here: PackedStringArray = scene.groups_at(at)
46
+ for group: Variant in removing:
47
+ var name: String = str(group)
48
+ if not node.is_in_group(name):
49
+ var held: Array = _sorted(node.get_groups())
50
+ return (
51
+ "%s is in no group %s; %s."
52
+ % [node.name, name, "it is in " + ", ".join(held) if not held.is_empty() else "it is in none"]
53
+ )
54
+ if not here.has(name):
55
+ var from: String = scene.values_from(node, at)
56
+ if from.is_empty():
57
+ return (
58
+ "%s is in %s, which no scene file puts it in: something puts it there as it is made, such as its script."
59
+ % [node.name, name]
60
+ )
61
+ return (
62
+ (
63
+ "%s is in %s because %s puts it there, and no scene using that one can take it out: "
64
+ + "remove the group in %s, which takes it out in every scene using that one."
65
+ )
66
+ % [node.name, name, from, from]
67
+ )
68
+ for group: Variant in adding:
69
+ node.add_to_group(str(group), true)
70
+ for group: Variant in removing:
71
+ node.remove_from_group(str(group))
72
+ return ""
73
+
74
+
75
+ ## Why [param saved], the node as the written file loads it, is not in the groups asked, or "".
76
+ static func unkept(saved: Node, adding: Array, removing: Array) -> String:
77
+ for group: Variant in adding:
78
+ if not saved.is_in_group(str(group)):
79
+ return "%s would load in no group %s." % [saved.name, str(group)]
80
+ for group: Variant in removing:
81
+ if saved.is_in_group(str(group)):
82
+ return "%s would still load in the group %s." % [saved.name, str(group)]
83
+ return ""
84
+
85
+
86
+ static func _sorted(groups: Variant) -> Array:
87
+ var names: Array = []
88
+ for group: Variant in groups:
89
+ names.append(str(group))
90
+ names.sort()
91
+ return names
@@ -0,0 +1 @@
1
+ uid://76w2hadtl7nab
@@ -6,7 +6,7 @@ extends RefCounted
6
6
 
7
7
  const GdhFileWalk = preload("../file_walk.gd")
8
8
  const GdhRead = preload("../reading.gd")
9
- const GdhPropertyValues = preload("../property_values.gd")
9
+ const GdhSame = preload("../same_values.gd")
10
10
 
11
11
 
12
12
  ## The scene to instance for an add, or null for a node of a class, beside why neither can be made
@@ -85,7 +85,7 @@ static func copied(source: Node, root: Node, copies: Dictionary) -> Node:
85
85
  continue
86
86
  var property: String = str(declared.get("name", ""))
87
87
  var value: Variant = source.get(property)
88
- if not GdhPropertyValues.same(copy.get(property), value):
88
+ if not GdhSame.same(copy.get(property), value):
89
89
  copy.set(property, value)
90
90
  for group: StringName in source.get_groups():
91
91
  if not copy.is_in_group(group):
@@ -171,6 +171,72 @@ static func place(node: Node, asked: Variant) -> void:
171
171
  node.get_parent().move_child(node, GdhRead.as_int(asked))
172
172
 
173
173
 
174
+ ## The properties that hold where [param node] is drawn, which a reparent keeping it in place
175
+ ## changes: a Control keeps only its position, as Node.reparent keeps it.
176
+ static func placing(node: Node) -> Array[String]:
177
+ if node is Node3D:
178
+ return ["position", "rotation", "scale"]
179
+ if node is Control:
180
+ return ["position"]
181
+ if node is Node2D:
182
+ return ["position", "rotation", "scale", "skew"]
183
+ return []
184
+
185
+
186
+ ## Gives [param node], just moved from under [param was], the transform that keeps it where it was
187
+ ## drawn, as the editor's reparent and Node.reparent do. Worked out from the local transforms
188
+ ## rather than read off the global ones, which the engine answers only for a node in the tree, and
189
+ ## the scene a tool edits is loaded from its file and never is. Nothing changes for a top-level
190
+ ## node, or where the old and new parents place their children alike.
191
+ static func keep_in_place(node: Node, was: Node) -> void:
192
+ if node is Node3D:
193
+ var spatial: Node3D = node
194
+ var before: Transform3D = _spatial_space(was)
195
+ var after: Transform3D = _spatial_space(node.get_parent())
196
+ if not spatial.top_level and not before.is_equal_approx(after):
197
+ spatial.transform = after.affine_inverse() * before * spatial.transform
198
+ elif node is CanvasItem:
199
+ var drawn: CanvasItem = node
200
+ var before_2d: Transform2D = _canvas_space(was)
201
+ var after_2d: Transform2D = _canvas_space(node.get_parent())
202
+ if drawn.top_level or before_2d.is_equal_approx(after_2d):
203
+ return
204
+ var held: Transform2D = after_2d.affine_inverse() * before_2d * drawn.get_transform()
205
+ if node is Control:
206
+ var control: Control = node
207
+ control.position = held.origin
208
+ else:
209
+ var flat: Node2D = node
210
+ flat.transform = held
211
+
212
+
213
+ ## Where the children of [param parent] are drawn in 3D: the transforms of it and the Node3Ds
214
+ ## above it, up to the first that is top-level or whose parent is no Node3D.
215
+ static func _spatial_space(parent: Node) -> Transform3D:
216
+ var space: Transform3D = Transform3D.IDENTITY
217
+ var at: Node = parent
218
+ while at is Node3D:
219
+ var spatial: Node3D = at
220
+ space = spatial.transform * space
221
+ if spatial.top_level:
222
+ break
223
+ at = at.get_parent()
224
+ return space
225
+
226
+
227
+ ## Where the children of [param parent] are drawn in 2D, as [method _spatial_space] works it out in 3D.
228
+ static func _canvas_space(parent: Node) -> Transform2D:
229
+ var space: Transform2D = Transform2D.IDENTITY
230
+ var at: Node = parent
231
+ while at is CanvasItem:
232
+ var drawn: CanvasItem = at
233
+ space = drawn.get_transform() * space
234
+ if drawn.top_level:
235
+ break
236
+ at = at.get_parent()
237
+ return space
238
+
239
+
174
240
  ## Why [param instanced] cannot be instanced into [param scene_path], or "" when it can: a scene
175
241
  ## inside itself, directly or through the scenes it instances, which Godot's own dialog refuses and
176
242
  ## which never finishes loading.