gdharness 1.3.3 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/build/cli.js +532 -456
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +3 -2
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +181 -4
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
  7. package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
  8. package/build/godot/addons/gdharness_editor/property_values.gd +192 -231
  9. package/build/godot/addons/gdharness_editor/reading.gd +15 -0
  10. package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
  11. package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/scene_file.gd +97 -14
  13. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
  14. package/build/godot/addons/gdharness_editor/tool_executor.gd +9 -3
  15. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +38 -35
  16. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +20 -12
  17. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
  18. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
  19. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +68 -2
  21. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +94 -333
  22. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
  23. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_runtime/declared_values.gd +293 -0
  25. package/build/godot/addons/gdharness_runtime/declared_values.gd.uid +1 -0
  26. package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
  27. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -0
  28. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +29 -1
  29. package/build/godot/addons/gdharness_runtime/runtime_input.gd +26 -39
  30. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +18 -0
  31. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
  32. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
  33. package/build/godot/addons/gdharness_runtime/runtime_values.gd +62 -5
  34. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +48 -15
  35. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
  36. package/build/godot/operations/animation_reads.gd +3 -2
  37. package/build/godot/operations/classdb_queries.gd +112 -46
  38. package/build/godot/operations/declared_values.gd +181 -4
  39. package/build/godot/operations/dependencies.gd +14 -39
  40. package/build/godot/operations/editor_tools.gd +4 -3
  41. package/build/godot/operations/file_reads.gd +15 -3
  42. package/build/godot/operations/gdscript_analysis.gd +229 -75
  43. package/build/godot/operations/gdscript_authoring.gd +157 -36
  44. package/build/godot/operations/import_pipeline.gd +17 -6
  45. package/build/godot/operations/logger.gd +7 -0
  46. package/build/godot/operations/property_paths.gd +225 -0
  47. package/build/godot/operations/property_values.gd +192 -231
  48. package/build/godot/operations/reading.gd +15 -0
  49. package/build/godot/operations/same_values.gd +177 -0
  50. package/build/godot/operations/serialisation.gd +75 -3
  51. package/build/keeper.js +83 -69
  52. 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
