gdharness 1.2.39 → 1.2.41
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 +1435 -1410
- package/build/godot/addons/gdharness_editor/animation_reads.gd +11 -2
- package/build/godot/addons/gdharness_editor/bridge_client.gd +2 -1
- package/build/godot/addons/gdharness_editor/headless_check.gd +22 -11
- package/build/godot/addons/gdharness_editor/property_values.gd +1 -1
- package/build/godot/addons/gdharness_editor/reading.gd +15 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +9 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +39 -5
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -15
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +12 -12
- package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +3 -1
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +15 -2
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +175 -0
- package/build/godot/addons/gdharness_runtime/runtime_items.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +43 -10
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +61 -20
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +3 -1
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +48 -18
- package/build/godot/operations/animation_reads.gd +11 -2
- package/build/godot/operations/editor_tools.gd +9 -0
- package/build/godot/operations/godot_operations.gd +2 -1
- package/build/godot/operations/property_values.gd +1 -1
- package/build/godot/operations/reading.gd +15 -0
- package/build/index.js +2789 -2765
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ extends RefCounted
|
|
|
5
5
|
## stores, which runs no script of the scene's, so a read answers the same either way.
|
|
6
6
|
|
|
7
7
|
const FileReads = preload("file_reads.gd")
|
|
8
|
+
const Read = preload("reading.gd")
|
|
8
9
|
const Serialisation = preload("serialisation.gd")
|
|
9
10
|
|
|
10
11
|
const LOOP_MODE_NAMES: Dictionary[int, String] = {
|
|
@@ -60,7 +61,12 @@ static func _bare(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
60
61
|
return [null, read]
|
|
61
62
|
var type: String = str(read.get("type", ""))
|
|
62
63
|
if not ClassDB.class_exists(type) or not ClassDB.is_parent_class(type, kind):
|
|
63
|
-
|
|
64
|
+
var said: String = "%s not found at: %s" % [kind, node_path]
|
|
65
|
+
if read.is_empty() and FileReads.scene_node(scene_path, node_path.get_slice("/", 0)).is_empty():
|
|
66
|
+
said = Read.from_the_root(
|
|
67
|
+
said, node_path, str(FileReads.scene_node(scene_path, ".").get("name", ""))
|
|
68
|
+
)
|
|
69
|
+
return [null, {"ok": false, "error": said}]
|
|
64
70
|
var bare: Node = ClassDB.instantiate(type)
|
|
65
71
|
bare.name = str(read["name"])
|
|
66
72
|
var given: Dictionary = read["properties"]
|
|
@@ -164,7 +170,10 @@ static func state_machine(tree: AnimationTree, state_machine_path: String) -> An
|
|
|
164
170
|
## the path that failed, and what is there instead.
|
|
165
171
|
static func no_machine(tree: AnimationTree, state_machine_path: String) -> String:
|
|
166
172
|
if tree == null or tree.tree_root == null:
|
|
167
|
-
return
|
|
173
|
+
return (
|
|
174
|
+
"The AnimationTree has no tree_root. Make one with resource_edit create of an "
|
|
175
|
+
+ "AnimationNodeStateMachine, then give its res:// path to tree_root with scene_node set."
|
|
176
|
+
)
|
|
168
177
|
var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
|
|
169
178
|
if current == null:
|
|
170
179
|
return (
|
|
@@ -544,7 +544,8 @@ func send_tool_result(request_id: String, success: bool, result: Variant = null,
|
|
|
544
544
|
|
|
545
545
|
func _send_message(message: Dictionary) -> void:
|
|
546
546
|
if socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
|
|
547
|
-
|
|
547
|
+
# Keys in the order they were put in, which for a map read is the order the project keeps.
|
|
548
|
+
var sent: Error = socket.send_text(JSON.stringify(message, "", false))
|
|
548
549
|
if sent != OK:
|
|
549
550
|
push_error("gdharness: could not send to the server: " + error_string(sent))
|
|
550
551
|
|
|
@@ -147,12 +147,8 @@ static func reached(object: Object, property: String) -> Dictionary:
|
|
|
147
147
|
## Whether [param object] holds, at any depth, an embedded resource with a script that is not a tool.
|
|
148
148
|
static func holds_scripted_embedded(object: Object, seen: Dictionary) -> bool:
|
|
149
149
|
for value: Variant in stored(object).values():
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if not item is Resource or seen.has(item):
|
|
153
|
-
continue
|
|
154
|
-
var resource: Resource = item
|
|
155
|
-
if not resource.is_built_in():
|
|
150
|
+
for resource: Resource in resources_in(value):
|
|
151
|
+
if seen.has(resource) or not resource.is_built_in():
|
|
156
152
|
continue
|
|
157
153
|
seen[resource] = true
|
|
158
154
|
var script: Script = resource.get_script()
|
|
@@ -163,6 +159,25 @@ static func holds_scripted_embedded(object: Object, seen: Dictionary) -> bool:
|
|
|
163
159
|
return false
|
|
164
160
|
|
|
165
161
|
|
|
162
|
+
## The resources [param value] is or holds through arrays and dictionaries at any depth, keys
|
|
163
|
+
## included: a file embeds a resource kept as a map's value, the way an AnimationLibrary keeps its
|
|
164
|
+
## animations, as it embeds one held directly.
|
|
165
|
+
static func resources_in(value: Variant) -> Array[Resource]:
|
|
166
|
+
var found: Array[Resource] = []
|
|
167
|
+
if value is Resource:
|
|
168
|
+
found.append(value)
|
|
169
|
+
elif value is Array:
|
|
170
|
+
var items: Array = value
|
|
171
|
+
for item: Variant in items:
|
|
172
|
+
found.append_array(resources_in(item))
|
|
173
|
+
elif value is Dictionary:
|
|
174
|
+
var entries_held: Dictionary = value
|
|
175
|
+
for key: Variant in entries_held:
|
|
176
|
+
found.append_array(resources_in(key))
|
|
177
|
+
found.append_array(resources_in(entries_held[key]))
|
|
178
|
+
return found
|
|
179
|
+
|
|
180
|
+
|
|
166
181
|
## Puts the file at [param file_path] back as [param previous], or removes it when there was none,
|
|
167
182
|
## answering "" or a sentence saying that failed too.
|
|
168
183
|
static func put_back(file_path: String, previous: Variant) -> String:
|
|
@@ -370,11 +385,7 @@ static func settle_saved(
|
|
|
370
385
|
## Every embedded resource [param values] reach, at any depth, by its id in the file.
|
|
371
386
|
static func embedded_in(values: Array, found: Dictionary) -> Dictionary:
|
|
372
387
|
for value: Variant in values:
|
|
373
|
-
|
|
374
|
-
for item: Variant in items:
|
|
375
|
-
if not item is Resource:
|
|
376
|
-
continue
|
|
377
|
-
var resource: Resource = item
|
|
388
|
+
for resource: Resource in resources_in(value):
|
|
378
389
|
var id: String = resource.get_scene_unique_id()
|
|
379
390
|
if not resource.is_built_in() or id.is_empty() or found.has(id):
|
|
380
391
|
continue
|
|
@@ -590,7 +590,7 @@ static func _listed(type: int) -> bool:
|
|
|
590
590
|
|
|
591
591
|
## [param value] as a caller would write it, for a refusal to quote.
|
|
592
592
|
func shown(value: Variant) -> String:
|
|
593
|
-
return JSON.stringify(_values.serialize_value(value))
|
|
593
|
+
return JSON.stringify(_values.serialize_value(value), "", false)
|
|
594
594
|
|
|
595
595
|
|
|
596
596
|
## A dictionary carrying its own type name, as the serialiser writes it.
|
|
@@ -99,3 +99,18 @@ static func json_or_null(text: String) -> Variant:
|
|
|
99
99
|
if json.parse(text) != OK:
|
|
100
100
|
return null
|
|
101
101
|
return json.data
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## [param said], the refusal for [param node_path] found nowhere in a scene whose root is called
|
|
105
|
+
## [param root_name], with how paths are read when the path starts with that name and the root has
|
|
106
|
+
## no child called that. Paths go from the root, so its own name there names a child of it, and a
|
|
107
|
+
## caller who read the root as Main in a tree wrote Main/Body and was told only that nothing is there.
|
|
108
|
+
static func from_the_root(said: String, node_path: String, root_name: String) -> String:
|
|
109
|
+
var first: String = node_path.get_slice("/", 0)
|
|
110
|
+
if root_name.is_empty() or first != root_name:
|
|
111
|
+
return said
|
|
112
|
+
var rest: String = node_path.substr(first.length() + 1)
|
|
113
|
+
var how: String = 'Paths go from the root, %s, whose own path is "."' % root_name
|
|
114
|
+
if not rest.is_empty():
|
|
115
|
+
how += ', so this one is "%s"' % rest
|
|
116
|
+
return "%s%s %s." % [said, "" if said.ends_with(".") else ".", how]
|
|
@@ -30,6 +30,7 @@ const HeadlessCheck = preload("headless_check.gd")
|
|
|
30
30
|
## How many times what scripts changed is put back before a write is refused for it.
|
|
31
31
|
const PUTTING_BACK_PASSES: int = 4
|
|
32
32
|
const PropertyValues = preload("property_values.gd")
|
|
33
|
+
const Read = preload("reading.gd")
|
|
33
34
|
|
|
34
35
|
## Whether a scan asked for through [method scan_later] is still to start, which a caller waiting
|
|
35
36
|
## for the editor's files to settle has to wait through as well.
|
|
@@ -150,6 +151,14 @@ func refuse(error: String) -> Dictionary:
|
|
|
150
151
|
return {"ok": false, "error": error}
|
|
151
152
|
|
|
152
153
|
|
|
154
|
+
## [param said], the refusal for [param node_path] found nowhere in this scene, with how a path is
|
|
155
|
+
## read when it starts with the root's own name.
|
|
156
|
+
func not_found(said: String, node_path: String) -> String:
|
|
157
|
+
if root == null or root.has_node(NodePath(node_path.get_slice("/", 0))):
|
|
158
|
+
return said
|
|
159
|
+
return Read.from_the_root(said, node_path, str(root.name))
|
|
160
|
+
|
|
161
|
+
|
|
153
162
|
func close() -> void:
|
|
154
163
|
if root != null:
|
|
155
164
|
root.free()
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
@tool
|
|
2
2
|
extends Node
|
|
3
3
|
|
|
4
|
-
## Animations and animation trees, edited in the open editor
|
|
4
|
+
## Animations and animation trees, edited in the open editor or, for a project no editor serves, by a
|
|
5
|
+
## headless engine.
|
|
5
6
|
|
|
6
7
|
const AnimationReads = preload("../animation_reads.gd")
|
|
8
|
+
const HeadlessCheck = preload("../headless_check.gd")
|
|
7
9
|
const PropertyValues = preload("../property_values.gd")
|
|
8
10
|
const Read = preload("../reading.gd")
|
|
9
11
|
const SceneFile = preload("../scene_file.gd")
|
|
@@ -46,7 +48,9 @@ func _open_at(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
46
48
|
return [scene, null, refused]
|
|
47
49
|
var node: Node = _find_node(scene.root, node_path)
|
|
48
50
|
if node == null or not node.is_class(kind):
|
|
49
|
-
return [
|
|
51
|
+
return [
|
|
52
|
+
scene, null, scene.refuse(scene.not_found("%s not found at: %s" % [kind, node_path], node_path))
|
|
53
|
+
]
|
|
50
54
|
var foreign: String = scene.foreign(node)
|
|
51
55
|
if not foreign.is_empty():
|
|
52
56
|
return [scene, null, scene.refuse(foreign)]
|
|
@@ -62,25 +66,55 @@ func _read_at(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
62
66
|
return [scene, null, refused]
|
|
63
67
|
var node: Node = _find_node(scene.root, node_path)
|
|
64
68
|
if node == null or not node.is_class(kind):
|
|
65
|
-
return [
|
|
69
|
+
return [
|
|
70
|
+
scene, null, scene.refuse(scene.not_found("%s not found at: %s" % [kind, node_path], node_path))
|
|
71
|
+
]
|
|
66
72
|
return [scene, node, {}]
|
|
67
73
|
|
|
68
74
|
|
|
69
75
|
## Writes [param scene] once [param check] passes, then the files of [param homes], answering
|
|
70
76
|
## [param answer] or a refusal.
|
|
71
77
|
func _finish(scene: SceneFile, check: Callable, homes: Dictionary, answer: Dictionary) -> Dictionary:
|
|
78
|
+
if not Engine.is_editor_hint():
|
|
79
|
+
for file: Variant in homes:
|
|
80
|
+
var home: Resource = homes[file]
|
|
81
|
+
if _scripted(home):
|
|
82
|
+
return (
|
|
83
|
+
scene
|
|
84
|
+
. refuse(
|
|
85
|
+
(
|
|
86
|
+
(
|
|
87
|
+
(
|
|
88
|
+
"Nothing was written: %s holds a resource with a script that is not a tool, and what "
|
|
89
|
+
+ "that script changed when a headless engine loaded it cannot be told apart from the "
|
|
90
|
+
+ "edit. "
|
|
91
|
+
)
|
|
92
|
+
% file
|
|
93
|
+
)
|
|
94
|
+
+ HeadlessCheck.REMEDY
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
)
|
|
72
98
|
var written: Dictionary = scene.write(check)
|
|
73
99
|
if not written.is_empty():
|
|
74
100
|
return written
|
|
75
101
|
for file: Variant in homes:
|
|
76
102
|
var resource: Resource = homes[file]
|
|
77
|
-
var saving: Error =
|
|
103
|
+
var saving: Error = SceneFile.save_keeping_uid(resource, str(file))
|
|
78
104
|
if saving != OK:
|
|
79
105
|
return {"ok": false, "error": "Failed to save %s: %s" % [file, error_string(saving)]}
|
|
80
|
-
|
|
106
|
+
if Engine.is_editor_hint():
|
|
107
|
+
EditorInterface.get_resource_filesystem().update_file(str(file))
|
|
81
108
|
return answer
|
|
82
109
|
|
|
83
110
|
|
|
111
|
+
## Whether [param resource], or one it embeds, has a script that is not a tool, which a headless
|
|
112
|
+
## engine ran as it loaded the file.
|
|
113
|
+
func _scripted(resource: Resource) -> bool:
|
|
114
|
+
var script: Script = resource.get_script()
|
|
115
|
+
return (script != null and not script.is_tool()) or HeadlessCheck.holds_scripted_embedded(resource, {})
|
|
116
|
+
|
|
117
|
+
|
|
84
118
|
func _parse_json_maybe(value: Variant) -> Variant:
|
|
85
119
|
if typeof(value) != TYPE_STRING:
|
|
86
120
|
return value
|
|
@@ -399,6 +399,19 @@ func _atlas(entry: Variant, index: int) -> Array:
|
|
|
399
399
|
if texture_path.split("/").has(".."):
|
|
400
400
|
return [false, "sources[%d].texture leaves the project: %s" % [index, texture_path]]
|
|
401
401
|
if not ResourceLoader.exists(texture_path):
|
|
402
|
+
# An image the engine has not imported loads as nothing, where "no texture" would send a caller
|
|
403
|
+
# looking for a file that is there.
|
|
404
|
+
if FileAccess.file_exists(texture_path) and not FileAccess.file_exists(texture_path + ".import"):
|
|
405
|
+
return [
|
|
406
|
+
false,
|
|
407
|
+
(
|
|
408
|
+
(
|
|
409
|
+
"sources[%d].texture: %s has not been imported, so the engine has no texture for it "
|
|
410
|
+
+ "yet. project_import reimport with resourcePath %s imports it."
|
|
411
|
+
)
|
|
412
|
+
% [index, texture_path, texture_path]
|
|
413
|
+
)
|
|
414
|
+
]
|
|
402
415
|
return [false, "sources[%d].texture: no texture at %s" % [index, texture_path]]
|
|
403
416
|
var texture: Texture2D = load(texture_path) as Texture2D
|
|
404
417
|
if texture == null:
|
|
@@ -720,13 +733,15 @@ func set_theme_color(args: Dictionary) -> Dictionary:
|
|
|
720
733
|
return {"ok": false, "error": "The theme saved without %s/%s" % [control_type, color_name]}
|
|
721
734
|
|
|
722
735
|
var stored: Color = saved.get_color(color_name, control_type)
|
|
723
|
-
return
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
736
|
+
return _noting_variation(
|
|
737
|
+
{
|
|
738
|
+
"ok": true,
|
|
739
|
+
"themePath": theme_path,
|
|
740
|
+
"controlType": control_type,
|
|
741
|
+
"colorName": color_name,
|
|
742
|
+
"color": {"r": stored.r, "g": stored.g, "b": stored.b, "a": stored.a}
|
|
743
|
+
}
|
|
744
|
+
)
|
|
730
745
|
|
|
731
746
|
|
|
732
747
|
func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
@@ -743,7 +758,23 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
|
743
758
|
if theme == null:
|
|
744
759
|
return {"ok": false, "error": "No Theme at %s. Create one there first." % theme_path}
|
|
745
760
|
|
|
746
|
-
|
|
761
|
+
# Godot reads a size of 0 or less as unset and answers the theme's default, so -3 would be read
|
|
762
|
+
# back as 16.
|
|
763
|
+
var pixels: float = Read.as_float(args.get("size"), 0.0)
|
|
764
|
+
if not is_finite(pixels) or pixels < 1 or pixels != floorf(pixels):
|
|
765
|
+
var shown: String = str(int(pixels)) if pixels == floorf(pixels) else str(pixels)
|
|
766
|
+
return {
|
|
767
|
+
"ok": false,
|
|
768
|
+
"error":
|
|
769
|
+
(
|
|
770
|
+
(
|
|
771
|
+
"size must be a whole number of pixels, 1 or more, not %s: Godot reads 0 or less as no size "
|
|
772
|
+
% shown
|
|
773
|
+
)
|
|
774
|
+
+ "and uses the theme's default instead."
|
|
775
|
+
)
|
|
776
|
+
}
|
|
777
|
+
theme.set_font_size(font_size_name, control_type, int(pixels))
|
|
747
778
|
|
|
748
779
|
var unsaved: String = _save_theme(theme, theme_path, "%s/font_sizes/%s" % [control_type, font_size_name])
|
|
749
780
|
if not unsaved.is_empty():
|
|
@@ -753,10 +784,32 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
|
753
784
|
if saved == null or not saved.has_font_size(font_size_name, control_type):
|
|
754
785
|
return {"ok": false, "error": "The theme saved without %s/%s" % [control_type, font_size_name]}
|
|
755
786
|
|
|
756
|
-
return
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
787
|
+
return _noting_variation(
|
|
788
|
+
{
|
|
789
|
+
"ok": true,
|
|
790
|
+
"themePath": theme_path,
|
|
791
|
+
"controlType": control_type,
|
|
792
|
+
"fontSizeName": font_size_name,
|
|
793
|
+
"size": saved.get_font_size(font_size_name, control_type)
|
|
794
|
+
}
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
## [param answer] with a note when its controlType names no class a theme styles. Any name is a
|
|
799
|
+
## theme type, so one that is no class is a type variation, which styles only a control whose
|
|
800
|
+
## theme_type_variation names it, and a misspelt "Buton" would otherwise read as Button set.
|
|
801
|
+
func _noting_variation(answer: Dictionary) -> Dictionary:
|
|
802
|
+
var type: String = str(answer.get("controlType", ""))
|
|
803
|
+
if (
|
|
804
|
+
ClassDB.class_exists(type)
|
|
805
|
+
and (ClassDB.is_parent_class(type, "Control") or ClassDB.is_parent_class(type, "Window"))
|
|
806
|
+
):
|
|
807
|
+
return answer
|
|
808
|
+
answer["note"] = (
|
|
809
|
+
(
|
|
810
|
+
"%s is not a Control or Window class, so this is a type variation: it styles only a control "
|
|
811
|
+
+ "whose theme_type_variation is %s."
|
|
812
|
+
)
|
|
813
|
+
% [type, type]
|
|
814
|
+
)
|
|
815
|
+
return answer
|
|
@@ -111,7 +111,7 @@ func _kept(scene: SceneFile, saved: Node, node: Node, names: Array) -> String:
|
|
|
111
111
|
"%s.%s would load from the file as %s: the scene does not keep it, because the property "
|
|
112
112
|
+ "is not one a scene stores or the node belongs to another scene."
|
|
113
113
|
)
|
|
114
|
-
% [at, property, JSON.stringify(_values.serialize_value(loaded))]
|
|
114
|
+
% [at, property, JSON.stringify(_values.serialize_value(loaded), "", false)]
|
|
115
115
|
)
|
|
116
116
|
return ""
|
|
117
117
|
|
|
@@ -312,7 +312,7 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
312
312
|
var root: Node = scene.root
|
|
313
313
|
var parent: Node = _find_node(root, parent_node_path)
|
|
314
314
|
if not parent:
|
|
315
|
-
return scene.refuse("Parent node not found: " + parent_node_path)
|
|
315
|
+
return scene.refuse(scene.not_found("Parent node not found: " + parent_node_path, parent_node_path))
|
|
316
316
|
var foreign: String = scene.foreign(parent)
|
|
317
317
|
if not foreign.is_empty():
|
|
318
318
|
return scene.refuse(foreign)
|
|
@@ -387,7 +387,7 @@ func delete_node(args: Dictionary) -> Dictionary:
|
|
|
387
387
|
|
|
388
388
|
var node: Node = _find_node(scene.root, node_path)
|
|
389
389
|
if not node:
|
|
390
|
-
return scene.refuse("Node not found: " + node_path)
|
|
390
|
+
return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
|
|
391
391
|
var fixed: String = scene.fixed(node)
|
|
392
392
|
if not fixed.is_empty():
|
|
393
393
|
return scene.refuse(fixed)
|
|
@@ -427,7 +427,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
|
|
|
427
427
|
var root: Node = scene.root
|
|
428
428
|
var source: Node = _find_node(root, node_path)
|
|
429
429
|
if not source:
|
|
430
|
-
return scene.refuse("Node not found: " + node_path)
|
|
430
|
+
return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
|
|
431
431
|
if source == root:
|
|
432
432
|
return scene.refuse("The root of %s cannot be copied into its own scene." % scene_path)
|
|
433
433
|
var foreign: String = scene.foreign(source)
|
|
@@ -438,7 +438,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
|
|
|
438
438
|
if not parent_path.is_empty():
|
|
439
439
|
target_parent = _find_node(root, parent_path)
|
|
440
440
|
if not target_parent:
|
|
441
|
-
return scene.refuse("Parent not found: " + parent_path)
|
|
441
|
+
return scene.refuse(scene.not_found("Parent not found: " + parent_path, parent_path))
|
|
442
442
|
foreign = scene.foreign(target_parent)
|
|
443
443
|
if not foreign.is_empty():
|
|
444
444
|
return scene.refuse(foreign)
|
|
@@ -491,9 +491,9 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
491
491
|
var node: Node = _find_node(root, node_path)
|
|
492
492
|
var new_parent: Node = _find_node(root, new_parent_path)
|
|
493
493
|
if not node:
|
|
494
|
-
return scene.refuse("Node not found: " + node_path)
|
|
494
|
+
return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
|
|
495
495
|
if not new_parent:
|
|
496
|
-
return scene.refuse("New parent not found: " + new_parent_path)
|
|
496
|
+
return scene.refuse(scene.not_found("New parent not found: " + new_parent_path, new_parent_path))
|
|
497
497
|
var fixed: String = scene.fixed(node)
|
|
498
498
|
if not fixed.is_empty():
|
|
499
499
|
return scene.refuse(fixed)
|
|
@@ -562,7 +562,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
562
562
|
|
|
563
563
|
var node: Node = _find_node(scene.root, node_path)
|
|
564
564
|
if not node:
|
|
565
|
-
return scene.refuse("Node not found: " + node_path)
|
|
565
|
+
return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
|
|
566
566
|
var foreign: String = scene.foreign(node)
|
|
567
567
|
if not foreign.is_empty():
|
|
568
568
|
return scene.refuse(foreign)
|
|
@@ -606,7 +606,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
|
|
|
606
606
|
|
|
607
607
|
var node: Node = _find_node(scene.root, node_path)
|
|
608
608
|
if not node:
|
|
609
|
-
return scene.refuse("Node not found: " + node_path)
|
|
609
|
+
return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
|
|
610
610
|
|
|
611
611
|
# Against the default the node would have in no scene: its script's for what the script
|
|
612
612
|
# declares, the class's for the rest. Compared with a bare instance of the class, every exported
|
|
@@ -792,9 +792,9 @@ func _connection_ends(
|
|
|
792
792
|
var source: Node = _find_node(scene.root, source_node_path)
|
|
793
793
|
var target: Node = _find_node(scene.root, target_node_path)
|
|
794
794
|
if not source:
|
|
795
|
-
return [null, "Source node not found: " + source_node_path]
|
|
795
|
+
return [null, scene.not_found("Source node not found: " + source_node_path, source_node_path)]
|
|
796
796
|
if not target:
|
|
797
|
-
return [null, "Target node not found: " + target_node_path]
|
|
797
|
+
return [null, scene.not_found("Target node not found: " + target_node_path, target_node_path)]
|
|
798
798
|
# pack drops a connection with either end inside an instanced scene, and the answer said it was
|
|
799
799
|
# made or removed.
|
|
800
800
|
for end: Node in [source, target]:
|
|
@@ -897,7 +897,7 @@ func list_connections(args: Dictionary) -> Dictionary:
|
|
|
897
897
|
if not filter_path.is_empty():
|
|
898
898
|
filtered = _find_node(root, filter_path)
|
|
899
899
|
if filtered == null:
|
|
900
|
-
return scene.refuse("Node not found: " + filter_path)
|
|
900
|
+
return scene.refuse(scene.not_found("Node not found: " + filter_path, filter_path))
|
|
901
901
|
var nodes: Array = []
|
|
902
902
|
_collect_nodes_recursive(root, ".", nodes)
|
|
903
903
|
|
|
@@ -99,3 +99,18 @@ static func json_or_null(text: String) -> Variant:
|
|
|
99
99
|
if json.parse(text) != OK:
|
|
100
100
|
return null
|
|
101
101
|
return json.data
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## [param said], the refusal for [param node_path] found nowhere in a scene whose root is called
|
|
105
|
+
## [param root_name], with how paths are read when the path starts with that name and the root has
|
|
106
|
+
## no child called that. Paths go from the root, so its own name there names a child of it, and a
|
|
107
|
+
## caller who read the root as Main in a tree wrote Main/Body and was told only that nothing is there.
|
|
108
|
+
static func from_the_root(said: String, node_path: String, root_name: String) -> String:
|
|
109
|
+
var first: String = node_path.get_slice("/", 0)
|
|
110
|
+
if root_name.is_empty() or first != root_name:
|
|
111
|
+
return said
|
|
112
|
+
var rest: String = node_path.substr(first.length() + 1)
|
|
113
|
+
var how: String = 'Paths go from the root, %s, whose own path is "."' % root_name
|
|
114
|
+
if not rest.is_empty():
|
|
115
|
+
how += ', so this one is "%s"' % rest
|
|
116
|
+
return "%s%s %s." % [said, "" if said.ends_with(".") else ".", how]
|
|
@@ -660,7 +660,9 @@ static func _is_empty(owed: Variant) -> bool:
|
|
|
660
660
|
## taken the lot: see [member _outgoing].
|
|
661
661
|
func _send_response(client: StreamPeerTCP, data: Dictionary) -> void:
|
|
662
662
|
var owed: PackedByteArray = _outgoing.get(client, PackedByteArray())
|
|
663
|
-
|
|
663
|
+
# Keys left in the order they were put in: sorted, a map read came back in another order than
|
|
664
|
+
# its keys() and the game iterate it in.
|
|
665
|
+
owed.append_array((JSON.stringify(data, "", false) + "\n").to_utf8_buffer())
|
|
664
666
|
_outgoing[client] = owed
|
|
665
667
|
|
|
666
668
|
|
|
@@ -129,8 +129,8 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
129
129
|
% [
|
|
130
130
|
reached["called"],
|
|
131
131
|
named,
|
|
132
|
-
JSON.stringify(_values.serialize(given)),
|
|
133
|
-
JSON.stringify(_values.serialize(now)),
|
|
132
|
+
JSON.stringify(_values.serialize(given), "", false),
|
|
133
|
+
JSON.stringify(_values.serialize(now), "", false),
|
|
134
134
|
why,
|
|
135
135
|
]
|
|
136
136
|
)
|
|
@@ -12,6 +12,7 @@ const Values = preload("runtime_values.gd")
|
|
|
12
12
|
const Queries = preload("runtime_queries.gd")
|
|
13
13
|
const Read = preload("reading.gd")
|
|
14
14
|
const Hover = preload("runtime_hover.gd")
|
|
15
|
+
const Items = preload("runtime_items.gd")
|
|
15
16
|
const Screen = preload("runtime_screen.gd")
|
|
16
17
|
const Targets = preload("runtime_targets.gd")
|
|
17
18
|
const Mouse = preload("runtime_mouse.gd")
|
|
@@ -509,6 +510,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
509
510
|
var node_path: String = str(params.get("path", ""))
|
|
510
511
|
var wanted: String = str(params.get("says", ""))
|
|
511
512
|
var found: Variant = null
|
|
513
|
+
var item: Variant = null
|
|
512
514
|
if not wanted.is_empty():
|
|
513
515
|
var picked: Dictionary = Targets.control_saying(
|
|
514
516
|
_host.get_tree().root, "/root" if node_path.is_empty() else node_path, wanted, params.get("index")
|
|
@@ -517,6 +519,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
517
519
|
return picked
|
|
518
520
|
node_path = picked["path"]
|
|
519
521
|
found = picked["found"]
|
|
522
|
+
item = picked.get("item")
|
|
520
523
|
if node_path.is_empty():
|
|
521
524
|
return {"type": "error", "message": "click needs a path, or says to find the control by its words"}
|
|
522
525
|
|
|
@@ -541,6 +544,16 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
541
544
|
# other words, a link among them. Said either way, so a press on the middle is never read as one
|
|
542
545
|
# on the words.
|
|
543
546
|
var aim: Vector2 = control.size * 0.5
|
|
547
|
+
var item_scrolled: bool = false
|
|
548
|
+
if item != null:
|
|
549
|
+
var found_by_words: Dictionary = found
|
|
550
|
+
var placed_item: Dictionary = await Items.in_view(
|
|
551
|
+
_host.get_tree(), control, node_path, item, wanted, found_by_words
|
|
552
|
+
)
|
|
553
|
+
if placed_item.has("message"):
|
|
554
|
+
return placed_item
|
|
555
|
+
aim = placed_item["point"]
|
|
556
|
+
item_scrolled = placed_item["scrolled"]
|
|
544
557
|
var rich: RichTextLabel = control as RichTextLabel
|
|
545
558
|
if found != null and rich != null:
|
|
546
559
|
var placed: Dictionary = Targets.point_on_words(rich, wanted)
|
|
@@ -568,9 +581,9 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
568
581
|
# off the top of its container is inside the viewport, behind whatever is drawn up there, so
|
|
569
582
|
# the click went to that instead and said so. Measured on a hall whose staff panel had been
|
|
570
583
|
# scrolled past: the button was at y 69 and its container started at y 166.
|
|
571
|
-
var scrolled: bool =
|
|
584
|
+
var scrolled: bool = item_scrolled
|
|
572
585
|
if not _in_sight(control, own, local):
|
|
573
|
-
scrolled = await _scroll_into_view(control)
|
|
586
|
+
scrolled = await _scroll_into_view(control) or item_scrolled
|
|
574
587
|
var left: String = Values.afterwards(control)
|
|
575
588
|
if left != "in_tree":
|
|
576
589
|
return {
|