gdharness 0.5.10 → 0.5.12
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
CHANGED
|
@@ -15751,7 +15751,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15751
15751
|
type: "string",
|
|
15752
15752
|
description: "tree, find: where to start, default /root. rect: the node to place. property: the node to read."
|
|
15753
15753
|
},
|
|
15754
|
-
property: {
|
|
15754
|
+
property: {
|
|
15755
|
+
type: "string",
|
|
15756
|
+
description: "property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label."
|
|
15757
|
+
},
|
|
15755
15758
|
depth: { type: "number", description: "tree: levels to descend. Default 3." },
|
|
15756
15759
|
includeProperties: {
|
|
15757
15760
|
type: "boolean",
|
|
@@ -15778,7 +15781,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15778
15781
|
operations: {
|
|
15779
15782
|
tree: { summary: "the live scene tree", requires: [] },
|
|
15780
15783
|
find: {
|
|
15781
|
-
summary: "the paths of every node matching className, script, namePattern or group",
|
|
15784
|
+
summary: "the paths of every node matching className, script, namePattern or group, with property read off each",
|
|
15782
15785
|
requires: []
|
|
15783
15786
|
},
|
|
15784
15787
|
rect: {
|
|
@@ -15860,7 +15863,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15860
15863
|
requires: [],
|
|
15861
15864
|
operations: {
|
|
15862
15865
|
click: {
|
|
15863
|
-
summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed",
|
|
15866
|
+
summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved",
|
|
15864
15867
|
requires: ["nodePath"]
|
|
15865
15868
|
},
|
|
15866
15869
|
action: { summary: "press or release an action", requires: ["action"] },
|
|
@@ -37251,11 +37254,13 @@ class GodotServer {
|
|
|
37251
37254
|
if (Object.keys(filters).length === 0) {
|
|
37252
37255
|
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group.");
|
|
37253
37256
|
}
|
|
37257
|
+
const property = readNonEmptyString(args, "property");
|
|
37254
37258
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
37255
37259
|
...filters,
|
|
37256
37260
|
projectPath: args["projectPath"],
|
|
37257
37261
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
37258
|
-
limit: readPositiveNumber(args, "limit") ?? 100
|
|
37262
|
+
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
37263
|
+
...property === undefined ? {} : { property }
|
|
37259
37264
|
});
|
|
37260
37265
|
}
|
|
37261
37266
|
async handleRuntimeWait(op, args) {
|
|
@@ -12,6 +12,10 @@ const TO_SMALL: int = 32
|
|
|
12
12
|
const NEWLINE: int = 10
|
|
13
13
|
const TAB: int = 9
|
|
14
14
|
|
|
15
|
+
## What a game started without a window gets whatever the project settings say, and the usual
|
|
16
|
+
## reason a control is out of reach. Only worth telling somebody when it is what they have.
|
|
17
|
+
const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
|
|
18
|
+
|
|
15
19
|
var _host: Node
|
|
16
20
|
var _values: Values
|
|
17
21
|
|
|
@@ -216,10 +220,33 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
|
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
|
|
223
|
+
## Scrolls whatever is holding [param control] until it is on screen, and answers whether
|
|
224
|
+
## anything moved.
|
|
225
|
+
##
|
|
226
|
+
## Innermost container first and outwards, because ensuring visibility inside an inner one moves
|
|
227
|
+
## the control within the outer one, so the outer has to be asked after the inner has finished
|
|
228
|
+
## moving it. A control with no ScrollContainer over it moves nothing and answers false, which is
|
|
229
|
+
## what keeps the refusal below saying the right thing about a control that is simply off screen.
|
|
230
|
+
static func _scroll_into_view(control: Control) -> bool:
|
|
231
|
+
var moved: bool = false
|
|
232
|
+
var walking: Node = control.get_parent()
|
|
233
|
+
while walking != null:
|
|
234
|
+
var holder: ScrollContainer = walking as ScrollContainer
|
|
235
|
+
if holder != null:
|
|
236
|
+
holder.ensure_control_visible(control)
|
|
237
|
+
moved = true
|
|
238
|
+
walking = walking.get_parent()
|
|
239
|
+
return moved
|
|
240
|
+
|
|
241
|
+
|
|
219
242
|
## A whole click on a Control: the pointer moves onto it, the button goes down, a frame passes,
|
|
220
243
|
## the button comes up. BaseButton fires on the release, which is why a single injected press
|
|
221
244
|
## never pressed anything. The position is the control's centre carried into window pixels, so
|
|
222
245
|
## the caller never has to do that arithmetic.
|
|
246
|
+
##
|
|
247
|
+
## A control out of sight inside a ScrollContainer is scrolled to first; the answer says so under
|
|
248
|
+
## `scrolled_into_view`, because the view having moved is a thing that happened to the screen and
|
|
249
|
+
## the caller is the only one who can tell whether that matters.
|
|
223
250
|
func click(params: Dictionary) -> Dictionary:
|
|
224
251
|
var node_path: String = str(params.get("path", ""))
|
|
225
252
|
if node_path.is_empty():
|
|
@@ -236,6 +263,19 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
236
263
|
|
|
237
264
|
var viewport: Viewport = control.get_viewport()
|
|
238
265
|
var centre: Vector2 = control.get_global_transform_with_canvas() * (control.size * 0.5)
|
|
266
|
+
|
|
267
|
+
# A control below the fold of a ScrollContainer is not out of reach, it is one scroll away,
|
|
268
|
+
# which is what a person does without thinking about it before they click. Refusing it
|
|
269
|
+
# instead sent callers to emit the button's own signal, which presses nothing, runs none of
|
|
270
|
+
# the input path and reports success.
|
|
271
|
+
var scrolled: bool = false
|
|
272
|
+
if not viewport.get_visible_rect().has_point(centre):
|
|
273
|
+
scrolled = _scroll_into_view(control)
|
|
274
|
+
if scrolled:
|
|
275
|
+
# A container moves its child on the next layout pass rather than inside the call.
|
|
276
|
+
await _host.get_tree().process_frame
|
|
277
|
+
centre = control.get_global_transform_with_canvas() * (control.size * 0.5)
|
|
278
|
+
|
|
239
279
|
var position: Vector2 = viewport.get_final_transform() * centre
|
|
240
280
|
# The GUI only delivers to what is inside the viewport, so a centre outside it would be a
|
|
241
281
|
# click that silently reached nothing. A game with no window has a 64 by 64 viewport
|
|
@@ -243,11 +283,17 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
243
283
|
# something the caller can read off the rect on its own.
|
|
244
284
|
if not viewport.get_visible_rect().has_point(centre):
|
|
245
285
|
var why: String = ""
|
|
246
|
-
if
|
|
247
|
-
why =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
286
|
+
if scrolled:
|
|
287
|
+
why = ". It was scrolled as far as what holds it goes and is still out there"
|
|
288
|
+
elif not _host.get_tree().root.can_draw():
|
|
289
|
+
why = ". This game has no window: run it with a window to reach this control"
|
|
290
|
+
# Only where it is true. The rect is printed just above, so claiming 64 by 64 over a
|
|
291
|
+
# viewport somebody has resized says two different things in one sentence.
|
|
292
|
+
if viewport.get_visible_rect().size == HEADLESS_VIEWPORT:
|
|
293
|
+
why = (
|
|
294
|
+
". This game has no window, and a game with no window has a 64 by 64 viewport "
|
|
295
|
+
+ "whatever the project settings say: run it with a window to reach this control"
|
|
296
|
+
)
|
|
251
297
|
return {
|
|
252
298
|
"type": "error",
|
|
253
299
|
"message":
|
|
@@ -298,6 +344,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
298
344
|
"hovered": hovered_path,
|
|
299
345
|
"landed": landed,
|
|
300
346
|
"control_afterwards": afterwards,
|
|
347
|
+
"scrolled_into_view": scrolled,
|
|
301
348
|
}
|
|
302
349
|
|
|
303
350
|
|
|
@@ -36,12 +36,19 @@ func get_tree(params: Dictionary) -> Dictionary:
|
|
|
36
36
|
## Nodes matching every filter given, as paths, so a caller can name what it wants without
|
|
37
37
|
## reading the whole tree to find it. `class` matches native classes and their subclasses, and
|
|
38
38
|
## the global name of a script class; `name` is a case-insensitive glob; `script` is a path.
|
|
39
|
+
##
|
|
40
|
+
## `property` names one to read off each of them, which is the difference between one question and
|
|
41
|
+
## one call per answer. A panel of a dozen labels took thirteen calls to read, and a tree that
|
|
42
|
+
## rebuilds between them, which any HUD following a clock does, hands back paths that are gone by
|
|
43
|
+
## the time they are asked about. A node without that property says so rather than answering null,
|
|
44
|
+
## because null is what a node holding null answers.
|
|
39
45
|
func find_nodes(params: Dictionary) -> Dictionary:
|
|
40
46
|
var root_path: String = str(params.get("root", "/root"))
|
|
41
47
|
var wanted_class: String = str(params.get("class", ""))
|
|
42
48
|
var wanted_script: String = str(params.get("script", ""))
|
|
43
49
|
var wanted_name: String = str(params.get("name", ""))
|
|
44
50
|
var wanted_group: String = str(params.get("group", ""))
|
|
51
|
+
var wanted_property: String = str(params.get("property", ""))
|
|
45
52
|
var limit: int = clampi(int(params.get("limit", FIND_LIMIT)), 1, FIND_LIMIT_CEILING)
|
|
46
53
|
|
|
47
54
|
if (
|
|
@@ -67,7 +74,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
67
74
|
if found.size() >= limit:
|
|
68
75
|
truncated = true
|
|
69
76
|
break
|
|
70
|
-
found.append(
|
|
77
|
+
found.append(_found(node, wanted_property))
|
|
71
78
|
var children: Array[Node] = node.get_children()
|
|
72
79
|
for index: int in range(children.size() - 1, -1, -1):
|
|
73
80
|
pending.push_front(children[index])
|
|
@@ -75,6 +82,29 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
75
82
|
return {"type": "nodes", "count": found.size(), "truncated": truncated, "nodes": found}
|
|
76
83
|
|
|
77
84
|
|
|
85
|
+
## One match, with the named property on it when one was named.
|
|
86
|
+
##
|
|
87
|
+
## `has_property` alongside the value, because a node that has not got it and a node holding null
|
|
88
|
+
## are different answers and a bare null reads as the second. The same distinction the `property`
|
|
89
|
+
## op makes by refusing, which it cannot do here: a find over a panel matches nodes of several
|
|
90
|
+
## classes on purpose, and refusing the whole answer because one of them has no `text` would make
|
|
91
|
+
## the question unaskable.
|
|
92
|
+
func _found(node: Node, wanted_property: String) -> Dictionary:
|
|
93
|
+
var entry: Dictionary = _serialize_node(node, false)
|
|
94
|
+
if wanted_property.is_empty():
|
|
95
|
+
return entry
|
|
96
|
+
var has: bool = false
|
|
97
|
+
for prop: Dictionary in node.get_property_list():
|
|
98
|
+
if str(prop["name"]) == wanted_property:
|
|
99
|
+
has = true
|
|
100
|
+
break
|
|
101
|
+
entry["property"] = wanted_property
|
|
102
|
+
entry["has_property"] = has
|
|
103
|
+
if has:
|
|
104
|
+
entry["value"] = _values.serialize(node.get(wanted_property))
|
|
105
|
+
return entry
|
|
106
|
+
|
|
107
|
+
|
|
78
108
|
func _matches(
|
|
79
109
|
node: Node, wanted_class: String, wanted_script: String, wanted_name: String, wanted_group: String
|
|
80
110
|
) -> bool:
|
package/build/index.js
CHANGED
|
@@ -28265,7 +28265,10 @@ var TOOL_SPECS = [
|
|
|
28265
28265
|
type: "string",
|
|
28266
28266
|
description: "tree, find: where to start, default /root. rect: the node to place. property: the node to read."
|
|
28267
28267
|
},
|
|
28268
|
-
property: {
|
|
28268
|
+
property: {
|
|
28269
|
+
type: "string",
|
|
28270
|
+
description: "property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label."
|
|
28271
|
+
},
|
|
28269
28272
|
depth: { type: "number", description: "tree: levels to descend. Default 3." },
|
|
28270
28273
|
includeProperties: {
|
|
28271
28274
|
type: "boolean",
|
|
@@ -28292,7 +28295,7 @@ var TOOL_SPECS = [
|
|
|
28292
28295
|
operations: {
|
|
28293
28296
|
tree: { summary: "the live scene tree", requires: [] },
|
|
28294
28297
|
find: {
|
|
28295
|
-
summary: "the paths of every node matching className, script, namePattern or group",
|
|
28298
|
+
summary: "the paths of every node matching className, script, namePattern or group, with property read off each",
|
|
28296
28299
|
requires: []
|
|
28297
28300
|
},
|
|
28298
28301
|
rect: {
|
|
@@ -28374,7 +28377,7 @@ var TOOL_SPECS = [
|
|
|
28374
28377
|
requires: [],
|
|
28375
28378
|
operations: {
|
|
28376
28379
|
click: {
|
|
28377
|
-
summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed",
|
|
28380
|
+
summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved",
|
|
28378
28381
|
requires: ["nodePath"]
|
|
28379
28382
|
},
|
|
28380
28383
|
action: { summary: "press or release an action", requires: ["action"] },
|
|
@@ -30024,11 +30027,13 @@ class GodotServer {
|
|
|
30024
30027
|
if (Object.keys(filters).length === 0) {
|
|
30025
30028
|
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group.");
|
|
30026
30029
|
}
|
|
30030
|
+
const property = readNonEmptyString(args, "property");
|
|
30027
30031
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
30028
30032
|
...filters,
|
|
30029
30033
|
projectPath: args["projectPath"],
|
|
30030
30034
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
30031
|
-
limit: readPositiveNumber(args, "limit") ?? 100
|
|
30035
|
+
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
30036
|
+
...property === undefined ? {} : { property }
|
|
30032
30037
|
});
|
|
30033
30038
|
}
|
|
30034
30039
|
async handleRuntimeWait(op, args) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.12",
|
|
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",
|