gdharness 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +163 -85
- package/build/godot/addons/gdharness_editor/animation_reads.gd +14 -11
- package/build/godot/addons/gdharness_editor/declared_values.gd +157 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
- package/build/godot/addons/gdharness_editor/file_walk.gd +134 -0
- package/build/godot/addons/gdharness_editor/file_walk.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
- package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
- package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +249 -227
- package/build/godot/addons/gdharness_editor/reading.gd +15 -0
- package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
- package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +100 -15
- package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
- package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +402 -55
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +88 -5
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +102 -337
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +10 -26
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +30 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
- package/build/godot/operations/animation_reads.gd +14 -11
- package/build/godot/operations/declared_values.gd +157 -4
- package/build/godot/operations/dependencies.gd +14 -39
- package/build/godot/operations/editor_tools.gd +7 -3
- package/build/godot/operations/file_reads.gd +15 -3
- package/build/godot/operations/file_walk.gd +1 -0
- package/build/godot/operations/gdscript_analysis.gd +229 -75
- package/build/godot/operations/gdscript_authoring.gd +142 -34
- package/build/godot/operations/import_pipeline.gd +17 -6
- package/build/godot/operations/logger.gd +7 -0
- package/build/godot/operations/property_paths.gd +225 -0
- package/build/godot/operations/property_values.gd +249 -227
- package/build/godot/operations/reading.gd +15 -0
- package/build/godot/operations/same_values.gd +177 -0
- package/build/godot/operations/serialisation.gd +75 -3
- package/package.json +1 -1
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends Node
|
|
3
|
+
|
|
4
|
+
## Signal connections in a scene file, made, removed and listed on a copy of the file as the other
|
|
5
|
+
## scene tools edit one.
|
|
6
|
+
|
|
7
|
+
const GdhRead = preload("../reading.gd")
|
|
8
|
+
const GdhSceneFile = preload("../scene_file.gd")
|
|
9
|
+
|
|
10
|
+
## What every emission meets when a connection cannot call its method.
|
|
11
|
+
const FAILS: String = ", so every emission fails to call it."
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
func connect_signal(args: Dictionary) -> Dictionary:
|
|
15
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
16
|
+
var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
|
|
17
|
+
var source_node_path: String = str(args.get("sourceNodePath", ""))
|
|
18
|
+
var signal_name: String = str(args.get("signalName", ""))
|
|
19
|
+
var target_node_path: String = str(args.get("targetNodePath", ""))
|
|
20
|
+
var method_name: String = str(args.get("methodName", ""))
|
|
21
|
+
# A connection without CONNECT_PERSIST is a runtime one, and PackedScene.pack drops those on
|
|
22
|
+
# 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
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
source_node_path.is_empty()
|
|
27
|
+
or signal_name.is_empty()
|
|
28
|
+
or target_node_path.is_empty()
|
|
29
|
+
or method_name.is_empty()
|
|
30
|
+
):
|
|
31
|
+
return {"ok": false, "error": "Missing required signal connection arguments"}
|
|
32
|
+
|
|
33
|
+
var scene: GdhSceneFile = GdhSceneFile.new()
|
|
34
|
+
var refused: Dictionary = scene.open(scene_path, true)
|
|
35
|
+
if not refused.is_empty():
|
|
36
|
+
return refused
|
|
37
|
+
|
|
38
|
+
var ends: Array = _connection_ends(scene, source_node_path, signal_name, target_node_path)
|
|
39
|
+
if ends[0] == null:
|
|
40
|
+
return scene.refuse(str(ends[1]))
|
|
41
|
+
var source: Node = ends[0]
|
|
42
|
+
var target: Node = ends[1]
|
|
43
|
+
|
|
44
|
+
# Reconnected rather than left alone when the flags differ, so the connection saved is the one
|
|
45
|
+
# asked for. Left alone, the answer gave the flags asked for and the file kept the old ones.
|
|
46
|
+
var callable: Callable = Callable(target, method_name)
|
|
47
|
+
var method_found: bool = target.has_method(method_name)
|
|
48
|
+
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)
|
|
55
|
+
var connect_result: Error = source.connect(signal_name, callable, flags)
|
|
56
|
+
if connect_result != OK:
|
|
57
|
+
return scene.refuse("Failed to connect signal: " + error_string(connect_result))
|
|
58
|
+
|
|
59
|
+
var from: String = scene.path_of(source)
|
|
60
|
+
var to: String = scene.path_of(target)
|
|
61
|
+
var saved_flags: Array = [-1]
|
|
62
|
+
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:
|
|
66
|
+
return "the connection would not be in the scene when it loads."
|
|
67
|
+
return ""
|
|
68
|
+
)
|
|
69
|
+
if not written.is_empty():
|
|
70
|
+
return written
|
|
71
|
+
|
|
72
|
+
var answer: Dictionary = {
|
|
73
|
+
"ok": true,
|
|
74
|
+
"sourceNodePath": from,
|
|
75
|
+
"signalName": signal_name,
|
|
76
|
+
"targetNodePath": to,
|
|
77
|
+
"methodName": method_name,
|
|
78
|
+
"flags": saved_flags[0],
|
|
79
|
+
"flagNames": GdhSceneFile.flag_names(GdhRead.as_int(saved_flags[0])),
|
|
80
|
+
# Connected either way, as the editor's own dialog allows, since the method may be written
|
|
81
|
+
# next; a caller told nothing learnt of a misspelt one only when the signal fired.
|
|
82
|
+
"methodFound": method_found
|
|
83
|
+
}
|
|
84
|
+
# Written all the same, for the reason a missing method is: the method may be changed next.
|
|
85
|
+
if method_found:
|
|
86
|
+
answer["argumentsFit"] = misfit.is_empty()
|
|
87
|
+
if not misfit.is_empty():
|
|
88
|
+
answer["misfit"] = misfit
|
|
89
|
+
# A second connect of the same pair read exactly like the first, so a caller could not tell a
|
|
90
|
+
# connection made from one that was already there, and a list showing one where two were made
|
|
91
|
+
# looked like a lost write.
|
|
92
|
+
if flags_before >= 0:
|
|
93
|
+
answer["alreadyConnected"] = true
|
|
94
|
+
if flags_before != GdhRead.as_int(saved_flags[0]):
|
|
95
|
+
answer["flagsBefore"] = GdhSceneFile.flag_names(flags_before)
|
|
96
|
+
return answer
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## The two ends of a connection in [param scene], as [source, target], or [null, why not].
|
|
100
|
+
func _connection_ends(
|
|
101
|
+
scene: GdhSceneFile, source_node_path: String, signal_name: String, target_node_path: String
|
|
102
|
+
) -> Array:
|
|
103
|
+
var source: Node = GdhSceneFile.node_at(scene.root, source_node_path)
|
|
104
|
+
var target: Node = GdhSceneFile.node_at(scene.root, target_node_path)
|
|
105
|
+
if not source:
|
|
106
|
+
return [null, scene.not_found("Source node not found: " + source_node_path, source_node_path)]
|
|
107
|
+
if not target:
|
|
108
|
+
return [null, scene.not_found("Target node not found: " + target_node_path, target_node_path)]
|
|
109
|
+
# pack drops a connection with either end inside an instanced scene, and the answer said it was
|
|
110
|
+
# made or removed.
|
|
111
|
+
for end: Node in [source, target]:
|
|
112
|
+
var foreign: String = scene.foreign(end)
|
|
113
|
+
if not foreign.is_empty():
|
|
114
|
+
return [null, foreign]
|
|
115
|
+
if not source.has_signal(signal_name):
|
|
116
|
+
var said: String = "%s has no signal %s" % [source_node_path, signal_name]
|
|
117
|
+
var offered: String = GdhRead.offered(said, signal_name, GdhRead.signals_of(source))
|
|
118
|
+
return [null, GdhSceneFile.missing_on(source, offered)]
|
|
119
|
+
return [source, target]
|
|
120
|
+
|
|
121
|
+
|
|
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:
|
|
125
|
+
var source: Node = root.get_node_or_null(from)
|
|
126
|
+
var target: Node = root.get_node_or_null(to)
|
|
127
|
+
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
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
## Why what [param signal_name] of [param source] passes cannot reach [param callable], or "" when it
|
|
142
|
+
## can or the method is not there to judge. Only what fails on every emission is named: too few or
|
|
143
|
+
## too many arguments, an object passed where the method takes a value or the reverse, and an engine
|
|
144
|
+
## class unrelated to the one the method takes. Anything that may convert or narrow when it runs is
|
|
145
|
+
## left alone, since a warning about a connection that works is one nobody can act on.
|
|
146
|
+
static func _misfit(source: Node, signal_name: String, callable: Callable, flags: int) -> String:
|
|
147
|
+
var target: Object = callable.get_object()
|
|
148
|
+
var method: String = str(callable.get_method())
|
|
149
|
+
var taken: Dictionary = {}
|
|
150
|
+
for info: Dictionary in target.get_method_list() if target != null else []:
|
|
151
|
+
if str(info.get("name", "")) == method:
|
|
152
|
+
taken = info
|
|
153
|
+
break
|
|
154
|
+
if taken.is_empty() or GdhRead.as_int(taken.get("flags", 0)) & METHOD_FLAG_VARARG:
|
|
155
|
+
return ""
|
|
156
|
+
var passed: Array[Dictionary] = []
|
|
157
|
+
for info: Dictionary in source.get_signal_list():
|
|
158
|
+
if str(info.get("name", "")) == signal_name:
|
|
159
|
+
for argument: Dictionary in info.get("args", []):
|
|
160
|
+
passed.append(argument)
|
|
161
|
+
break
|
|
162
|
+
if flags & Object.CONNECT_APPEND_SOURCE_OBJECT:
|
|
163
|
+
passed.append({"name": "source", "type": TYPE_OBJECT, "class_name": source.get_class()})
|
|
164
|
+
passed = passed.slice(0, maxi(passed.size() - callable.get_unbound_arguments_count(), 0))
|
|
165
|
+
for bound: Variant in callable.get_bound_arguments():
|
|
166
|
+
var bound_class: String = ""
|
|
167
|
+
if bound is Object:
|
|
168
|
+
var object: Object = bound
|
|
169
|
+
bound_class = object.get_class()
|
|
170
|
+
passed.append({"name": "bound", "type": typeof(bound), "class_name": bound_class})
|
|
171
|
+
|
|
172
|
+
var parameters: Array = taken.get("args", [])
|
|
173
|
+
var defaults: Array = taken.get("default_args", [])
|
|
174
|
+
var needed: int = parameters.size() - defaults.size()
|
|
175
|
+
if passed.size() < needed:
|
|
176
|
+
return "%s passes %s, and %s needs %d%s" % [signal_name, _count(passed.size()), method, needed, FAILS]
|
|
177
|
+
if passed.size() > parameters.size():
|
|
178
|
+
var most: String = "none" if parameters.is_empty() else "at most %d" % parameters.size()
|
|
179
|
+
return "%s passes %s, and %s takes %s%s" % [signal_name, _count(passed.size()), method, most, FAILS]
|
|
180
|
+
for at: int in passed.size():
|
|
181
|
+
var given: Dictionary = passed[at]
|
|
182
|
+
var parameter: Dictionary = parameters[at]
|
|
183
|
+
if not _unrelated(given, parameter):
|
|
184
|
+
continue
|
|
185
|
+
return (
|
|
186
|
+
"%s passes argument %d as %s, and %s takes it as %s: %s%s"
|
|
187
|
+
% [
|
|
188
|
+
signal_name,
|
|
189
|
+
at + 1,
|
|
190
|
+
_type_named(given),
|
|
191
|
+
method,
|
|
192
|
+
str(parameter.get("name", "")),
|
|
193
|
+
_type_named(parameter),
|
|
194
|
+
FAILS
|
|
195
|
+
]
|
|
196
|
+
)
|
|
197
|
+
return ""
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
## Whether a value declared as [param given] can never be passed as [param parameter].
|
|
201
|
+
static func _unrelated(given: Dictionary, parameter: Dictionary) -> bool:
|
|
202
|
+
var given_type: int = GdhRead.as_int(given.get("type", TYPE_NIL))
|
|
203
|
+
var taken_type: int = GdhRead.as_int(parameter.get("type", TYPE_NIL))
|
|
204
|
+
if given_type == TYPE_NIL or taken_type == TYPE_NIL:
|
|
205
|
+
return false
|
|
206
|
+
if (given_type == TYPE_OBJECT) != (taken_type == TYPE_OBJECT):
|
|
207
|
+
return true
|
|
208
|
+
if given_type != TYPE_OBJECT:
|
|
209
|
+
return false
|
|
210
|
+
var given_class: String = str(given.get("class_name", ""))
|
|
211
|
+
var taken_class: String = str(parameter.get("class_name", ""))
|
|
212
|
+
# A class a script declares is not in ClassDB, and an engine class can still hold an instance of
|
|
213
|
+
# one, so only two engine classes are judged.
|
|
214
|
+
if not ClassDB.class_exists(given_class) or not ClassDB.class_exists(taken_class):
|
|
215
|
+
return false
|
|
216
|
+
return (
|
|
217
|
+
not ClassDB.is_parent_class(given_class, taken_class)
|
|
218
|
+
and not ClassDB.is_parent_class(taken_class, given_class)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
static func _type_named(declared: Dictionary) -> String:
|
|
223
|
+
var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL))
|
|
224
|
+
var named: String = str(declared.get("class_name", ""))
|
|
225
|
+
if type == TYPE_OBJECT and not named.is_empty():
|
|
226
|
+
return named
|
|
227
|
+
return type_string(type)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
static func _count(arguments: int) -> String:
|
|
231
|
+
if arguments == 0:
|
|
232
|
+
return "no arguments"
|
|
233
|
+
return "1 argument" if arguments == 1 else "%d arguments" % arguments
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
func disconnect_signal(args: Dictionary) -> Dictionary:
|
|
237
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
238
|
+
var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
|
|
239
|
+
var source_node_path: String = str(args.get("sourceNodePath", ""))
|
|
240
|
+
var signal_name: String = str(args.get("signalName", ""))
|
|
241
|
+
var target_node_path: String = str(args.get("targetNodePath", ""))
|
|
242
|
+
var method_name: String = str(args.get("methodName", ""))
|
|
243
|
+
|
|
244
|
+
if (
|
|
245
|
+
source_node_path.is_empty()
|
|
246
|
+
or signal_name.is_empty()
|
|
247
|
+
or target_node_path.is_empty()
|
|
248
|
+
or method_name.is_empty()
|
|
249
|
+
):
|
|
250
|
+
return {"ok": false, "error": "Missing required signal disconnection arguments"}
|
|
251
|
+
|
|
252
|
+
var scene: GdhSceneFile = GdhSceneFile.new()
|
|
253
|
+
var refused: Dictionary = scene.open(scene_path, true)
|
|
254
|
+
if not refused.is_empty():
|
|
255
|
+
return refused
|
|
256
|
+
|
|
257
|
+
var ends: Array = _connection_ends(scene, source_node_path, signal_name, target_node_path)
|
|
258
|
+
if ends[0] == null:
|
|
259
|
+
return scene.refuse(str(ends[1]))
|
|
260
|
+
var source: Node = ends[0]
|
|
261
|
+
var target: Node = ends[1]
|
|
262
|
+
|
|
263
|
+
var from: String = scene.path_of(source)
|
|
264
|
+
var to: String = scene.path_of(target)
|
|
265
|
+
# A connection that was never there saved the scene unchanged and answered as removed, so a
|
|
266
|
+
# typo in either name read as success.
|
|
267
|
+
if _connection_flags(scene.root, from, signal_name, to, method_name) < 0:
|
|
268
|
+
return scene.refuse(
|
|
269
|
+
"%s has no connection from %s.%s to %s.%s." % [scene_path, from, signal_name, to, method_name]
|
|
270
|
+
)
|
|
271
|
+
source.disconnect(signal_name, Callable(target, method_name))
|
|
272
|
+
|
|
273
|
+
var written: Dictionary = scene.write(
|
|
274
|
+
func(saved: Node) -> String:
|
|
275
|
+
if _connection_flags(saved, from, signal_name, to, method_name) >= 0:
|
|
276
|
+
return "the connection would still be in the scene when it loads."
|
|
277
|
+
return ""
|
|
278
|
+
)
|
|
279
|
+
if not written.is_empty():
|
|
280
|
+
return written
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
"ok": true,
|
|
284
|
+
"removed": true,
|
|
285
|
+
"sourceNodePath": from,
|
|
286
|
+
"signalName": signal_name,
|
|
287
|
+
"targetNodePath": to,
|
|
288
|
+
"methodName": method_name
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
func list_connections(args: Dictionary) -> Dictionary:
|
|
293
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
294
|
+
var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
|
|
295
|
+
var filter_path: String = str(args.get("nodePath", ""))
|
|
296
|
+
|
|
297
|
+
var scene: GdhSceneFile = GdhSceneFile.new()
|
|
298
|
+
var refused: Dictionary = scene.open(scene_path, false)
|
|
299
|
+
if not refused.is_empty():
|
|
300
|
+
return refused
|
|
301
|
+
|
|
302
|
+
var root: Node = scene.root
|
|
303
|
+
var filtered: Node = null
|
|
304
|
+
if not filter_path.is_empty():
|
|
305
|
+
filtered = GdhSceneFile.node_at(root, filter_path)
|
|
306
|
+
if filtered == null:
|
|
307
|
+
return scene.refuse(scene.not_found("Node not found: " + filter_path, filter_path))
|
|
308
|
+
var nodes: Array = []
|
|
309
|
+
_collect_nodes_recursive(root, ".", nodes)
|
|
310
|
+
|
|
311
|
+
# Only persistent connections, which are the ones a scene file holds: a Container connects
|
|
312
|
+
# signals of each child to itself as the child is added, and those were answered as if the file
|
|
313
|
+
# held them. Matched on either end, since the filter is for connections involving the node.
|
|
314
|
+
var connections: Array = []
|
|
315
|
+
for entry: Dictionary in nodes:
|
|
316
|
+
var path: String = str(entry["path"])
|
|
317
|
+
var node: Node = entry["node"]
|
|
318
|
+
for signal_info: Dictionary in node.get_signal_list():
|
|
319
|
+
var signal_name: String = str(signal_info.get("name", ""))
|
|
320
|
+
if signal_name.is_empty():
|
|
321
|
+
continue
|
|
322
|
+
for conn: Dictionary in node.get_signal_connection_list(signal_name):
|
|
323
|
+
var flags: int = GdhRead.as_int(conn.get("flags", 0))
|
|
324
|
+
if not flags & Object.CONNECT_PERSIST:
|
|
325
|
+
continue
|
|
326
|
+
var callable: Callable = conn.get("callable", Callable())
|
|
327
|
+
var target_obj: Object = callable.get_object()
|
|
328
|
+
if filtered != null and node != filtered and target_obj != filtered:
|
|
329
|
+
continue
|
|
330
|
+
var target_path: String = ""
|
|
331
|
+
var editable: bool = scene.foreign(node).is_empty()
|
|
332
|
+
if target_obj is Node:
|
|
333
|
+
target_path = str(root.get_path_to(target_obj as Node))
|
|
334
|
+
editable = editable and scene.foreign(target_obj as Node).is_empty()
|
|
335
|
+
var listed: Dictionary = {
|
|
336
|
+
"sourceNodePath": path,
|
|
337
|
+
"signalName": signal_name,
|
|
338
|
+
"targetNodePath": target_path,
|
|
339
|
+
"methodName": str(callable.get_method()),
|
|
340
|
+
"flags": flags,
|
|
341
|
+
"flagNames": GdhSceneFile.flag_names(flags),
|
|
342
|
+
# False for a connection an instanced scene makes inside itself, which only that
|
|
343
|
+
# scene can change.
|
|
344
|
+
"editable": editable,
|
|
345
|
+
"methodFound": target_obj != null and target_obj.has_method(callable.get_method()),
|
|
346
|
+
}
|
|
347
|
+
var misfit: String = _misfit(node, signal_name, callable, flags)
|
|
348
|
+
if not misfit.is_empty():
|
|
349
|
+
listed["misfit"] = misfit
|
|
350
|
+
connections.append(listed)
|
|
351
|
+
|
|
352
|
+
scene.close()
|
|
353
|
+
var answer: Dictionary = {"ok": true, "connections": connections}
|
|
354
|
+
if Engine.is_editor_hint():
|
|
355
|
+
answer["unsavedInEditor"] = GdhSceneFile.unsaved(scene_path)
|
|
356
|
+
return answer
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
func _collect_nodes_recursive(node: Node, path: String, out_nodes: Array) -> void:
|
|
360
|
+
out_nodes.append({"path": path, "node": node})
|
|
361
|
+
for child: Node in node.get_children():
|
|
362
|
+
var child_path: String = str(child.name) if path == "." else path + "/" + str(child.name)
|
|
363
|
+
_collect_nodes_recursive(child, child_path, out_nodes)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://8ktrjlekhgex
|
|
@@ -189,6 +189,21 @@ static func offered(said: String, word: String, names: Array) -> String:
|
|
|
189
189
|
return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
190
190
|
|
|
191
191
|
|
|
192
|
+
## [param said], the refusal for [param property], which [param target] does not declare, with the
|
|
193
|
+
## names it declares nearest it when any are, as a misspelt one is usually one of them. Unlike
|
|
194
|
+
## [method offered], nothing is listed when none is near, since an object declares a hundred.
|
|
195
|
+
static func near_property(said: String, target: Object, property: String) -> String:
|
|
196
|
+
var headings: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
197
|
+
var names: Array[String] = []
|
|
198
|
+
for declared: Dictionary in target.get_property_list():
|
|
199
|
+
if not as_int(declared.get("usage", 0)) & headings:
|
|
200
|
+
names.append(str(declared.get("name", "")))
|
|
201
|
+
var close: PackedStringArray = nearest(property, PackedStringArray(names))
|
|
202
|
+
if close.is_empty():
|
|
203
|
+
return said
|
|
204
|
+
return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
205
|
+
|
|
206
|
+
|
|
192
207
|
## The signals [param object] has that a caller can connect, its own class's first: the engine
|
|
193
208
|
## lists them from Object down, which puts a Button's pressed after thirty a Control has, and names
|
|
194
209
|
## starting with an underscore are the engine's own.
|
|
@@ -17,9 +17,6 @@ const GdhScreen = preload("runtime_screen.gd")
|
|
|
17
17
|
const GdhTargets = preload("runtime_targets.gd")
|
|
18
18
|
const GdhMouse = preload("runtime_mouse.gd")
|
|
19
19
|
|
|
20
|
-
## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
|
|
21
|
-
const TO_SMALL: int = 32
|
|
22
|
-
|
|
23
20
|
## Set, to the desktop's name, for a process gdharness started on a desktop of its own and for
|
|
24
21
|
## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
|
|
25
22
|
const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
|
|
@@ -161,10 +158,11 @@ func inject_action(params: Dictionary) -> Dictionary:
|
|
|
161
158
|
var frames: int = holding
|
|
162
159
|
|
|
163
160
|
_say_action(action, whole or held, strength)
|
|
161
|
+
var lasted: Variant = null
|
|
164
162
|
if whole:
|
|
165
163
|
# A frame between the halves at least, as a click has: a listener that acts on the press and
|
|
166
164
|
# a listener that acts on the release both get their own frame to do it in.
|
|
167
|
-
await
|
|
165
|
+
lasted = await GdhValues.hold(_host.get_tree(), frames)
|
|
168
166
|
_say_action(action, false, strength)
|
|
169
167
|
await _host.get_tree().process_frame
|
|
170
168
|
|
|
@@ -175,15 +173,10 @@ func inject_action(params: Dictionary) -> Dictionary:
|
|
|
175
173
|
"pressed": held and not whole,
|
|
176
174
|
"whole": whole,
|
|
177
175
|
"held_frames": GdhValues.held_answer(whole, frames),
|
|
176
|
+
"held_for": lasted,
|
|
178
177
|
}
|
|
179
178
|
|
|
180
179
|
|
|
181
|
-
## Waits out [param frames] process frames, which is how long a whole press is held down.
|
|
182
|
-
func _held(frames: int) -> void:
|
|
183
|
-
for _frame: int in frames:
|
|
184
|
-
await _host.get_tree().process_frame
|
|
185
|
-
|
|
186
|
-
|
|
187
180
|
func _say_action(action: String, down: bool, strength: float) -> void:
|
|
188
181
|
var event: InputEventAction = InputEventAction.new()
|
|
189
182
|
event.action = action
|
|
@@ -237,11 +230,12 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
237
230
|
# TextEdit insert `unicode` and never consult the keycode, so an injected key could press any
|
|
238
231
|
# action in the map and still type nothing into a search box. Godot's keycodes for printable
|
|
239
232
|
# keys are the code points themselves, which is what makes this a mapping and not a table.
|
|
240
|
-
event.unicode =
|
|
233
|
+
event.unicode = GdhValues.glyph_of(event.keycode, event.shift_pressed)
|
|
241
234
|
|
|
242
235
|
Input.parse_input_event(event)
|
|
236
|
+
var lasted: Variant = null
|
|
243
237
|
if whole:
|
|
244
|
-
await
|
|
238
|
+
lasted = await GdhValues.hold(_host.get_tree(), frames)
|
|
245
239
|
var up: InputEventKey = event.duplicate()
|
|
246
240
|
up.pressed = false
|
|
247
241
|
Input.parse_input_event(up)
|
|
@@ -251,6 +245,7 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
251
245
|
"type": "input_injected",
|
|
252
246
|
"input_type": "key",
|
|
253
247
|
"held_frames": GdhValues.held_answer(whole, frames),
|
|
248
|
+
"held_for": lasted,
|
|
254
249
|
"keycode": event.keycode,
|
|
255
250
|
"physical_keycode": event.physical_keycode,
|
|
256
251
|
"shift": event.shift_pressed,
|
|
@@ -262,19 +257,6 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
262
257
|
}
|
|
263
258
|
|
|
264
259
|
|
|
265
|
-
## The character a key produces, or nothing for a key that produces none.
|
|
266
|
-
##
|
|
267
|
-
## Godot's keycodes below [constant KEY_SPECIAL] are the Unicode code points of the keys that
|
|
268
|
-
## print something, so the mapping is the value itself. Letters are held as their capitals, which
|
|
269
|
-
## is the one place the shift a caller asked for changes the answer rather than the key.
|
|
270
|
-
func _glyph_of(keycode: Key, shifted: bool) -> int:
|
|
271
|
-
if keycode >= KEY_SPECIAL:
|
|
272
|
-
return 0
|
|
273
|
-
if keycode >= KEY_A and keycode <= KEY_Z and not shifted:
|
|
274
|
-
return keycode + TO_SMALL
|
|
275
|
-
return keycode
|
|
276
|
-
|
|
277
|
-
|
|
278
260
|
## Clicks a mouse button at a position, and lets go of it again unless asked to hold it.
|
|
279
261
|
##
|
|
280
262
|
## The whole click by default, for the reason [method inject_action] is the whole press, and
|
|
@@ -314,12 +296,13 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
314
296
|
var _moved: Vector2 = _arrive(position)
|
|
315
297
|
var supplied: bool = _follow_in_the_window(position)
|
|
316
298
|
Input.parse_input_event(_button(position, button, whole or held, double))
|
|
299
|
+
var lasted: Variant = null
|
|
317
300
|
if notch:
|
|
318
301
|
Input.parse_input_event(_button(position, button, false, false))
|
|
319
302
|
elif whole:
|
|
320
303
|
# A frame between the halves at least, as inject_action has, so a control that acts on the
|
|
321
304
|
# press and one that acts on the release each get a frame to do it in.
|
|
322
|
-
await
|
|
305
|
+
lasted = await GdhValues.hold(_host.get_tree(), frames)
|
|
323
306
|
Input.parse_input_event(_button(position, button, false, false))
|
|
324
307
|
await _host.get_tree().process_frame
|
|
325
308
|
|
|
@@ -329,6 +312,7 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
329
312
|
"type": "input_injected",
|
|
330
313
|
"input_type": "mouse_click",
|
|
331
314
|
"held_frames": GdhValues.held_answer(whole and not notch, frames),
|
|
315
|
+
"held_for": lasted,
|
|
332
316
|
"position": [position.x, position.y],
|
|
333
317
|
"button": button,
|
|
334
318
|
"pressed": held and not whole,
|
|
@@ -69,11 +69,11 @@ func inject_joypad_button(params: Dictionary) -> Dictionary:
|
|
|
69
69
|
var frames: int = holding
|
|
70
70
|
|
|
71
71
|
Input.parse_input_event(_button(button, device, whole or held))
|
|
72
|
+
var lasted: Variant = null
|
|
72
73
|
if whole:
|
|
73
74
|
# A frame between the halves at least, as a key has, so a game that acts on the press and one
|
|
74
75
|
# that acts on the release each get a frame to do it in.
|
|
75
|
-
|
|
76
|
-
await _host.get_tree().process_frame
|
|
76
|
+
lasted = await GdhValues.hold(_host.get_tree(), frames)
|
|
77
77
|
Input.parse_input_event(_button(button, device, false))
|
|
78
78
|
await _host.get_tree().process_frame
|
|
79
79
|
|
|
@@ -81,6 +81,7 @@ func inject_joypad_button(params: Dictionary) -> Dictionary:
|
|
|
81
81
|
"type": "input_injected",
|
|
82
82
|
"input_type": "joypad_button",
|
|
83
83
|
"held_frames": GdhValues.held_answer(whole, frames),
|
|
84
|
+
"held_for": lasted,
|
|
84
85
|
"button": button,
|
|
85
86
|
"button_name": _name_of(button, JOY_BUTTONS),
|
|
86
87
|
"device": device,
|
|
@@ -4,6 +4,7 @@ extends RefCounted
|
|
|
4
4
|
## run off the game the node keeps. Every op that reads, writes, calls or waits on a game's state
|
|
5
5
|
## walks the same way, so the walk and what it says when a step is not there live in one place.
|
|
6
6
|
|
|
7
|
+
const GdhRead = preload("reading.gd")
|
|
7
8
|
const GdhSerialisation = preload("serialisation.gd")
|
|
8
9
|
const GdhValues = preload("runtime_values.gd")
|
|
9
10
|
|
|
@@ -640,7 +641,7 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
640
641
|
"%s has no property %s; it has a method of that name, which a path calls as %s()"
|
|
641
642
|
% [called_as, named, named]
|
|
642
643
|
)
|
|
643
|
-
return "%s has no property %s" % [called_as, named]
|
|
644
|
+
return GdhRead.near_property("%s has no property %s" % [called_as, named], object, named)
|
|
644
645
|
|
|
645
646
|
|
|
646
647
|
## Why [param method] is not a call a path makes on [param sort] ("a list", "a packed list" or "a
|
|
@@ -126,6 +126,36 @@ static func held_answer(whole: bool, frames: int) -> Variant:
|
|
|
126
126
|
return null
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
## Waits out [param frames] process frames of [param tree], how long a whole press is held down,
|
|
130
|
+
## and answers how long that was by the clock and in physics ticks. A process frame is no set
|
|
131
|
+
## time: a headless game draws nothing and runs them as fast as it can, so thirty held a movement
|
|
132
|
+
## read in _physics_process for twelve ticks, a fifth of a second, where a caller counting sixty
|
|
133
|
+
## frames a second expected half of one.
|
|
134
|
+
static func hold(tree: SceneTree, frames: int) -> Dictionary:
|
|
135
|
+
var began_usec: int = Time.get_ticks_usec()
|
|
136
|
+
var began_tick: int = Engine.get_physics_frames()
|
|
137
|
+
for _frame: int in frames:
|
|
138
|
+
await tree.process_frame
|
|
139
|
+
return {
|
|
140
|
+
"seconds": snappedf((Time.get_ticks_usec() - began_usec) / 1000000.0, 0.001),
|
|
141
|
+
"physics_frames": Engine.get_physics_frames() - began_tick,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
## The character a key produces, or nothing for a key that produces none.
|
|
146
|
+
##
|
|
147
|
+
## Godot's keycodes below [constant KEY_SPECIAL] are the Unicode code points of the keys that
|
|
148
|
+
## print something, so the mapping is the value itself. Letters are held as their capitals, which
|
|
149
|
+
## is the one place the shift a caller asked for changes the answer rather than the key: the small
|
|
150
|
+
## letter is 32 above its capital.
|
|
151
|
+
static func glyph_of(keycode: Key, shifted: bool) -> int:
|
|
152
|
+
if keycode >= KEY_SPECIAL:
|
|
153
|
+
return 0
|
|
154
|
+
if keycode >= KEY_A and keycode <= KEY_Z and not shifted:
|
|
155
|
+
return keycode + 32
|
|
156
|
+
return keycode
|
|
157
|
+
|
|
158
|
+
|
|
129
159
|
## What [param path] names: a node under [param root], as [method node_at] reads it, or an engine
|
|
130
160
|
## singleton by its name. Answers the object under "object", and under "node" the node that objects
|
|
131
161
|
## named in a value are found from, which for a singleton is [param root].
|
|
@@ -121,6 +121,10 @@ const BUILDERS: Dictionary = {
|
|
|
121
121
|
"Dictionary": "_build_dictionary",
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
## The built-in resources being described, outermost first, so one holding itself somewhere inside
|
|
125
|
+
## is answered by its class rather than without end.
|
|
126
|
+
var _describing: Array[Resource] = []
|
|
127
|
+
|
|
124
128
|
|
|
125
129
|
# Converts a Godot value into something JSON can carry. A type with no entry in the table
|
|
126
130
|
# passes through as itself, which is what the JSON-native ones want.
|
|
@@ -665,9 +669,21 @@ func _serialize_object(value: Object) -> Variant:
|
|
|
665
669
|
return _describe_input_event(event)
|
|
666
670
|
if value is Resource:
|
|
667
671
|
var resource: Resource = value
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
672
|
+
# A file's built-in resources load with a path of the file's own and "::", which no other
|
|
673
|
+
# file can refer to.
|
|
674
|
+
if resource.resource_path.is_empty() or resource.resource_path.contains("::"):
|
|
675
|
+
return _describe_built_in(resource)
|
|
676
|
+
var filed: Dictionary = {
|
|
677
|
+
"path": resource.resource_path, "class": resource.get_class(), "_type": "Resource"
|
|
678
|
+
}
|
|
679
|
+
# The engine class of a resource with a script is the one its script extends, so a Stats
|
|
680
|
+
# file read as a Resource and nothing said it was a Stats.
|
|
681
|
+
var attached: Variant = resource.get_script()
|
|
682
|
+
if attached is Script:
|
|
683
|
+
var script: Script = attached
|
|
684
|
+
if not script.get_global_name().is_empty():
|
|
685
|
+
filed["scriptClass"] = str(script.get_global_name())
|
|
686
|
+
return filed
|
|
671
687
|
if value is Node:
|
|
672
688
|
var node: Node = value
|
|
673
689
|
if node.is_inside_tree():
|
|
@@ -675,6 +691,62 @@ func _serialize_object(value: Object) -> Variant:
|
|
|
675
691
|
return _describe_object(value)
|
|
676
692
|
|
|
677
693
|
|
|
694
|
+
## A resource built into the file it was read from, in the form a write builds one from: its class
|
|
695
|
+
## under _type beside every property it stores away from its default. As its class alone it said
|
|
696
|
+
## nothing of what it held, so a CollisionShape2D's radius could not be read, and it could not be
|
|
697
|
+
## written back.
|
|
698
|
+
func _describe_built_in(resource: Resource) -> Dictionary:
|
|
699
|
+
if _describing.has(resource):
|
|
700
|
+
return {"class": resource.get_class(), "_type": "Resource"}
|
|
701
|
+
_describing.append(resource)
|
|
702
|
+
var described: Dictionary = {"_type": resource.get_class()}
|
|
703
|
+
var attached: Variant = resource.get_script()
|
|
704
|
+
var script: Script = null
|
|
705
|
+
if attached is Script:
|
|
706
|
+
script = attached
|
|
707
|
+
described["script"] = serialize_value(script)
|
|
708
|
+
var fresh: Object = _fresh(script)
|
|
709
|
+
for property: Dictionary in resource.get_property_list():
|
|
710
|
+
var usage: int = property.get("usage", 0)
|
|
711
|
+
var name: String = str(property.get("name", ""))
|
|
712
|
+
if (
|
|
713
|
+
not (usage & PROPERTY_USAGE_STORAGE)
|
|
714
|
+
or name == "script"
|
|
715
|
+
or (name.begins_with("resource_") and name != "resource_name")
|
|
716
|
+
):
|
|
717
|
+
continue
|
|
718
|
+
var held: Variant = resource.get(name)
|
|
719
|
+
var default: Variant = ClassDB.class_get_property_default_value(resource.get_class(), name)
|
|
720
|
+
if fresh != null:
|
|
721
|
+
default = fresh.get(name)
|
|
722
|
+
elif script != null and usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
|
|
723
|
+
default = script.get_property_default_value(name)
|
|
724
|
+
if typeof(held) == typeof(default) and held == default:
|
|
725
|
+
continue
|
|
726
|
+
described[name] = serialize_value(held)
|
|
727
|
+
_describing.pop_back()
|
|
728
|
+
return described
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
## A new instance of [param script] to read defaults off, or null where none should be made. What
|
|
732
|
+
## the script's own code sets as it is made (a resource_name in _init, a list built by a
|
|
733
|
+
## constructor, which the engine reports no default for) is then a default rather than a value
|
|
734
|
+
## the file holds. None is made for a script whose _init needs arguments, nor in the editor for one
|
|
735
|
+
## that is not a tool: the editor never runs those, and can_instantiate says so.
|
|
736
|
+
static func _fresh(script: Script) -> Object:
|
|
737
|
+
if script == null or not script.can_instantiate():
|
|
738
|
+
return null
|
|
739
|
+
for method: Dictionary in script.get_script_method_list():
|
|
740
|
+
if str(method.get("name", "")) == "_init":
|
|
741
|
+
var arguments: Array = method.get("args", [])
|
|
742
|
+
var defaults: Array = method.get("default_args", [])
|
|
743
|
+
if arguments.size() > defaults.size():
|
|
744
|
+
return null
|
|
745
|
+
# Called by name: new() is declared on each kind of script rather than on Script.
|
|
746
|
+
var made: Variant = script.call("new")
|
|
747
|
+
return made if made is Object else null
|
|
748
|
+
|
|
749
|
+
|
|
678
750
|
## An input event, by what it is bound to: the engine's own words for it and every field it stores
|
|
679
751
|
## away from its default. As a bare resource it was its class and nothing else, so an input action
|
|
680
752
|
## read back from project.godot could not say whether it was bound to Space or to a joypad button.
|