@@ -0,0 +1,293 @@
1
+ @tool
2
+ extends RefCounted
3
+
4
+ ## What a property declares beyond its type: the values of its enum and the range it is exported
5
+ ## with, for taking a value by name and for saying when a value written lies outside them.
6
+
7
+ const GdhRead = preload("reading.gd")
8
+
9
+
10
+ ## The values an enum hint names, by name, from its hint string: "SWORD:0,SHIELD:1" as a script
11
+ ## declares one, or "Add,Subtract" as an engine property does, each name then taking the number
12
+ ## after the one before it.
13
+ static func enum_values(hint_string: String) -> Dictionary:
14
+ var values: Dictionary = {}
15
+ var next: int = 0
16
+ for entry: String in hint_string.split(",", false):
17
+ var name: String = entry.get_slice(":", 0).strip_edges()
18
+ var number: String = entry.get_slice(":", 1).strip_edges() if ":" in entry else ""
19
+ var value: int = number.to_int() if number.is_valid_int() else next
20
+ values[name] = value
21
+ next = value + 1
22
+ return values
23
+
24
+
25
+ ## The values of the enum [param declared] declares, by name, spelt as the script of [param target]
26
+ ## spells them when it declares that enum. The engine's hint capitalises each name, "SWORD" as
27
+ ## "Sword", and a refusal listing those named values the script never spells.
28
+ static func enum_of(target: Object, declared: Dictionary) -> Dictionary:
29
+ var hinted: Dictionary = enum_values(str(declared.get("hint_string", "")))
30
+ var attached: Variant = target.get_script()
31
+ if not attached is Script:
32
+ return hinted
33
+ var script: Script = attached
34
+ for constant: Variant in script.get_script_constant_map().values():
35
+ if not constant is Dictionary:
36
+ continue
37
+ var values: Dictionary = constant
38
+ if values.size() != hinted.size():
39
+ continue
40
+ var spelt: Dictionary = {}
41
+ for name: Variant in values:
42
+ if hinted.get(str(name).capitalize()) == values[name]:
43
+ spelt[str(name)] = values[name]
44
+ if spelt.size() == hinted.size():
45
+ return spelt
46
+ return hinted
47
+
48
+
49
+ ## The number [param word] names among [param choices], matched as written and then ignoring case,
50
+ ## or null when it names none.
51
+ static func enum_named(choices: Dictionary, word: String) -> Variant:
52
+ var wanted: String = word.strip_edges()
53
+ if choices.has(wanted):
54
+ return choices[wanted]
55
+ for name: String in choices:
56
+ if name.to_lower() == wanted.to_lower():
57
+ return choices[name]
58
+ return null
59
+
60
+
61
+ ## The values of the engine enum [param property] of [param target] holds, by the constants its
62
+ ## class declares them as: blend_mode's Add is BLEND_MODE_ADD in a script, and the hint gives only
63
+ ## the inspector's word. Empty for an enum a script declares.
64
+ ##
65
+ ## The enum is named by the setter's argument, as Node.ProcessMode, since the property list gives
66
+ ## an engine enum property no class of its own.
67
+ static func engine_constants(target: Object, property: String) -> Dictionary:
68
+ var values: Dictionary = {}
69
+ var engine_class: String = target.get_class()
70
+ var setter: String = str(ClassDB.class_get_property_setter(engine_class, property))
71
+ if setter.is_empty():
72
+ return values
73
+ var named: String = ""
74
+ for method: Dictionary in ClassDB.class_get_method_list(engine_class):
75
+ var arguments: Array = method.get("args", [])
76
+ if str(method.get("name", "")) == setter and not arguments.is_empty():
77
+ var value_argument: Dictionary = arguments[-1]
78
+ named = str(value_argument.get("class_name", ""))
79
+ break
80
+ var holder: String = named.get_slice(".", 0)
81
+ if named.count(".") != 1 or not ClassDB.class_exists(holder):
82
+ return values
83
+ for constant: String in ClassDB.class_get_enum_constants(holder, named.get_slice(".", 1)):
84
+ values[constant] = ClassDB.class_get_integer_constant(holder, constant)
85
+ return values
86
+
87
+
88
+ ## [param word] as the number of the value the enum property [param declared] of [param target]
89
+ ## names by it, as [true, number], or [false, why] naming the values, with [param holder] naming the
90
+ ## target in the refusal. [false, ""] for a word that is a number, which converts as one.
91
+ ##
92
+ ## An enum is written by the name of a value as well as its number, since the name is what the script
93
+ ## and the inspector show and the number is only where the value happens to sit. Tried as the script
94
+ ## spells it, then as the hint does, then as the engine's constant.
95
+ static func enum_by_name(target: Object, declared: Dictionary, word: String, holder: String) -> Array:
96
+ var property: String = str(declared.get("name", ""))
97
+ var choices: Dictionary = enum_of(target, declared)
98
+ var chosen: Variant = enum_named(choices, word)
99
+ if chosen == null:
100
+ chosen = enum_named(enum_values(str(declared.get("hint_string", ""))), word)
101
+ var constants: Dictionary = {}
102
+ if chosen == null:
103
+ constants = engine_constants(target, property)
104
+ chosen = enum_named(constants, word)
105
+ if chosen != null:
106
+ return [true, chosen]
107
+ if word.strip_edges().is_valid_int():
108
+ return [false, ""]
109
+ return [false, enum_refusal(holder, property, word, choices, constants)]
110
+
111
+
112
+ static func enum_refusal(
113
+ holder: String, property: String, word: String, choices: Dictionary, constants: Dictionary = {}
114
+ ) -> String:
115
+ var ways: String = "by name or by number"
116
+ if not constants.is_empty():
117
+ ways = "by name, by number or by the engine's constant, such as %s" % str(constants.keys()[0])
118
+ var said: String = (
119
+ "%s.%s is an enum, and %s is none of its values: send %s %s"
120
+ % [holder, property, word, _listed(choices), ways]
121
+ )
122
+ var named: PackedStringArray = PackedStringArray(choices.keys())
123
+ named.append_array(PackedStringArray(constants.keys()))
124
+ var close: PackedStringArray = GdhRead.nearest(word, named)
125
+ if close.is_empty():
126
+ return said + "."
127
+ return said + ". The nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
128
+
129
+
130
+ ## Why [param value], shown as [param shown], lies outside what [param declared] allows, an export
131
+ ## range or an enum's values, or "" when it does not. A range marked or_greater or or_less is open
132
+ ## on that side.
133
+ static func outside(target: Object, declared: Dictionary, value: Variant, shown: String) -> String:
134
+ var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
135
+ var hint_string: String = str(declared.get("hint_string", ""))
136
+ var property: String = str(declared.get("name", ""))
137
+ if hint == PROPERTY_HINT_ENUM and (typeof(value) == TYPE_STRING or typeof(value) == TYPE_STRING_NAME):
138
+ var names: PackedStringArray = PackedStringArray(enum_values(hint_string).keys())
139
+ if names.is_empty() or names.has(str(value)):
140
+ return ""
141
+ return (
142
+ (
143
+ "%s is none of the names %s lists: %s, which the inspector holds it to; the engine "
144
+ + "stores it as given."
145
+ )
146
+ % [shown, property, ", ".join(names)]
147
+ )
148
+ if typeof(value) != TYPE_INT and typeof(value) != TYPE_FLOAT:
149
+ return ""
150
+ var number: float = GdhRead.as_float(value)
151
+ if hint == PROPERTY_HINT_ENUM and typeof(value) == TYPE_INT:
152
+ var choices: Dictionary = enum_of(target, declared)
153
+ if choices.is_empty() or choices.values().has(value):
154
+ return ""
155
+ return "%d is none of the values %s declares: %s." % [value, property, _listed(choices)]
156
+ if hint != PROPERTY_HINT_RANGE:
157
+ return ""
158
+ var parts: PackedStringArray = []
159
+ for part: String in hint_string.split(","):
160
+ var _added: bool = parts.append(part.strip_edges())
161
+ if parts.size() < 2 or not parts[0].is_valid_float() or not parts[1].is_valid_float():
162
+ return ""
163
+ var low: float = parts[0].to_float()
164
+ var high: float = parts[1].to_float()
165
+ if (number <= high or parts.has("or_greater")) and (number >= low or parts.has("or_less")):
166
+ return ""
167
+ return (
168
+ (
169
+ "%s is outside the range %s declares, %s to %s, which the inspector holds it to; the "
170
+ + "engine stores it as given."
171
+ )
172
+ % [shown, property, _bound(low), _bound(high)]
173
+ )
174
+
175
+
176
+ static func _listed(choices: Dictionary) -> String:
177
+ var listed: PackedStringArray = []
178
+ for name: String in choices:
179
+ var _added: bool = listed.append("%s (%d)" % [name, choices[name]])
180
+ return ", ".join(listed)
181
+
182
+
183
+ ## A range's end as the script wrote it: the engine's hint gives @export_range(0, 10) as 0.0 and 10.0.
184
+ static func _bound(end: float) -> String:
185
+ return str(int(end)) if end == floorf(end) and absf(end) < 1e15 else str(end)
186
+
187
+
188
+ ## Puts back on [param built] what its script set as it was attached and was not among
189
+ ## [param wanted], to the defaults it declares, outside the editor. Attaching runs the script's _init
190
+ ## there, which the editor never runs for a script that is not a tool, so a resource_name _init chose
191
+ ## was saved as if asked for. A property whose default the engine cannot report is left as made.
192
+ static func unmade(built: Resource, wanted: Dictionary) -> void:
193
+ var attached: Variant = built.get_script()
194
+ if Engine.is_editor_hint() or not attached is Script:
195
+ return
196
+ var script: Script = attached
197
+ var asked: Dictionary = {}
198
+ for key: Variant in wanted:
199
+ asked[str(key).get_slice(":", 0)] = true
200
+ for entry: Dictionary in built.get_property_list():
201
+ var name: String = str(entry["name"])
202
+ var usage: int = GdhRead.as_int(entry["usage"])
203
+ if usage & PROPERTY_USAGE_STORAGE == 0 or name == "script" or asked.has(name):
204
+ continue
205
+ var declared: Variant = ClassDB.class_get_property_default_value(built.get_class(), name)
206
+ if usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
207
+ declared = script.get_property_default_value(name)
208
+ var held: Variant = built.get(name)
209
+ if declared != null and not (typeof(held) == typeof(declared) and held == declared):
210
+ built.set(name, declared)
211
+
212
+
213
+ ## The element types a typed Array or Dictionary property declares: [element] for an array,
214
+ ## [key, value] for a dictionary, [] when nothing is declared.
215
+ ##
216
+ ## Read from the declaration because the value cannot say: a script's exported `Array[int]` sits
217
+ ## behind a placeholder in the editor, and measured on 4.7.2 that placeholder stores [1.5, 2.0]
218
+ ## as given, which the game then fails to load into the typed array.
219
+ static func elements_of(declared: Dictionary) -> Array:
220
+ var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
221
+ var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
222
+ var wanted: String = str(declared.get("hint_string", ""))
223
+ if wanted.is_empty():
224
+ return []
225
+ if hint == PROPERTY_HINT_TYPE_STRING:
226
+ var parts: PackedStringArray = wanted.split(";")
227
+ if type == TYPE_ARRAY and parts.size() == 1:
228
+ return [_type_in_hint(parts[0])]
229
+ if type == TYPE_DICTIONARY and parts.size() == 2:
230
+ return [_type_in_hint(parts[0]), _type_in_hint(parts[1])]
231
+ return []
232
+ if hint == PROPERTY_HINT_ARRAY_TYPE and type == TYPE_ARRAY:
233
+ return [_type_named(wanted)]
234
+ if hint == PROPERTY_HINT_DICTIONARY_TYPE and type == TYPE_DICTIONARY:
235
+ var parts: PackedStringArray = wanted.split(";")
236
+ if parts.size() == 2:
237
+ return [_type_named(parts[0]), _type_named(parts[1])]
238
+ return []
239
+
240
+
241
+ ## The type at the front of one entry of a type-string hint, "2:" or "24/17:Texture2D".
242
+ static func _type_in_hint(entry: String) -> int:
243
+ var number: String = entry.get_slice(":", 0).get_slice("/", 0)
244
+ return number.to_int() if number.is_valid_int() else TYPE_NIL
245
+
246
+
247
+ ## The Variant type [param name] names, or TYPE_OBJECT for a class.
248
+ static func _type_named(name: String) -> int:
249
+ var named: String = name.get_slice(":", 0).strip_edges()
250
+ for type: int in TYPE_MAX:
251
+ if type_string(type) == named:
252
+ return type
253
+ return TYPE_OBJECT if not named.is_empty() else TYPE_NIL
254
+
255
+
256
+ ## The class the objects of a typed list or map hold, and whether they are nodes, as
257
+ ## [class, nodes], read from entry [param at] of [param declared]'s hint: "24/17:Item" in a type
258
+ ## string, "Item" in an array or dictionary type.
259
+ static func object_kind(declared: Dictionary, at: int) -> Array:
260
+ var entry: String = str(declared.get("hint_string", "")).get_slice(";", at)
261
+ if GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE)) != PROPERTY_HINT_TYPE_STRING:
262
+ return [entry.strip_edges(), false]
263
+ var nodes: bool = entry.get_slice(":", 0).get_slice("/", 1) == str(PROPERTY_HINT_NODE_TYPE)
264
+ return [entry.get_slice(":", 1), nodes]
265
+
266
+
267
+ ## The base class and script that type a list or map holding [param wanted], as [class, script],
268
+ ## or [] when nothing in the project or the engine is called that.
269
+ static func object_typing(wanted: String) -> Array:
270
+ if ClassDB.class_exists(wanted):
271
+ return [StringName(wanted), null]
272
+ var script: Script = script_class(wanted)
273
+ return [] if script == null else [script.get_instance_base_type(), script]
274
+
275
+
276
+ ## The script declaring the class_name [param name], or null.
277
+ static func script_class(name: String) -> Script:
278
+ for entry: Dictionary in ProjectSettings.get_global_class_list():
279
+ if str(entry.get("class", "")) == name:
280
+ return load(str(entry.get("path", ""))) as Script
281
+ return null
282
+
283
+
284
+ ## Whether [param value] is the class [param name] names, an engine class or a script's class_name.
285
+ static func is_a(value: Object, name: String) -> bool:
286
+ if value.is_class(name):
287
+ return true
288
+ var script: Script = value.get_script()
289
+ while script != null:
290
+ if str(script.get_global_name()) == name:
291
+ return true
292
+ script = script.get_base_script()
293
+ return false
@@ -0,0 +1 @@
1
+ uid://c2r5ain6am9gy
@@ -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.