gdharness 1.3.40 → 1.3.42

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 (32) hide show
  1. package/build/cli.js +343 -142
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +31 -4
  3. package/build/godot/addons/gdharness_editor/file_reads.gd +36 -1
  4. package/build/godot/addons/gdharness_editor/file_walk.gd +33 -1
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +29 -0
  6. package/build/godot/addons/gdharness_editor/property_values.gd +6 -9
  7. package/build/godot/addons/gdharness_editor/scene_file.gd +27 -0
  8. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +5 -2
  9. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +6 -2
  10. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +15 -0
  11. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +7 -2
  12. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +59 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd.uid +1 -0
  14. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +75 -11
  15. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +77 -22
  16. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -1
  17. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +4 -3
  18. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +211 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_touch.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +15 -8
  21. package/build/godot/operations/animation_reads.gd +31 -4
  22. package/build/godot/operations/dependencies.gd +5 -2
  23. package/build/godot/operations/file_reads.gd +36 -1
  24. package/build/godot/operations/file_walk.gd +33 -1
  25. package/build/godot/operations/gdscript_analysis.gd +19 -1
  26. package/build/godot/operations/gdscript_authoring.gd +65 -4
  27. package/build/godot/operations/import_pipeline.gd +1 -1
  28. package/build/godot/operations/input_actions.gd +1 -0
  29. package/build/godot/operations/plugins.gd +39 -1
  30. package/build/godot/operations/project_config.gd +104 -1
  31. package/build/godot/operations/property_values.gd +6 -9
  32. package/package.json +1 -1
@@ -10,6 +10,9 @@ const GdhSerialisation = preload("../serialisation.gd")
10
10
 
11
11
  ## What every emission meets when a connection cannot call its method.
12
12
  const FAILS: String = ", so every emission fails to call it."
13
+ ## The engine's CONNECT_INHERITED, which GDScript is not given: on a connection an instanced or
14
+ ## inherited scene makes, as a scene holding one loads it.
15
+ const CONNECT_INHERITED: int = 32
13
16
  ## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
14
17
  ## the persistence every connection in a saved scene carries.
15
18
  const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
@@ -18,9 +21,7 @@ const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
18
21
  Object.CONNECT_ONE_SHOT: "one_shot",
19
22
  Object.CONNECT_REFERENCE_COUNTED: "reference_counted",
20
23
  Object.CONNECT_APPEND_SOURCE_OBJECT: "append_source_object",
21
- # The engine's CONNECT_INHERITED, which GDScript is not given: on a connection an instanced scene
22
- # makes inside itself, as a scene holding one loads it.
23
- 32: "inherited",
24
+ CONNECT_INHERITED: "inherited",
24
25
  }
25
26
  ## The flags a caller may give: the engine's own CONNECT_INHERITED marks a connection an instanced
26
27
  ## scene makes, and is never one a scene makes for itself.
