gdharness 1.3.9 → 1.3.10

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 (73) hide show
  1. package/build/cli.js +4563 -3195
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +102 -18
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
  7. package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
  8. package/build/godot/addons/gdharness_editor/reading.gd +51 -1
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +77 -140
  10. package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
  11. package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +11 -22
  13. package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
  14. package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tool_executor.gd +28 -5
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +39 -20
  17. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -362
  18. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  21. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  23. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +147 -492
  24. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  25. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -3
  26. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  27. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +79 -5
  28. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  29. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +183 -37
  30. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  31. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  32. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  33. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  34. package/build/godot/addons/gdharness_runtime/reading.gd +51 -1
  35. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  36. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +185 -30
  37. package/build/godot/addons/gdharness_runtime/runtime_input.gd +86 -79
  38. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  39. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  40. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +309 -22
  41. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +163 -20
  42. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  43. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +5 -2
  44. package/build/godot/addons/gdharness_runtime/runtime_values.gd +146 -5
  45. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  46. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  47. package/build/godot/addons/gdharness_runtime/serialisation.gd +11 -22
  48. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  49. package/build/godot/operations/animation_reads.gd +115 -4
  50. package/build/godot/operations/audio_buses.gd +93 -7
  51. package/build/godot/operations/classdb_queries.gd +119 -7
  52. package/build/godot/operations/declared_values.gd +272 -0
  53. package/build/godot/operations/dependencies.gd +154 -41
  54. package/build/godot/operations/editor_tools.gd +22 -14
  55. package/build/godot/operations/export_presets.gd +602 -4
  56. package/build/godot/operations/file_reads.gd +102 -18
  57. package/build/godot/operations/gdscript_authoring.gd +11 -1
  58. package/build/godot/operations/godot_operations.gd +16 -2
  59. package/build/godot/operations/import_pipeline.gd +20 -221
  60. package/build/godot/operations/input_actions.gd +108 -47
  61. package/build/godot/operations/project_config.gd +83 -69
  62. package/build/godot/operations/project_diagnostics.gd +4 -0
  63. package/build/godot/operations/project_validation.gd +266 -0
  64. package/build/godot/operations/property_paths.gd +89 -16
  65. package/build/godot/operations/property_values.gd +180 -121
  66. package/build/godot/operations/reading.gd +51 -1
  67. package/build/godot/operations/scene_connections.gd +222 -0
  68. package/build/godot/operations/serialisation.gd +11 -22
  69. package/build/godot/operations/setting_reads.gd +121 -0
  70. package/build/godot/operations/value_shapes.gd +128 -1
  71. package/build/godot/tests/listed.gd +20 -13
  72. package/build/keeper.js +1371 -706
  73. package/package.json +1 -1
@@ -22,6 +22,22 @@ static func read(scene: GdhSceneFile, node: Node) -> Dictionary:
22
22
  return answer
23
23
 
24
24
 
25
+ ## The groups of [param node], just added to this scene and put in [param adding] there, in the shape
26
+ ## [method read] answers: this scene's file does not hold the node until it is saved.
27
+ static func read_added(node: Node, adding: Array) -> Dictionary:
28
+ var all: Array = _sorted(node.get_groups())
29
+ if all.is_empty():
30
+ return {}
31
+ var answer: Dictionary = {"groups": all}
32
+ var here: Array = []
33
+ for group: String in _sorted(adding):
34
+ if not here.has(group):
35
+ here.append(group)
36
+ if here.size() != all.size():
37
+ answer["groupsHere"] = here
38
+ return answer
39
+
40
+
25
41
  ## The group names under [param key] of [param args], which the server has checked are strings, or
26
42
  ## none when not given.
27
43
  static func asked(args: Dictionary, key: String) -> Array:
@@ -7,6 +7,7 @@ extends RefCounted
7
7
  const GdhFileReads = preload("../file_reads.gd")
8
8
  const GdhPaths = preload("scene_paths.gd")
9
9
  const GdhSceneFile = preload("../scene_file.gd")
10
+ const GdhSceneUids = preload("../scene_uids.gd")
10
11
 
11
12
 
12
13
  ## Why a create at [param scene_path] is refused when a file is there already, which saving over
@@ -87,14 +88,14 @@ static func inherit(scene_path: String, base_path: String, root_name: String, sc
87
88
  "",
88
89
  (
89
90
  '[ext_resource type="PackedScene"%s path="%s" id="1_base"]'
90
- % [GdhSceneFile.reference_uid(base_path), base_path]
91
+ % [GdhSceneUids.reference_uid(base_path), base_path]
91
92
  ),
92
93
  ]
