gdharness 0.6.0 → 0.6.1

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.
@@ -220,6 +220,36 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
220
220
  }
221
221
 
222
222
 
223
+ ## The viewport a click aimed at [param control] has to be pushed into.
224
+ ##
225
+ ## Its own, unless that is an embedded [Window]. A ConfirmationDialog is a Window, and under
226
+ ## `gui_embed_subwindows` a Window is drawn inside its parent rather than given one by the
227
+ ## desktop: pushing into its own viewport delivers nothing, measured, with the pointer reading as
228
+ ## over no control at all. What reaches it is the parent, which is what the engine does with a
229
+ ## real pointer. Walked rather than stepped once, because a dialog can open a dialog.
230
+ static func _clicking_viewport(control: Control) -> Viewport:
231
+ var viewport: Viewport = control.get_viewport()
232
+ var window: Window = viewport as Window
233
+ while window != null and window.is_embedded() and window.get_parent() != null:
234
+ viewport = window.get_parent().get_viewport()
235
+ window = viewport as Window
236
+ return viewport
237
+
238
+
239
+ ## Where [param control]'s centre is in the viewport [method _clicking_viewport] names.
240
+ ##
241
+ ## The transform is against the control's own viewport, so every embedded window between it and
242
+ ## that one contributes its offset. The same walk, because the two answers have to agree about
243
+ ## which viewport they are describing.
244
+ static func _centre_of(control: Control) -> Vector2:
245
+ var centre: Vector2 = control.get_global_transform_with_canvas() * (control.size * 0.5)
246
+ var window: Window = control.get_viewport() as Window
247
+ while window != null and window.is_embedded() and window.get_parent() != null:
248
+ centre += Vector2(window.position)
249
+ window = window.get_parent().get_viewport() as Window
250
+ return centre
251
+
252
+
223
253
  ## Scrolls whatever is holding [param control] until it is on screen, and answers whether
224
254
  ## anything moved.
225
255
  ##
@@ -261,8 +291,8 @@ func click(params: Dictionary) -> Dictionary:
261
291
  if not control.is_visible_in_tree():
262
292
  return {"type": "error", "message": "%s is not visible, so nothing can click it" % node_path}
263
293
 
264
- var viewport: Viewport = control.get_viewport()
265
- var centre: Vector2 = control.get_global_transform_with_canvas() * (control.size * 0.5)
294
+ var viewport: Viewport = _clicking_viewport(control)
295
+ var centre: Vector2 = _centre_of(control)
266
296
 
267
297
  # A control below the fold of a ScrollContainer is not out of reach, it is one scroll away,
268
298
  # which is what a person does without thinking about it before they click. Refusing it
@@ -314,7 +344,12 @@ func click(params: Dictionary) -> Dictionary:
314
344
  # that went to it. Read in full here, path included, because nothing about that control
315
345
  # is guaranteed to survive the release: a button that opens the next screen takes the
316
346
  # whole menu out of the tree, and a node that has left the tree has no path to give.
317
- var hovered: Control = viewport.gui_get_hovered_control()
347
+ # Asked of the control's own viewport rather than the one the event went into, and for an
348
+ # embedded window those are two different objects: the parent takes the event and hands it on,
349
+ # and the window keeps the GUI state. Reading the parent reported every dialog as not landed
350
+ # while the button it was aimed at pressed perfectly well, which is the worst shape an answer
351
+ # can have, since the caller believes the miss over what the game just did.
352
+ var hovered: Control = control.get_viewport().gui_get_hovered_control()
318
353
  var hovered_path: Variant = null
319
354
  if hovered != null:
320
355
  hovered_path = str(hovered.get_path())
@@ -75,7 +75,13 @@ func find_nodes(params: Dictionary) -> Dictionary:
75
75
  truncated = true
76
76
  break
77
77
  found.append(_found(node, wanted_property))
78
- var children: Array[Node] = node.get_children()
78
+ # Internal children included, which they were not. A ConfirmationDialog builds its Yes and
79
+ # its No as internal nodes, and a ScrollContainer its bars, so a find over a screen for
80
+ # every Button came back without the two buttons the player is being asked to press:
81
+ # nothing here could see the dialog at all, and the way past it was to emit `confirmed`.
82
+ # A filtered query carries no cost for including them, because they only appear when they
83
+ # are what was asked for.
84
+ var children: Array[Node] = node.get_children(true)
79
85
  for index: int in range(children.size() - 1, -1, -1):
80
86
  pending.push_front(children[index])
81
87
 
@@ -286,7 +292,11 @@ func _serialize_node_tree(node: Node, depth: int, max_depth: int, include_proper
286
292
 
287
293
  if depth < max_depth:
288
294
  var children: Array = []
289
- for child: Node in node.get_children():
295
+ # Internal ones too, for the reason `find` takes them: a tree that answers "no children"
296
+ # over a ConfirmationDialog holding a Yes and a No is not tidier than one that says so,
297
+ # it is wrong, and it is what sends somebody looking for another way to press the button.
298
+ # `depth` is what keeps the answer a size worth reading.
299
+ for child: Node in node.get_children(true):
290
300
  children.append(_serialize_node_tree(child, depth + 1, max_depth, include_properties))
291
301
  result["children"] = children
292
302
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",