@@ -295,7 +296,9 @@ static func _misfit(source: Node, signal_name: String, callable: Callable, flags
295
296
  break
296
297
  if flags & Object.CONNECT_APPEND_SOURCE_OBJECT:
297
298
  passed.append({"name": "source", "type": TYPE_OBJECT, "class_name": source.get_class()})
298
- passed = passed.slice(0, maxi(passed.size() - callable.get_unbound_arguments_count(), 0))
299
+ var emitted: int = passed.size()
300
+ var unbound: int = callable.get_unbound_arguments_count()
301
+ passed = passed.slice(0, maxi(emitted - unbound, 0))
299
302
  var from_signal: int = passed.size()
300
303
  for bound: Variant in callable.get_bound_arguments():
301
304
  var bound_class: String = ""
@@ -303,43 +306,58 @@ static func _misfit(source: Node, signal_name: String, callable: Callable, flags
303
306
  var object: Object = bound
304
307
  bound_class = object.get_class()
305
308
  passed.append({"name": "bound", "type": typeof(bound), "class_name": bound_class})
309
+ var binds: int = passed.size() - from_signal
310
+
311
+ # What the method is called with, said in the parts that make it: an answer counting the bound
312
+ # arguments among the signal's said the signal passed more than it declares.
313
+ var called: String = "%s passes %s" % [signal_name, _count(emitted)]
314
+ if unbound > 0 or binds > 0:
315
+ var changed: Array[String] = []
316
+ if unbound > 0:
317
+ changed.append("unbinds %d" % unbound)
318
+ if binds > 0:
319
+ changed.append("binds %d" % binds)
320
+ called += (
321
+ " and the connection %s, so %s is called with %s, and"
322
+ % [" and ".join(PackedStringArray(changed)), method, _count(passed.size())]
323
+ )
324
+ else:
325
+ called += ", and " + method
306
326
 
307
327
  var parameters: Array = taken.get("args", [])
308
328
  var defaults: Array = taken.get("default_args", [])
309
329
  var needed: int = parameters.size() - defaults.size()
310
330
  if passed.size() < needed:
311
- return "%s passes %s, and %s needs %d%s" % [signal_name, _count(passed.size()), method, needed, FAILS]
331
+ return "%s needs %d%s" % [called, needed, FAILS]
312
332
  if passed.size() > parameters.size():
313
333
  var most: String = "none" if parameters.is_empty() else "at most %d" % parameters.size()
314
334
  # Said with the misfit, since unbinds is what a connection to a method wanting less is for, and
315
- # only when the extra arguments are the signal's: a bound one is not dropped by unbinding.
335
+ # only when the extra arguments are the signal's: a scene's connection unbinds the arguments
336
+ # the signal passes before it adds the bound ones, so a bound one is never dropped.
316
337
  var extra: int = passed.size() - parameters.size()
317
338
  var remedy: String = (
318
339
  (
319
- " Connected with unbinds %d, the last %s the signal passes %s left out."
320
- % [
321
- extra + callable.get_unbound_arguments_count(),
322
- "argument" if extra == 1 else "%d arguments" % extra,
323
- "is" if extra == 1 else "are",
324
- ]
340
+ " Connect it with unbinds %d to leave out the last %s the signal passes."
341
+ % [extra + unbound, "argument" if extra == 1 else "%d arguments" % extra]
325
342
  )
326
343
  if extra <= from_signal
327
344
  else ""
328
345
  )
329
- return (
330
- "%s passes %s, and %s takes %s%s%s"
331
- % [signal_name, _count(passed.size()), method, most, FAILS, remedy]
332
- )
346
+ return "%s takes %s%s%s" % [called, most, FAILS, remedy]
333
347
  for at: int in passed.size():
334
348
  var given: Dictionary = passed[at]
335
349
  var parameter: Dictionary = parameters[at]
336
350
  if not _unrelated(given, parameter):
337
351
  continue
352
+ var whose: String = (
353
+ "%s passes argument %d" % [signal_name, at + 1]
354
+ if at < from_signal
355
+ else "the connection binds argument %d" % (at + 1)
356
+ )
338
357
  return (
339
- "%s passes argument %d as %s, and %s takes it as %s: %s%s"
358
+ "%s as %s, and %s takes it as %s: %s%s"
340
359
  % [
341
- signal_name,
342
- at + 1,
360
+ whose,
343
361
  _type_named(given),
344
362
  method,
345
363
  str(parameter.get("name", "")),
@@ -380,6 +398,25 @@ static func _type_named(declared: Dictionary) -> String:
380
398
  return type_string(type)
381
399
 
382
400
 
401
+ ## The scene that makes the connection [param source] holds with [param flags], as a path, or "" for
402
+ ## one this scene makes. The engine marks such a connection inherited as the scene holding it loads:
403
+ ## from an instance, the scene instanced, and from the root of an inherited scene, the one it
404
+ ## inherits.
405
+ static func _made_in(scene: GdhSceneFile, source: Node, flags: int) -> String:
406
+ if not flags & CONNECT_INHERITED:
407
+ return ""
408
+ if source != scene.root and not source.scene_file_path.is_empty():
409
+ return source.scene_file_path
410
+ var holder: Node = source if source == scene.root else source.owner
411
+ while holder != null and holder != scene.root and holder.scene_file_path.is_empty():
412
+ holder = holder.owner
413
+ if holder != null and holder != scene.root:
414
+ return holder.scene_file_path
415
+ var packed: PackedScene = load(scene.path) as PackedScene
416
+ var base: SceneState = packed.get_state().get_base_scene_state() if packed != null else null
417
+ return base.get_path() if base != null else "a scene this one inherits"
418
+
419
+
383
420
  static func _count(arguments: int) -> String:
384
421
  if arguments == 0:
385
422
  return "no arguments"
@@ -428,6 +465,21 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
428
465
  % [scene_path, said_from, signal_name, said_to, method_name]
429
466
  )
430
467
  )
468
+ var made_in: String = _made_in(scene, source, GdhRead.as_int(held["flags"]))
469
+ if not made_in.is_empty():
470
+ var named_from: String = str(source.name) if source == scene.root else from
471
+ var named_to: String = str(target.name) if target == scene.root else to
472
+ var made_by: String = (
473
+ "The connection from %s.%s to %s.%s is made by %s"
474
+ % [named_from, signal_name, named_to, method_name, made_in]
475
+ )
476
+ return scene.refuse(
477
+ (
478
+ made_by
479
+ + ", and this scene saves nothing about it: disconnect it there, which changes every"
480
+ + " scene using it."
481
+ )
482
+ )
431
483
  var callable: Callable = held["callable"]
432
484
  source.disconnect(signal_name, callable)
433
485
 
@@ -498,6 +550,9 @@ func list_connections(args: Dictionary) -> Dictionary:
498
550
  if target_obj is Node:
499
551
  target_path = str(root.get_path_to(target_obj as Node))
500
552
  editable = editable and scene.foreign(target_obj as Node).is_empty()
553
+ # A connection an instanced scene makes on its own root sits on a node this scene owns,
554
+ # so the ends alone read it as one this scene could change.
555
+ var made_in: String = _made_in(scene, node, flags)
501
556
  var listed: Dictionary = {
502
557
  "sourceNodePath": path,
503
558
  "signalName": signal_name,
@@ -505,11 +560,11 @@ func list_connections(args: Dictionary) -> Dictionary:
505
560
  "methodName": str(callable.get_method()),
506
561
  "flags": flags,
507
562
  "flagNames": flag_names(flags),
508
- # False for a connection an instanced scene makes inside itself, which only that
509
- # scene can change.
510
- "editable": editable,
563
+ "editable": editable and made_in.is_empty(),
511
564
  "methodFound": target_obj != null and target_obj.has_method(callable.get_method()),
512
565
  }
566
+ if not made_in.is_empty():
567
+ listed["madeIn"] = made_in
513
568
  var bound: Dictionary = {
514
569
  "binds": callable.get_bound_arguments(),
515
570
  "unbinds": callable.get_unbound_arguments_count(),
@@ -26,6 +26,7 @@ const CHANGES: String = "runtime_changes.gd"
26
26
  const INPUT: String = "runtime_input.gd"
27
27
  const TYPING: String = "runtime_typing.gd"
28
28
  const PAD: String = "runtime_pad.gd"
29
+ const TOUCH: String = "runtime_touch.gd"
29
30
  const CAPTURE: String = "runtime_capture.gd"
30
31
  const WAITS: String = "runtime_waits.gd"
31
32
  const ACCESSIBILITY: String = "runtime_accessibility.gd"
@@ -52,6 +53,8 @@ const MODULE_COMMANDS: Dictionary = {
52
53
  "inject_mouse_drag": [INPUT, "inject_mouse_drag"],
53
54
  "inject_joypad_button": [PAD, "inject_joypad_button"],
54
55
  "inject_joypad_motion": [PAD, "inject_joypad_motion"],
56
+ "inject_touch": [TOUCH, "inject_touch"],
57
+ "inject_touch_drag": [TOUCH, "inject_touch_drag"],
55
58
  "click": [INPUT, "click"],
56
59
  "choose": [TYPING, "choose"],
57
60
  "wait_frames": [WAITS, "wait_frames"],
@@ -218,7 +221,7 @@ func _module(file: String) -> Object:
218
221
  match file:
219
222
  QUERIES:
220
223
  made = script.new(self, values, _frames)
221
- TYPING, PAD, ACCESSIBILITY:
224
+ TYPING, PAD, TOUCH, ACCESSIBILITY:
222
225
  made = script.new(self)
223
226
  _:
224
227
  made = script.new(self, values)
@@ -96,11 +96,12 @@ static func where(aim: Dictionary, viewport: Viewport, points: Array[Vector2]) -
96
96
 
97
97
 
98
98
  ## The points of a drag: x, y, then each of via, then toX, toY. Answers the String saying what is
99
- ## wrong instead, since a point left out would otherwise read as the window's corner.
100
- static func stroke_of(params: Dictionary) -> Variant:
99
+ ## wrong instead, since a point left out would otherwise read as the window's corner. [param op] is
100
+ ## the drag the refusal names.
101
+ static func stroke_of(params: Dictionary, op: String = "mouse_drag") -> Variant:
101
102
  for key: String in ["x", "y", "toX", "toY"]:
102
103
  if not params.has(key):
103
- return "mouse_drag needs x, y, toX and toY: %s is missing." % key
104
+ return "%s needs x, y, toX and toY: %s is missing." % [op, key]
104
105
  var points: Array[Vector2] = [Vector2(GdhRead.as_float(params["x"]), GdhRead.as_float(params["y"]))]
105
106
  var via: Variant = params.get("via", [])
106
107
  if not via is Array:
@@ -0,0 +1,211 @@
1
+ extends RefCounted
2
+
3
+ ## What reaches the running game the way a touchscreen reaches it: a finger pressed, held, lifted
4
+ ## and dragged, as InputEventScreenTouch and InputEventScreenDrag.
5
+ ##
6
+ ## The events themselves rather than a mouse standing in for them: input code tells a finger from a
7
+ ## mouse by the event's class, or by the mouse events the engine makes from a touch, which carry
8
+ ## InputEvent.DEVICE_ID_EMULATION. A mouse_click carries the mouse's own device, so a game switching
9
+ ## its glyphs or focus on a touch never saw one. Sent through Input for the game's window, so the
10
+ ## engine makes those mouse events itself where input_devices/pointing/emulate_mouse_from_touch is on.
11
+
12
+ const GdhRead = preload("reading.gd")
13
+ const GdhValues = preload("runtime_values.gd")
14
+ const GdhMouse = preload("runtime_mouse.gd")
15
+
16
+ const EMULATE_SETTING: String = "input_devices/pointing/emulate_mouse_from_touch"
17
+
18
+ var _host: Node
19
+ # The finger the engine makes mouse events from, as Input keeps it for the touches sent here: the
20
+ # first one pressed while none is, until it lifts. -1 for none.
21
+ var _emulating: int = -1
22
+
23
+
24
+ func _init(host: Node) -> void:
25
+ _host = host
26
+
27
+
28
+ ## Presses a finger at a position, and lifts it a frame later unless asked to hold it, as
29
+ ## inject_mouse_click does a button: a finger left down is a touch that never ends.
30
+ func inject_touch(params: Dictionary) -> Dictionary:
31
+ var root: Window = _host.get_tree().root
32
+ var aimed: Variant = GdhMouse.aim_of(params, root)
33
+ if aimed is String:
34
+ return {"type": "error", "message": aimed}
35
+ var aim: Dictionary = aimed
36
+ var point: Variant = GdhMouse.read_point(params, "x", "y")
37
+ if point is String:
38
+ return {"type": "error", "message": point}
39
+ var finger: Variant = _finger_of(params)
40
+ if finger is String:
41
+ return {"type": "error", "message": finger}
42
+ var index: int = finger
43
+ var holding: Variant = GdhValues.held_for(params)
44
+ if holding is String:
45
+ return {"type": "error", "message": holding}
46
+ var frames: int = holding
47
+ var local: Vector2 = point
48
+ var placed: Dictionary = GdhMouse.carried(aim, local, root)
49
+ var viewport: Viewport = placed["viewport"]
50
+ var position: Vector2 = placed["position"]
51
+ var held: bool = GdhRead.as_bool(params.get("pressed", true), true)
52
+ var whole: bool = not params.has("pressed")
53
+
54
+ var at: Array[Vector2] = [position]
55
+ var where: Dictionary = GdhMouse.where(aim, viewport, at)
56
+ var emulates: bool = _send(viewport, _touch(position, index, whole or held))
57
+ var lasted: Variant = null
58
+ if whole:
59
+ lasted = await GdhValues.hold(_host.get_tree(), frames)
60
+ var _lifted: bool = _send(viewport, _touch(position, index, false))
61
+ await _host.get_tree().process_frame
62
+
63
+ var answer: Dictionary = {
64
+ "type": "input_injected",
65
+ "input_type": "touch",
66
+ "index": index,
67
+ "position": [position.x, position.y],
68
+ "held_frames": GdhValues.held_answer(whole, frames),
69
+ "held_for": lasted,
70
+ "pressed": held and not whole,
71
+ "whole": whole,
72
+ "emulates_mouse": emulates,
73
+ }
74
+ answer.merge(where)
75
+ return answer
76
+
77
+
78
+ ## Drags a finger from one point to another through any between: the touch, one drag each process
79
+ ## frame along the stroke, and the lift where the last drag left it, a frame after it. Each drag goes
80
+ ## in a frame of its own because the engine merges the drags it is given within one frame, as it
81
+ ## merges a mouse's motions.
82
+ func inject_touch_drag(params: Dictionary) -> Dictionary:
83
+ var root: Window = _host.get_tree().root
84
+ var aimed: Variant = GdhMouse.aim_of(params, root)
85
+ if aimed is String:
86
+ return {"type": "error", "message": aimed}
87
+ var aim: Dictionary = aimed
88
+ var stroke: Variant = GdhMouse.stroke_of(params, "touch_drag")
89
+ if stroke is String:
90
+ return {"type": "error", "message": stroke}
91
+ var finger: Variant = _finger_of(params)
92
+ if finger is String:
93
+ return {"type": "error", "message": finger}
94
+ var index: int = finger
95
+ var frames: int = GdhRead.as_int(params.get("frames", 0), 0)
96
+ if frames < 1 or frames > GdhValues.MOST_HELD:
97
+ return {
98
+ "type": "error", "message": "frames is %d, and it takes 1 to %d." % [frames, GdhValues.MOST_HELD]
99
+ }
100
+ # Carried once, before the touch: a finger drags across the screen, not along a node that moves.
101
+ var named: Array[Vector2] = stroke
102
+ var points: Array[Vector2] = []
103
+ var viewport: Viewport = root
104
+ for local: Vector2 in named:
105
+ var placed: Dictionary = GdhMouse.carried(aim, local, root)
106
+ viewport = placed["viewport"]
107
+ points.append(placed["position"])
108
+
109
+ var lengths: Array[float] = GdhMouse.lengths_of(points)
110
+ var start: Vector2 = points[0]
111
+ var end: Vector2 = points[points.size() - 1]
112
+ var where: Dictionary = GdhMouse.where(aim, viewport, points)
113
+ var emulates: bool = _send(viewport, _touch(start, index, true))
114
+ var last: Vector2 = start
115
+ var travelled: Vector2 = Vector2.ZERO
116
+ for step: int in range(1, frames + 1):
117
+ await _host.get_tree().process_frame
118
+ if not is_instance_valid(viewport):
119
+ return {
120
+ "type": "error",
121
+ "message":
122
+ (
123
+ (
124
+ "%s was freed after the touch and %d of %d drags, so the rest of the stroke"
125
+ + " and the lift went nowhere"
126
+ )
127
+ % [where.get("window", "The window"), step - 1, frames]
128
+ )
129
+ }
130
+ var at: Vector2 = GdhMouse.along(points, lengths, float(step) / frames)
131
+ var relative: Vector2 = at - last
132
+ last = at
133
+ travelled += relative
134
+ var _dragged: bool = _send(viewport, _drag(at, relative, index))
135
+ await _host.get_tree().process_frame
136
+ var _lifted: bool = _send(viewport, _touch(end, index, false))
137
+ await _host.get_tree().process_frame
138
+
139
+ var answer: Dictionary = {
140
+ "type": "input_injected",
141
+ "input_type": "touch_drag",
142
+ "index": index,
143
+ "from": [start.x, start.y],
144
+ "to": [end.x, end.y],
145
+ "motions": frames,
146
+ "relative_total": [travelled.x, travelled.y],
147
+ "emulates_mouse": emulates,
148
+ }
149
+ answer.merge(where)
150
+ return answer
151
+
152
+
153
+ ## The finger [param params].index names, 0 when none is given, or the String refusing it.
154
+ static func _finger_of(params: Dictionary) -> Variant:
155
+ if not params.has("index"):
156
+ return 0
157
+ var given: Variant = params["index"]
158
+ var number: float = GdhRead.as_float(given)
159
+ if not (given is int or given is float) or number != roundf(number):
160
+ return "index is the finger, a whole number from 0, and was %s." % JSON.stringify(given)
161
+ var index: int = roundi(number)
162
+ if index < 0:
163
+ return "index is the finger, a whole number from 0, and was %d." % index
164
+ return index
165
+
166
+
167
+ ## Sends [param event] into [param viewport]: through Input for the game's window, and pushed into
168
+ ## any other, since Input reaches only the game's window. Answers whether the engine makes a mouse
169
+ ## event from it, which it does in Input, with the setting on, for one finger at a time.
170
+ func _send(viewport: Viewport, event: InputEvent) -> bool:
171
+ if viewport != _host.get_tree().root:
172
+ if is_instance_valid(viewport):
173
+ viewport.push_input(event)
174
+ return false
175
+ Input.parse_input_event(event)
176
+ if not GdhRead.as_bool(ProjectSettings.get_setting(EMULATE_SETTING, true), true):
177
+ return false
178
+ var touch: InputEventScreenTouch = event as InputEventScreenTouch
179
+ if touch == null:
180
+ return (event as InputEventScreenDrag).index == _emulating
181
+ if touch.pressed:
182
+ if _emulating != -1:
183
+ return false
184
+ _emulating = touch.index
185
+ return true
186
+ if touch.index != _emulating:
187
+ return false
188
+ _emulating = -1
189
+ return true
190
+
191
+
192
+ static func _touch(position: Vector2, index: int, pressed: bool) -> InputEventScreenTouch:
193
+ var event: InputEventScreenTouch = InputEventScreenTouch.new()
194
+ event.index = index
195
+ event.position = position
196
+ event.pressed = pressed
197
+ return event
198
+
199
+
200
+ ## A drag carrying [param relative] as the movement since the last, and the speed that is at the
201
+ ## frame rate the game runs at.
202
+ func _drag(position: Vector2, relative: Vector2, index: int) -> InputEventScreenDrag:
203
+ var event: InputEventScreenDrag = InputEventScreenDrag.new()
204
+ event.index = index
205
+ event.position = position
206
+ event.relative = relative
207
+ event.screen_relative = relative
208
+ var seconds: float = maxf(_host.get_process_delta_time(), 1.0 / 60.0)
209
+ event.velocity = relative / seconds
210
+ event.screen_velocity = event.velocity
211
+ return event
@@ -0,0 +1 @@
1
+ uid://ciivqlhs2dev0
@@ -112,11 +112,15 @@ static func field_before(focused: Control) -> Dictionary:
112
112
  ## Whether [param focused], read as [param before], stopped being edited, as a [LineEdit] does when
113
113
  ## it is submitted: it keeps the focus and drops every key after, so a caller reading only the focus
114
114
  ## typed next into nothing.
115
- static func ended_editing(focused: Control, before: Dictionary) -> bool:
115
+ ##
116
+ ## [param focused] is a Variant because the key can free it, as Escape closing the panel a field is
117
+ ## on does, and a parameter typed Control refuses a freed object before the check here can run.
118
+ static func ended_editing(focused: Variant, before: Dictionary) -> bool:
116
119
  var was: bool = before.get("editing", false)
117
120
  if not was or not is_instance_valid(focused):
118
121
  return false
119
- var field: LineEdit = focused as LineEdit
122
+ var held: Object = focused
123
+ var field: LineEdit = held as LineEdit
120
124
  return field != null and not field.is_editing()
121
125
 
122
126
 
@@ -124,15 +128,18 @@ static func ended_editing(focused: Control, before: Dictionary) -> bool:
124
128
  ## typed_into and holds where its text changed, and editing_ended where it stopped being edited. A
125
129
  ## field being edited takes a printable key as typing and keeps it from the game's _unhandled_input,
126
130
  ## so Space into a name field pressed no jump, and an answer naming only the focus did not say why.
127
- static func field_noted(said: Dictionary, focused: Control, before: Dictionary) -> Dictionary:
131
+ ## A Variant for the reason [method ended_editing] gives.
132
+ static func field_noted(said: Dictionary, focused: Variant, before: Dictionary) -> Dictionary:
128
133
  if before.is_empty() or not is_instance_valid(focused):
129
134
  return said
130
- var field: LineEdit = focused as LineEdit
131
- var now: String = field.text if field != null else (focused as TextEdit).text
135
+ var held: Object = focused
136
+ var control: Control = held as Control
137
+ var field: LineEdit = control as LineEdit
138
+ var now: String = field.text if field != null else (control as TextEdit).text
132
139
  if now != before["text"]:
133
- said["typed_into"] = str(focused.get_path())
134
- said["holds"] = _what_it_holds(focused)
135
- if ended_editing(focused, before):
140
+ said["typed_into"] = str(control.get_path())
141
+ said["holds"] = _what_it_holds(control)
142
+ if ended_editing(control, before):
136
143
  said["editing_ended"] = true
137
144
  return said
138
145
 
@@ -64,7 +64,13 @@ static func scene_states(scene_path: String, tree_path: String, state_machine_pa
64
64
  player_path = "."
65
65
  var found: Array = _bare(scene_path, player_path, "AnimationPlayer")
66
66
  player = found[0]
67
- var answer: Dictionary = states(tree, state_machine_path, player, player_path)
67
+ var reached: Node = null
68
+ if player == null and not player_path.is_empty():
69
+ reached = _bare(scene_path, player_path, "Node")[0]
70
+ var unplayed: String = no_player(str(tree.anim_player), reached)
71
+ if reached != null:
72
+ reached.free()
73
+ var answer: Dictionary = states(tree, state_machine_path, player, player_path, unplayed)
68
74
  tree.free()
69
75
  if player != null:
70
76
  player.free()
@@ -236,6 +242,20 @@ static func no_machine(tree: AnimationTree, state_machine_path: String) -> Strin
236
242
  return "No state machine at " + state_machine_path
237
243
 
238
244
 
245
+ ## Why no state finds its animation when [param anim_player], an AnimationTree's path to its
246
+ ## player, reaches no AnimationPlayer: [param reached] is the node it reaches instead, null for none.
247
+ static func no_player(anim_player: String, reached: Node) -> String:
248
+ var until: String = "so no state plays anything until scene_node set points anim_player at one."
249
+ if anim_player.is_empty():
250
+ return "The AnimationTree has no anim_player, " + until
251
+ if reached == null:
252
+ return "The AnimationTree's anim_player %s reaches no node, %s" % [anim_player, until]
253
+ return (
254
+ "The AnimationTree's anim_player %s reaches %s, a %s rather than an AnimationPlayer, %s"
255
+ % [anim_player, reached.name, reached.get_class(), until]
256
+ )
257
+
258
+
239
259
  ## Why a state playing [param animation_name] from [param player], at [param player_path] from the
240
260
  ## scene root as playerNodePath takes it, plays nothing yet. anim_player is relative to the tree
241
261
  ## ("../Anim"), which no argument takes.
@@ -254,9 +274,14 @@ static func missing_animation(player: AnimationPlayer, player_path: String, anim
254
274
 
255
275
  ## The states and transitions of the state machine at [param state_machine_path] in [param tree].
256
276
  ## A state playing an animation says whether [param player], at [param player_path], has it, as
257
- ## add_state answers: a state naming one it lacks plays nothing, and says so only in a log.
277
+ ## add_state answers: a state naming one it lacks plays nothing, and says so only in a log. With no
278
+ ## player, each says [param unplayed], what [method no_player] made of the tree's path.
258
279
  static func states(
259
- tree: AnimationTree, state_machine_path: String, player: AnimationPlayer, player_path: String
280
+ tree: AnimationTree,
281
+ state_machine_path: String,
282
+ player: AnimationPlayer,
283
+ player_path: String,
284
+ unplayed: String
260
285
  ) -> Dictionary:
261
286
  var sm: AnimationNodeStateMachine = state_machine(tree, state_machine_path)
262
287
  if sm == null:
@@ -270,7 +295,9 @@ static func states(
270
295
  entry["animationName"] = str(plays.animation)
271
296
  var found: bool = player != null and player.has_animation(plays.animation)
272
297
  entry["animationFound"] = found
273
- if player != null and not found:
298
+ if player == null:
299
+ entry["animationMissing"] = unplayed
300
+ elif not found:
274
301
  entry["animationMissing"] = missing_animation(player, player_path, str(plays.animation))
275
302
  listed.append(entry)
276
303
  var transitions: Array[Dictionary] = []
@@ -428,7 +428,10 @@ func _declared_class_name(path: String) -> String:
428
428
  func _usage_files(kinds: Array) -> Array[String]:
429
429
  var found: Array[String] = []
430
430
  for kind: Variant in kinds:
431
- match str(kind):
431
+ # As project_search reads an extension: ".tscn" searched no file, and the answer said
432
+ # nothing used a script its scene attaches.
433
+ var named: String = str(kind).trim_prefix(".").to_lower()
434
+ match named:
432
435
  "godot":
433
436
  found.append("res://project.godot")
434
437
  "cfg":
@@ -436,7 +439,7 @@ func _usage_files(kinds: Array) -> Array[String]:
436
439
  if FileAccess.file_exists("res://export_presets.cfg"):
437
440
  found.append("res://export_presets.cfg")
438
441
  _:
439
- found.append_array(_files.find_files("res://", "." + str(kind)))
442
+ found.append_array(_files.find_files("res://", "." + named))
440
443
  return found
441
444
 
442
445
 
@@ -11,6 +11,7 @@ extends RefCounted
11
11
  ## initialisers run, and a resource with a script is made with it.
12
12
 
13
13
  const GdhRead = preload("reading.gd")
14
+ const GdhFileWalk = preload("file_walk.gd")
14
15
  const GdhSame = preload("same_values.gd")
15
16
  const GdhSerialisation = preload("serialisation.gd")
16
17
  const GdhShapes = preload("value_shapes.gd")
@@ -487,7 +488,7 @@ static func _stored(type: String, given: Dictionary, include_defaults: bool) ->
487
488
  static func resource(res_path: String, include_defaults: bool) -> Dictionary:
488
489
  var file: String = res_path.get_slice("::", 0)
489
490
  if not FileAccess.file_exists(file):
490
- return {"ok": false, "error": "No resource at " + file}
491
+ return {"ok": false, "error": GdhFileWalk.new().with_nearest_files("No resource at " + file, file)}
491
492
  var whole: Resource = ResourceLoader.load(file, "", ResourceLoader.CACHE_MODE_IGNORE)
492
493
  if whole == null:
493
494
  return {"ok": false, "error": "%s did not load as a resource." % file}
@@ -516,6 +517,18 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
516
517
  scripted[str(declared.get("name", ""))] = true
517
518
  var serialiser: GdhSerialisation = GdhSerialisation.new()
518
519
  var properties: Dictionary = {}
520
+ # A headless engine runs a script that is not a tool as the file loads, so what its _init and
521
+ # setters gave other properties read as values the file sets; the file's own lines say which it
522
+ # does. The editor runs no such script.
523
+ var untooled: bool = script != null and not script.is_tool() and not Engine.is_editor_hint()
524
+ var from_file: bool = (
525
+ untooled and file.ends_with(".tres") and not GdhShapes.STORED_APART.has(loaded.get_class())
526
+ )
527
+ var set_in_file: Dictionary = (
528
+ _names_set_in(FileAccess.get_file_as_string(file), res_path.trim_prefix(file).trim_prefix("::"))
529
+ if from_file
530
+ else {}
531
+ )
519
532
  # A Curve's points are read off one with none, since the engine has no default for point_0.
520
533
  var fresh: Object = (
521
534
  ClassDB.instantiate(loaded.get_class()) if GdhShapes.STORED_APART.has(loaded.get_class()) else null
@@ -537,6 +550,8 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
537
550
  )
538
551
  if GdhSame.same(value, default):
539
552
  continue
553
+ if from_file and not set_in_file.has(property):
554
+ continue
540
555
  properties[property] = serialiser.serialize_value(value)
541
556
  var answer: Dictionary = {
542
557
  "ok": true, "resourcePath": res_path, "resourceType": loaded.get_class(), "properties": properties
@@ -551,6 +566,26 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
551
566
  return answer
552
567
 
553
568
 
569
+ ## The names the [code][resource][/code] section of [param text], a .tres file, sets, or those of the
570
+ ## embedded resource with the id [param id] when one is given, as a set.
571
+ static func _names_set_in(text: String, id: String) -> Dictionary:
572
+ var names: Dictionary = {}
573
+ var inside: bool = false
574
+ var key: RegEx = RegEx.create_from_string("^([^\\s=\\[][^=]*?) = ")
575
+ for line: String in text.split("\n"):
576
+ if line.begins_with("["):
577
+ inside = (
578
+ line.begins_with("[resource]")
579
+ if id.is_empty()
580
+ else line.begins_with("[sub_resource") and line.contains('id="%s"' % id)
581
+ )
582
+ continue
583
+ var found: RegExMatch = key.search(line) if inside else null
584
+ if found != null:
585
+ names[found.get_string(1)] = true
586
+ return names
587
+
588
+
554
589
  ## The resource [param file] keeps under [param id], found in [param value] or anything it holds.
555
590
  ## The engine loads such a path only while the file is cached, and a read loads the file uncached,
556
591
  ## so it is looked for among what the file holds. A resource of another file is neither taken nor