93
94
  if script != null:
94
95
  var _resourced: bool = lines.append(
95
96
  (
96
97
  '[ext_resource type="Script"%s path="%s" id="2_script"]'
97
- % [GdhSceneFile.reference_uid(script.resource_path), script.resource_path]
98
+ % [GdhSceneUids.reference_uid(script.resource_path), script.resource_path]
98
99
  )
99
100
  )
100
101
  lines.append_array(["", '[node name="%s" instance=ExtResource("1_base")]' % name])
@@ -179,7 +180,7 @@ static func follow(changed: String, old_path: String, new_path: String, carried:
179
180
  var moves: Dictionary = {}
180
181
  if _inherits(scene_path, changed, texts):
181
182
  moves[old_path] = new_path
182
- var read: Dictionary = GdhFileReads.scene_tree(scene_path, -1, false)
183
+ var read: Dictionary = GdhFileReads.scene_tree(scene_path, -1, false, false)
183
184
  if not read.get("ok", false):
184
185
  not_followed.append({"scenePath": scene_path, "reason": str(read.get("error", ""))})
185
186
  continue
@@ -14,6 +14,7 @@ enum { UNCHANGED, REWRITTEN, DELETED }
14
14
  const GdhHeadlessCheck = preload("../headless_check.gd")
15
15
  const GdhRead = preload("../reading.gd")
16
16
  const GdhSceneFile = preload("../scene_file.gd")
17
+ const GdhSceneUids = preload("../scene_uids.gd")
17
18
  ## PROPERTY_HINT_NO_NODEPATH, which marks a property the editor's rename skips. It is not bound for
18
19
  ## scripts, and follows PROPERTY_HINT_ONESHOT in the engine's list.
19
20
  const HINT_NO_NODEPATH: int = PROPERTY_HINT_ONESHOT + 1
@@ -127,7 +128,7 @@ func put_back(scene_path: String, why: String = "") -> void:
127
128
  func answer() -> Dictionary:
128
129
  for file: String in _homes:
129
130
  var resource: Resource = _homes[file]
130
- var saving: Error = GdhSceneFile.save_keeping_uid(resource, file)
131
+ var saving: Error = GdhSceneUids.save_keeping_uid(resource, file)
131
132
  if saving == OK:
132
133
  if Engine.is_editor_hint():
133
134
  EditorInterface.get_resource_filesystem().update_file(file)
@@ -3,17 +3,20 @@ extends Node
3
3
 
4
4
  ## Scenes and their nodes, edited in the open editor and read back from what it holds.
5
5
 
6
+ const GdhDeclared = preload("../declared_values.gd")
6
7
  const GdhFileReads = preload("../file_reads.gd")
7
8
  const GdhRead = preload("../reading.gd")
8
9
  const GdhGroups = preload("scene_groups.gd")
10
+ const GdhHeadlessCheck = preload("../headless_check.gd")
9
11
  const GdhKin = preload("scene_kin.gd")
12
+ const GdhNavigationBake = preload("navigation_bake.gd")
10
13
  const GdhPaths = preload("scene_paths.gd")
11
14
  const GdhPlacing = preload("scene_placing.gd")
12
15
  const GdhPropertyValues = preload("../property_values.gd")
13
- const GdhResourceTools = preload("resource_tools.gd")
14
16
  const GdhSame = preload("../same_values.gd")
15
17
  const GdhSceneFile = preload("../scene_file.gd")
16
18
  const GdhSerialisation = preload("../serialisation.gd")
19
+ const GdhTilesetTools = preload("tileset_tools.gd")
17
20
 
18
21
  var _editor_plugin: EditorPlugin = null
19
22
  var _values: GdhSerialisation = GdhSerialisation.new()
@@ -216,10 +219,11 @@ func list_scene_nodes(args: Dictionary) -> Dictionary:
216
219
  var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
217
220
  var depth: int = GdhRead.as_int(args.get("depth", -1), -1)
218
221
  var include_properties: bool = GdhRead.as_bool(args.get("includeProperties", false))
222
+ var include_defaults: bool = GdhRead.as_bool(args.get("includeDefaults", false))
219
223
 
220
224
  # Read as a headless engine reads it with no editor connected, so the answer does not depend on
221
225
  # whether one is.
222
- var read: Dictionary = GdhFileReads.scene_tree(scene_path, depth, include_properties)
226
+ var read: Dictionary = GdhFileReads.scene_tree(scene_path, depth, include_properties, include_defaults)
223
227
  if read.get("ok", false):
224
228
  read["unsavedInEditor"] = GdhSceneFile.unsaved(scene_path)
225
229
  return read
@@ -287,6 +291,9 @@ func add_node(args: Dictionary) -> Dictionary:
287
291
  return scene.refuse("Nothing was written: " + unsettled)
288
292
  scene.asked(new_node, properties.keys())
289
293
  var unwritten: String = _properties.write_all(new_node, properties)
294
+ scene.asked(new_node, _properties.moved_by_engine.keys())
295
+ if not unwritten.is_empty():
296
+ unwritten = GdhHeadlessCheck.setter_ran(new_node, _properties.unheld, unwritten)
290
297
  if unwritten.is_empty():
291
298
  unwritten = GdhGroups.change(scene, new_node, adding, [])
292
299
  if not unwritten.is_empty():
@@ -308,7 +315,7 @@ func add_node(args: Dictionary) -> Dictionary:
308
315
  answer["properties"] = held
309
316
  _properties.note_write(answer, new_node)
310
317
  if not adding.is_empty():
311
- answer.merge(GdhGroups.read(scene, new_node))
318
+ answer.merge(GdhGroups.read_added(new_node, adding))
312
319
  var written: Dictionary = scene.write(
313
320
  func(saved: Node) -> String:
314
321
  var unkept: String = _kept(
@@ -619,8 +626,13 @@ func set_node_properties(args: Dictionary) -> Dictionary:
619
626
  var old_path: String = scene.path_of(node)
620
627
  scene.asked(node, properties.keys())
621
628
  var refused_property: String = _properties.write_all(node, properties)
629
+ scene.asked(node, _properties.moved_by_engine.keys())
622
630
  if not refused_property.is_empty():
623
- return scene.refuse(GdhSceneFile.missing_on(node, refused_property))
631
+ return scene.refuse(
632
+ GdhHeadlessCheck.setter_ran(
633
+ node, _properties.unheld, GdhSceneFile.missing_on(node, refused_property)
634
+ )
635
+ )
624
636
  var ungrouped: String = GdhGroups.change(scene, node, adding, removing)
625
637
  if not ungrouped.is_empty():
626
638
  return scene.refuse(ungrouped)
@@ -726,11 +738,55 @@ func get_node_properties(args: Dictionary) -> Dictionary:
726
738
  # names this scene's file sets are always answered.
727
739
  var set_here: Dictionary = scene.set_here(node)
728
740
  var props: Dictionary = _properties_of(node, include_defaults, set_here)
729
- var cells: Dictionary = GdhResourceTools.cells_of(node)
741
+ var cells: Dictionary = GdhTilesetTools.cells_of(node)
730
742
  var at: String = scene.path_of(node)
731
743
  var here: Array = set_here.keys()
732
744
  here.sort()
733
745
  var answer: Dictionary = {"ok": true, "nodePath": at, "properties": props, "setHere": here}
746
+ # A Node3D stores only its transform, a matrix, while a set takes the position, rotation and
747
+ # scale the inspector shows, so a node turned 45 degrees down read back as nine numbers. A
748
+ # sheared one has no such three, and is left to its transform.
749
+ var placed: Node3D = node as Node3D
750
+ var rebuilt: Transform3D = Transform3D()
751
+ if placed != null:
752
+ var turned: Basis = Basis.from_euler(placed.rotation, placed.rotation_order)
753
+ rebuilt = Transform3D(turned * Basis.from_scale(placed.scale), placed.position)
754
+ if placed != null and props.has("transform") and rebuilt.is_equal_approx(placed.transform):
755
+ # Rounded, since a node never scaled came back with a scale of 0.99999994 out of the matrix;
756
+ # the transform beside it is exact.
757
+ var step: Vector3 = Vector3.ONE * 0.00001
758
+ answer["placement"] = {
759
+ "position": _readable(node, placed.position.snapped(step)),
760
+ "rotation_degrees": _readable(node, placed.rotation_degrees.snapped(step)),
761
+ "scale": _readable(node, placed.scale.snapped(step)),
762
+ }
763
+ # A player's getter answers Master for a bus the layout does not have, while the file keeps the
764
+ # name, so a player left on a renamed or removed bus read as one put on Master.
765
+ var bus: String = str(set_here.get("bus", ""))
766
+ var plays: bool = node is AudioStreamPlayer or node is AudioStreamPlayer2D or node is AudioStreamPlayer3D
767
+ if plays and not bus.is_empty() and AudioServer.get_bus_index(bus) == -1:
768
+ props["bus"] = bus
769
+ var buses: PackedStringArray = []
770
+ for index: int in AudioServer.bus_count:
771
+ var _added: bool = buses.append(AudioServer.get_bus_name(index))
772
+ answer["busNote"] = (
773
+ (
774
+ "%s names the bus %s, which the project's bus layout does not have, so it plays on %s; "
775
+ + "the buses are %s."
776
+ )
777
+ % [str(node.name) if at == "." else at, bus, AudioServer.get_bus_name(0), ", ".join(buses)]
778
+ )
779
+ # A mask reads as a number, and which layers it holds, under the names the project gives them,
780
+ # is what a caller reads it for.
781
+ var layers: Dictionary = {}
782
+ for declared: Dictionary in node.get_property_list():
783
+ var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
784
+ var named: String = str(declared.get("name", ""))
785
+ var mask: Variant = node.get(named) if props.has(named) else null
786
+ if hint in GdhDeclared.LAYER_NAMES and mask is int:
787
+ layers[named] = GdhDeclared.layers_in(hint, GdhRead.as_int(mask))
788
+ if not layers.is_empty():
789
+ answer["layers"] = layers
734
790
  answer.merge(GdhSceneFile.class_of(node))
735
791
  answer.merge(GdhGroups.read(scene, node))
736
792
  var from: String = scene.values_from(node, at)
@@ -753,6 +809,24 @@ func get_node_properties(args: Dictionary) -> Dictionary:
753
809
  return answer
754
810
 
755
811
 
812
+ func bake_navigation(args: Dictionary) -> Dictionary:
813
+ var project_path: String = str(args.get("projectPath", ""))
814
+ var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
815
+ var node_path: String = str(args.get("nodePath", "."))
816
+ var opened: Array = _open(scene_path, true)
817
+ var scene: GdhSceneFile = opened[0]
818
+ var refused: Dictionary = opened[1]
819
+ if not refused.is_empty():
820
+ return refused
821
+ var node: Node = GdhSceneFile.node_at(scene.root, node_path)
822
+ if not node:
823
+ return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
824
+ var foreign: String = scene.foreign(node)
825
+ if not foreign.is_empty():
826
+ return scene.refuse(foreign)
827
+ return GdhNavigationBake.bake(scene, node)
828
+
829
+
756
830
  func save_scene(args: Dictionary) -> Dictionary:
757
831
  var project_path: String = str(args.get("projectPath", ""))
758
832
  var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
@@ -10,12 +10,12 @@ extends Node
10
10
  ## of the two a caller wants is theirs to say, so the server only comes here when asked, and the
11
11
  ## difference is the reason the argument exists rather than an inconvenience to paper over.
12
12
  ##
13
- ## Every value goes through the same serialiser the headless answer uses, because the two are one
13
+ ## Every answer is built by the same reads the headless answer uses, because the two are one
14
14
  ## question asked of two places: a value spelled differently here would read as the editor and the
15
15
  ## file disagreeing about something they agree about.
16
16
 
17
- const GdhRead = preload("../reading.gd")
18
17
  const GdhSerialisation = preload("../serialisation.gd")
18
+ const GdhSettingReads = preload("../setting_reads.gd")
19
19
 
20
20
  var _values: GdhSerialisation = GdhSerialisation.new()
21
21
 
@@ -23,42 +23,19 @@ var _values: GdhSerialisation = GdhSerialisation.new()
23
23
  func get_project_setting(args: Dictionary) -> Dictionary:
24
24
  var prefix: String = str(args.get("prefix", ""))
25
25
  if not prefix.is_empty():
26
- return _settings_under(prefix)
26
+ var family: Dictionary = GdhSettingReads.under(prefix, _values)
27
+ family["ok"] = true
28
+ return family
27
29
 
28
30
  var setting: String = str(args.get("setting", ""))
29
31
  if setting.is_empty():
30
32
  return {"ok": false, "error": "setting is required, or prefix to read every setting under one"}
31
33
 
32
- var exists: bool = ProjectSettings.has_setting(setting)
33
- var result: Dictionary = {"ok": true, "setting_path": setting, "exists": exists, "value": null}
34
- if exists:
35
- result["value"] = _values.serialize_value(ProjectSettings.get_setting(setting))
36
- else:
37
- result["message"] = "Setting does not exist"
34
+ var result: Dictionary = GdhSettingReads.one(setting, _values)
35
+ result["ok"] = true
38
36
  return result
39
37
 
40
38
 
41
- ## Every setting under [prefix] with the type the engine registers for each, which is the half a
42
- ## name hides: a family of levels can hold a setting that is a bool, and writing a level over it
43
- ## looks like it worked.
44
- func _settings_under(prefix: String) -> Dictionary:
45
- var found: Array[Dictionary] = []
46
- for property: Dictionary in ProjectSettings.get_property_list():
47
- var setting: String = str(property.get("name", ""))
48
- if not setting.begins_with(prefix):
49
- continue
50
- var kind: int = GdhRead.as_int(property.get("type", TYPE_NIL), TYPE_NIL)
51
- var described: Dictionary = {
52
- "setting": setting,
53
- "type": type_string(kind),
54
- "value": _values.serialize_value(ProjectSettings.get_setting(setting)),
55
- }
56
- found.append(described)
57
-
58
- found.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["setting"] < b["setting"])
59
- return {"ok": true, "prefix": prefix, "count": found.size(), "settings": found}
60
-
61
-
62
39
  ## Takes the named settings from project.godot as it is on disk, after something else wrote it.
63
40
  ##
64
41
  ## The editor loads the file once, at startup. Written from outside, it goes on holding the old
@@ -6,9 +6,33 @@ extends Node
6
6
 
7
7
  const GdhRead = preload("../reading.gd")
8
8
  const GdhSceneFile = preload("../scene_file.gd")
9
+ const GdhSerialisation = preload("../serialisation.gd")
9
10
 
10
11
  ## What every emission meets when a connection cannot call its method.
11
12
  const FAILS: String = ", so every emission fails to call it."
13
+ ## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
14
+ ## the persistence every connection in a saved scene carries.
15
+ const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
16
+ Object.CONNECT_DEFERRED: "deferred",
17
+ Object.CONNECT_PERSIST: "persist",
18
+ Object.CONNECT_ONE_SHOT: "one_shot",
19
+ Object.CONNECT_REFERENCE_COUNTED: "reference_counted",
20
+ Object.CONNECT_APPEND_SOURCE_OBJECT: "append_source_object",
21
+ # The engine's CONNECT_INHERITED, which GDScript is not given: on a connection an instanced scene
22
+ # makes inside itself, as a scene holding one loads it.
23
+ 32: "inherited",
24
+ }
25
+ ## The flags a caller may give: the engine's own CONNECT_INHERITED marks a connection an instanced
26
+ ## scene makes, and is never one a scene makes for itself.
27
+ const GIVEN_FLAGS: int = (
28
+ Object.CONNECT_DEFERRED
29
+ | Object.CONNECT_PERSIST
30
+ | Object.CONNECT_ONE_SHOT
31
+ | Object.CONNECT_REFERENCE_COUNTED
32
+ | Object.CONNECT_APPEND_SOURCE_OBJECT
33
+ )
34
+
35
+ var _values: GdhSerialisation = GdhSerialisation.new()
12
36
 
13
37
 
14
38
  func connect_signal(args: Dictionary) -> Dictionary:
@@ -20,7 +44,8 @@ func connect_signal(args: Dictionary) -> Dictionary:
20
44
  var method_name: String = str(args.get("methodName", ""))
21
45
  # A connection without CONNECT_PERSIST is a runtime one, and PackedScene.pack drops those on
22
46
  # the way out: without this the scene saves unchanged and this answers success over nothing.
23
- var flags: int = GdhRead.as_int(args.get("flags", 0)) | Object.CONNECT_PERSIST
47
+ var given_flags: int = GdhRead.as_int(args.get("flags", 0))
48
+ var flags: int = given_flags | Object.CONNECT_PERSIST
24
49
 
25
50
  if (
26
51
  source_node_path.is_empty()
@@ -29,6 +54,9 @@ func connect_signal(args: Dictionary) -> Dictionary:
29
54
  or method_name.is_empty()
30
55
  ):
31
56
  return {"ok": false, "error": "Missing required signal connection arguments"}
57
+ var unasked: String = _unasked(given_flags)
58
+ if not unasked.is_empty():
59
+ return {"ok": false, "error": unasked}
32
60
 
33
61
  var scene: GdhSceneFile = GdhSceneFile.new()
34
62
  var refused: Dictionary = scene.open(scene_path, true)
@@ -43,40 +71,57 @@ func connect_signal(args: Dictionary) -> Dictionary:
43
71
 
44
72
  # Reconnected rather than left alone when the flags differ, so the connection saved is the one
45
73
  # asked for. Left alone, the answer gave the flags asked for and the file kept the old ones.
74
+ # Found by its object and method, as the engine finds it: a connection carrying binds is another
75
+ # callable, so comparing callables missed it, and the engine's is_connected then found it, took it
76
+ # out and put a bare one in its place, losing the binds without a word.
77
+ var before: Dictionary = _held(source, signal_name, target, method_name)
78
+ var binds: Array = []
79
+ var unbinds: int = 0
80
+ if not before.is_empty():
81
+ binds = before["binds"]
82
+ unbinds = before["unbinds"]
83
+ if args.has("unbinds"):
84
+ unbinds = GdhRead.as_int(args["unbinds"])
85
+ if args.has("binds"):
86
+ var given: Array = args["binds"]
87
+ binds = _bound_values(given, source, signal_name, Callable(target, method_name), unbinds, flags)
46
88
  var callable: Callable = Callable(target, method_name)
89
+ if not binds.is_empty():
90
+ callable = callable.bindv(binds)
91
+ if unbinds > 0:
92
+ callable = callable.unbind(unbinds)
47
93
  var method_found: bool = target.has_method(method_name)
48
94
  var misfit: String = _misfit(source, signal_name, callable, flags)
49
- var flags_before: int = -1
50
- for connection: Dictionary in source.get_signal_connection_list(signal_name):
51
- if connection.get("callable") == callable:
52
- flags_before = GdhRead.as_int(connection.get("flags", 0))
53
- if source.is_connected(signal_name, callable):
54
- source.disconnect(signal_name, callable)
95
+ if not before.is_empty():
96
+ var held: Callable = before["callable"]
97
+ source.disconnect(signal_name, held)
55
98
  var connect_result: Error = source.connect(signal_name, callable, flags)
56
99
  if connect_result != OK:
57
100
  return scene.refuse("Failed to connect signal: " + error_string(connect_result))
58
101
 
59
102
  var from: String = scene.path_of(source)
60
103
  var to: String = scene.path_of(target)
61
- var saved_flags: Array = [-1]
104
+ var saved: Dictionary = {}
62
105
  var written: Dictionary = scene.write(
63
- func(saved: Node) -> String:
64
- saved_flags[0] = _connection_flags(saved, from, signal_name, to, method_name)
65
- if saved_flags[0] < 0:
106
+ func(loaded: Node) -> String:
107
+ var found: Dictionary = _saved(loaded, from, signal_name, to, method_name)
108
+ saved.merge(found)
109
+ if found.is_empty():
66
110
  return "the connection would not be in the scene when it loads."
67
111
  return ""
68
112
  )
69
113
  if not written.is_empty():
70
114
  return written
71
115
 
116
+ var saved_flags: int = saved["flags"]
72
117
  var answer: Dictionary = {
73
118
  "ok": true,
74
119
  "sourceNodePath": from,
75
120
  "signalName": signal_name,
76
121
  "targetNodePath": to,
77
122
  "methodName": method_name,
78
- "flags": saved_flags[0],
79
- "flagNames": GdhSceneFile.flag_names(GdhRead.as_int(saved_flags[0])),
123
+ "flags": saved_flags,
124
+ "flagNames": flag_names(saved_flags),
80
125
  # Connected either way, as the editor's own dialog allows, since the method may be written
81
126
  # next; a caller told nothing learnt of a misspelt one only when the signal fired.
82
127
  "methodFound": method_found
@@ -89,13 +134,108 @@ func connect_signal(args: Dictionary) -> Dictionary:
89
134
  # A second connect of the same pair read exactly like the first, so a caller could not tell a
90
135
  # connection made from one that was already there, and a list showing one where two were made
91
136
  # looked like a lost write.
92
- if flags_before >= 0:
137
+ _bound_into(answer, saved)
138
+ if not before.is_empty():
93
139
  answer["alreadyConnected"] = true
94
- if flags_before != GdhRead.as_int(saved_flags[0]):
95
- answer["flagsBefore"] = GdhSceneFile.flag_names(flags_before)
140
+ var flags_before: int = before["flags"]
141
+ if flags_before != saved_flags:
142
+ answer["flagsBefore"] = flag_names(flags_before)
143
+ var binds_before: Variant = _values.serialize_value(before["binds"])
144
+ if JSON.stringify(binds_before) != JSON.stringify(answer.get("binds", [])):
145
+ answer["bindsBefore"] = binds_before
146
+ if before["unbinds"] != saved["unbinds"]:
147
+ answer["unbindsBefore"] = before["unbinds"]
96
148
  return answer
97
149
 
98
150
 
151
+ ## Why [param given_flags] cannot be connected with, or "" when it can: a bit no flag a scene
152
+ ## connects with has. The server checks the type of every argument, and nothing knows the bits.
153
+ static func _unasked(given_flags: int) -> String:
154
+ if given_flags >= 0 and not given_flags & ~GIVEN_FLAGS:
155
+ return ""
156
+ var named: Array[String] = []
157
+ for bit: int in CONNECT_FLAG_NAMES:
158
+ if bit & GIVEN_FLAGS:
159
+ named.append("%s %d" % [CONNECT_FLAG_NAMES[bit], bit])
160
+ return (
161
+ "flags %d holds bits no flag a scene connects with has: they are %s, added together."
162
+ % [given_flags, ", ".join(named)]
163
+ )
164
+
165
+
166
+ ## [param given] as the values [param called] is bound with, each number fitted to the parameter it
167
+ ## lands on. JSON has one kind of number, so 3 arrived as 3.0, the file said [3.0], and a whole
168
+ ## number for an untyped parameter is an int, as GDScript reads the literal 3.
169
+ func _bound_values(
170
+ given: Array, source: Node, signal_name: String, called: Callable, unbinds: int, flags: int
171
+ ) -> Array:
172
+ var passed: int = 0
173
+ for info: Dictionary in source.get_signal_list():
174
+ if str(info.get("name", "")) == signal_name:
175
+ var arguments: Array = info.get("args", [])
176
+ passed = arguments.size()
177
+ if flags & Object.CONNECT_APPEND_SOURCE_OBJECT:
178
+ passed += 1
179
+ passed = maxi(passed - unbinds, 0)
180
+ var parameters: Array = []
181
+ for info: Dictionary in called.get_object().get_method_list():
182
+ if str(info.get("name", "")) == str(called.get_method()):
183
+ parameters = info.get("args", [])
184
+ break
185
+ var values: Array = []
186
+ for at: int in given.size():
187
+ var value: Variant = _values.deserialize_value(given[at])
188
+ var declared: int = TYPE_NIL
189
+ if passed + at < parameters.size():
190
+ var parameter: Dictionary = parameters[passed + at]
191
+ declared = GdhRead.as_int(parameter.get("type", TYPE_NIL))
192
+ if value is float and declared != TYPE_FLOAT:
193
+ var number: float = value
194
+ if is_finite(number) and number == floorf(number) and absf(number) < 9007199254740992.0:
195
+ value = int(number)
196
+ values.append(value)
197
+ return values
198
+
199
+
200
+ ## The connection from [param signal_name] of [param source] to [param method] on [param target],
201
+ ## as {callable, flags, binds, unbinds}, or {} when there is none.
202
+ static func _held(source: Node, signal_name: String, target: Object, method: String) -> Dictionary:
203
+ for conn: Dictionary in source.get_signal_connection_list(signal_name):
204
+ var callable: Callable = conn.get("callable", Callable())
205
+ if callable.get_object() == target and str(callable.get_method()) == method:
206
+ return {
207
+ "callable": callable,
208
+ "flags": GdhRead.as_int(conn.get("flags", 0)),
209
+ "binds": callable.get_bound_arguments(),
210
+ "unbinds": callable.get_unbound_arguments_count(),
211
+ }
212
+ return {}
213
+
214
+
215
+ ## [param held]'s binds and unbinds into [param answer], each only when it has any, as the scene
216
+ ## file writes them.
217
+ func _bound_into(answer: Dictionary, held: Dictionary) -> void:
218
+ var binds: Array = held["binds"]
219
+ if not binds.is_empty():
220
+ answer["binds"] = _values.serialize_value(binds)
221
+ var unbinds: int = held["unbinds"]
222
+ if unbinds > 0:
223
+ answer["unbinds"] = unbinds
224
+
225
+
226
+ ## Each bit set in [param flags], a connection's, by name, and any bits left unnamed as one number.
227
+ static func flag_names(flags: int) -> Array[String]:
228
+ var names: Array[String] = []
229
+ var rest: int = flags
230
+ for bit: int in CONNECT_FLAG_NAMES:
231
+ if flags & bit:
232
+ names.append(CONNECT_FLAG_NAMES[bit])
233
+ rest &= ~bit
234
+ if rest != 0:
235
+ names.append(str(rest))
236
+ return names
237
+
238
+
99
239
  ## The two ends of a connection in [param scene], as [source, target], or [null, why not].
100
240
  func _connection_ends(
101
241
  scene: GdhSceneFile, source_node_path: String, signal_name: String, target_node_path: String
@@ -119,23 +259,17 @@ func _connection_ends(
119
259
  return [source, target]
120
260
 
121
261
 
122
- ## The flags of the persistent connection from [param from]'s [param signal_name] to
123
- ## [param method] on [param to] in the tree under [param root], or -1 when there is none.
124
- func _connection_flags(root: Node, from: String, signal_name: String, to: String, method: String) -> int:
262
+ ## The persistent connection from [param from]'s [param signal_name] to [param method] on [param to]
263
+ ## in the tree under [param root], as [method _held] answers it, or {} when there is none.
264
+ static func _saved(root: Node, from: String, signal_name: String, to: String, method: String) -> Dictionary:
125
265
  var source: Node = root.get_node_or_null(from)
126
266
  var target: Node = root.get_node_or_null(to)
127
267
  if source == null or target == null or not source.has_signal(signal_name):
128
- return -1
129
- for conn: Dictionary in source.get_signal_connection_list(signal_name):
130
- var flags: int = GdhRead.as_int(conn.get("flags", 0))
131
- var callable: Callable = conn.get("callable", Callable())
132
- if (
133
- flags & Object.CONNECT_PERSIST
134
- and callable.get_object() == target
135
- and str(callable.get_method()) == method
136
- ):
137
- return flags
138
- return -1
268
+ return {}
269
+ var held: Dictionary = _held(source, signal_name, target, method)
270
+ if held.is_empty() or not GdhRead.as_int(held["flags"]) & Object.CONNECT_PERSIST:
271
+ return {}
272
+ return held
139
273
 
140
274
 
141
275
  ## Why what [param signal_name] of [param source] passes cannot reach [param callable], or "" when it
@@ -264,7 +398,8 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
264
398
  var to: String = scene.path_of(target)
265
399
  # A connection that was never there saved the scene unchanged and answered as removed, so a
266
400
  # typo in either name read as success.
267
- if _connection_flags(scene.root, from, signal_name, to, method_name) < 0:
401
+ var held: Dictionary = _saved(scene.root, from, signal_name, to, method_name)
402
+ if held.is_empty():
268
403
  # The root's path is ".", which before ".method" read as "..method".
269
404
  var said_from: String = str(source.name) if source == scene.root else from
270
405
  var said_to: String = str(target.name) if target == scene.root else to
@@ -274,25 +409,31 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
274
409
  % [scene_path, said_from, signal_name, said_to, method_name]
275
410
  )
276
411
  )
277
- source.disconnect(signal_name, Callable(target, method_name))
412
+ var callable: Callable = held["callable"]
413
+ source.disconnect(signal_name, callable)
278
414
 
279
415
  var written: Dictionary = scene.write(
280
- func(saved: Node) -> String:
281
- if _connection_flags(saved, from, signal_name, to, method_name) >= 0:
416
+ func(loaded: Node) -> String:
417
+ if not _saved(loaded, from, signal_name, to, method_name).is_empty():
282
418
  return "the connection would still be in the scene when it loads."
283
419
  return ""
284
420
  )
285
421
  if not written.is_empty():
286
422
  return written
287
423
 
288
- return {
424
+ var removed_flags: int = held["flags"]
425
+ var answer: Dictionary = {
289
426
  "ok": true,
290
427
  "removed": true,
291
428
  "sourceNodePath": from,
292
429
  "signalName": signal_name,
293
430
  "targetNodePath": to,
294
- "methodName": method_name
431
+ "methodName": method_name,
432
+ "flags": removed_flags,
433
+ "flagNames": flag_names(removed_flags),
295
434
  }
435
+ _bound_into(answer, held)
436
+ return answer
296
437
 
297
438
 
298
439
  func list_connections(args: Dictionary) -> Dictionary:
@@ -344,12 +485,17 @@ func list_connections(args: Dictionary) -> Dictionary:
344
485
  "targetNodePath": target_path,
345
486
  "methodName": str(callable.get_method()),
346
487
  "flags": flags,
347
- "flagNames": GdhSceneFile.flag_names(flags),
488
+ "flagNames": flag_names(flags),
348
489
  # False for a connection an instanced scene makes inside itself, which only that
349
490
  # scene can change.
350
491
  "editable": editable,
351
492
  "methodFound": target_obj != null and target_obj.has_method(callable.get_method()),
352
493
  }
494
+ var bound: Dictionary = {
495
+ "binds": callable.get_bound_arguments(),
496
+ "unbinds": callable.get_unbound_arguments_count(),
497
+ }
498
+ _bound_into(listed, bound)
353
499
  var misfit: String = _misfit(node, signal_name, callable, flags)
354
500
  if not misfit.is_empty():
355
501
  listed["misfit"] = misfit