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.
- package/build/cli.js +4563 -3195
- package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
- package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +102 -18
- package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
- package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
- package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
- package/build/godot/addons/gdharness_editor/reading.gd +51 -1
- package/build/godot/addons/gdharness_editor/scene_file.gd +77 -140
- package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +11 -22
- package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
- package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +28 -5
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +39 -20
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -362
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +147 -492
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -3
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +79 -5
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +183 -37
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
- package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +51 -1
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +185 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +86 -79
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +309 -22
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +163 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +5 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +146 -5
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +11 -22
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
- package/build/godot/operations/animation_reads.gd +115 -4
- package/build/godot/operations/audio_buses.gd +93 -7
- package/build/godot/operations/classdb_queries.gd +119 -7
- package/build/godot/operations/declared_values.gd +272 -0
- package/build/godot/operations/dependencies.gd +154 -41
- package/build/godot/operations/editor_tools.gd +22 -14
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +102 -18
- package/build/godot/operations/gdscript_authoring.gd +11 -1
- package/build/godot/operations/godot_operations.gd +16 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +108 -47
- package/build/godot/operations/project_config.gd +83 -69
- package/build/godot/operations/project_diagnostics.gd +4 -0
- package/build/godot/operations/project_validation.gd +266 -0
- package/build/godot/operations/property_paths.gd +89 -16
- package/build/godot/operations/property_values.gd +180 -121
- package/build/godot/operations/reading.gd +51 -1
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +11 -22
- package/build/godot/operations/setting_reads.gd +121 -0
- package/build/godot/operations/value_shapes.gd +128 -1
- package/build/godot/tests/listed.gd +20 -13
- package/build/keeper.js +1371 -706
- 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
|
-
% [
|
|
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
|
-
% [
|
|
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 =
|
|
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.
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
104
|
+
var saved: Dictionary = {}
|
|
62
105
|
var written: Dictionary = scene.write(
|
|
63
|
-
func(
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
79
|
-
"flagNames":
|
|
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
|
-
|
|
137
|
+
_bound_into(answer, saved)
|
|
138
|
+
if not before.is_empty():
|
|
93
139
|
answer["alreadyConnected"] = true
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
123
|
-
##
|
|
124
|
-
func
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
412
|
+
var callable: Callable = held["callable"]
|
|
413
|
+
source.disconnect(signal_name, callable)
|
|
278
414
|
|
|
279
415
|
var written: Dictionary = scene.write(
|
|
280
|
-
func(
|
|
281
|
-
if
|
|
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
|
-
|
|
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":
|
|
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
|