gdharness 0.6.6 → 0.6.8
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
|
@@ -15785,7 +15785,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15785
15785
|
operations: {
|
|
15786
15786
|
tree: { summary: "the live scene tree", requires: [] },
|
|
15787
15787
|
text: {
|
|
15788
|
-
summary: "every line of text under nodePath, in the order somebody reads the screen, leaving out what is hidden and everything under it",
|
|
15788
|
+
summary: "every line of text under nodePath, the values in its fields included, in the order somebody reads the screen, leaving out what is hidden and everything under it",
|
|
15789
15789
|
requires: []
|
|
15790
15790
|
},
|
|
15791
15791
|
find: {
|
|
@@ -15893,7 +15893,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15893
15893
|
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
15894
15894
|
key: { summary: "press a key, or hold it", requires: ["keycode"] },
|
|
15895
15895
|
text: {
|
|
15896
|
-
summary: "type a string
|
|
15896
|
+
summary: "type a string into the field being edited, a character at a time, and say what it landed in",
|
|
15897
15897
|
requires: ["text"]
|
|
15898
15898
|
},
|
|
15899
15899
|
mouse_click: { summary: "one mouse button event at a position", requires: ["x", "y"] },
|
|
@@ -15905,14 +15905,17 @@ var init_tool_definitions = __esm(() => {
|
|
|
15905
15905
|
description: "Lets the running game get on with it and answers when something has happened: a number of frames, a signal, or a property reaching a value. Needs the game running with the runtime addon.",
|
|
15906
15906
|
parameters: {
|
|
15907
15907
|
projectPath: RUNNING_PROJECT_PATH,
|
|
15908
|
-
frames: {
|
|
15908
|
+
frames: {
|
|
15909
|
+
type: "number",
|
|
15910
|
+
description: "frames: how many to let pass, 1 to 600. More than that is refused."
|
|
15911
|
+
},
|
|
15909
15912
|
nodePath: { type: "string", description: "signal, until: the node." },
|
|
15910
15913
|
signal: { type: "string", description: "signal: the signal name." },
|
|
15911
15914
|
property: { type: "string", description: "until: the property name." },
|
|
15912
15915
|
value: { description: "until: the value to wait for, fitted to the property's type." },
|
|
15913
15916
|
timeoutMs: {
|
|
15914
15917
|
type: "number",
|
|
15915
|
-
description: "signal, until: how long to wait before answering anyway. Default 5000."
|
|
15918
|
+
description: "signal, until: how long to wait before answering anyway, 1 to 120000. Default 5000."
|
|
15916
15919
|
}
|
|
15917
15920
|
},
|
|
15918
15921
|
requires: [],
|
|
@@ -38827,7 +38830,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
38827
38830
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
38828
38831
|
| What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
38829
38832
|
| Press a button | \`runtime_input click\`, which says what was under the pointer |
|
|
38830
|
-
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text
|
|
38833
|
+
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\`; a submitted field has to be clicked again |
|
|
38831
38834
|
| Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
|
|
38832
38835
|
| Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says |
|
|
38833
38836
|
| Answer a dialog, or press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
|
|
@@ -161,6 +161,11 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
161
161
|
return {"type": "error", "message": "text required"}
|
|
162
162
|
|
|
163
163
|
var viewport: Viewport = _host.get_tree().root
|
|
164
|
+
var focused: Control = viewport.gui_get_focus_owner()
|
|
165
|
+
var shut: String = _shut_to_typing(focused)
|
|
166
|
+
if not shut.is_empty():
|
|
167
|
+
return {"type": "error", "message": shut}
|
|
168
|
+
|
|
164
169
|
for index: int in text.length():
|
|
165
170
|
var down: InputEventKey = _typed(text.unicode_at(index))
|
|
166
171
|
viewport.push_input(down)
|
|
@@ -169,7 +174,41 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
169
174
|
up.pressed = false
|
|
170
175
|
viewport.push_input(up)
|
|
171
176
|
|
|
172
|
-
|
|
177
|
+
# Where it went, which is the one thing a caller cannot see from here. Null is a game reading
|
|
178
|
+
# keys for itself with nothing focused, which is a real thing to be typing at.
|
|
179
|
+
var into: Variant = null
|
|
180
|
+
if focused != null:
|
|
181
|
+
into = str(focused.get_path())
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
"type": "input_injected",
|
|
185
|
+
"input_type": "text",
|
|
186
|
+
"text": text,
|
|
187
|
+
"characters": text.length(),
|
|
188
|
+
"into": into,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## Why nothing typed would reach [param focused], or "" when it would.
|
|
193
|
+
##
|
|
194
|
+
## Having the focus is not the same as being edited. Since Godot 4.4 a field is focused and shut
|
|
195
|
+
## until something opens it, which is what a click does and what submitting undoes: press Enter in
|
|
196
|
+
## a box and it keeps the focus and drops every key that arrives afterwards. Typing into one
|
|
197
|
+
## answered that the characters had gone in and put nothing anywhere, which is the shape a refusal
|
|
198
|
+
## exists to prevent. Measured on a spin box in a real game: `has_focus` true, `is_editing` false,
|
|
199
|
+
## three characters reported and the field unchanged.
|
|
200
|
+
##
|
|
201
|
+
## Only a [LineEdit] is refused: it is the one control the engine will say this about, since
|
|
202
|
+
## [TextEdit] has no editing state of its own. A game reading keys for itself is typed at with
|
|
203
|
+
## nothing focused at all, and that is not this tool's business to judge.
|
|
204
|
+
static func _shut_to_typing(focused: Control) -> String:
|
|
205
|
+
var field: LineEdit = focused as LineEdit
|
|
206
|
+
if field == null or field.is_editing():
|
|
207
|
+
return ""
|
|
208
|
+
return (
|
|
209
|
+
"%s has the focus and is not being edited, so nothing typed lands in it. Click it first."
|
|
210
|
+
% focused.get_path()
|
|
211
|
+
)
|
|
173
212
|
|
|
174
213
|
|
|
175
214
|
## The key press that produces [param glyph], as a keyboard would send it.
|
|
@@ -299,6 +338,24 @@ static func _centre_of(control: Control) -> Vector2:
|
|
|
299
338
|
## the control within the outer one, so the outer has to be asked after the inner has finished
|
|
300
339
|
## moving it. A control with no ScrollContainer over it moves nothing and answers false, which is
|
|
301
340
|
## what keeps the refusal below saying the right thing about a control that is simply off screen.
|
|
341
|
+
## Whether [param centre] is somewhere a click can reach it: inside the viewport, and inside every
|
|
342
|
+
## ScrollContainer between the control and the root, each of which clips what it holds.
|
|
343
|
+
static func _in_sight(control: Control, viewport: Viewport, centre: Vector2) -> bool:
|
|
344
|
+
if not viewport.get_visible_rect().has_point(centre):
|
|
345
|
+
return false
|
|
346
|
+
var walking: Node = control.get_parent()
|
|
347
|
+
while walking != null:
|
|
348
|
+
var holder: ScrollContainer = walking as ScrollContainer
|
|
349
|
+
# Carried into the same space the centre is in, which is the canvas rather than the
|
|
350
|
+
# container's own: the two are only the same while nothing above it is transformed.
|
|
351
|
+
if holder != null:
|
|
352
|
+
var seen: Rect2 = holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size)
|
|
353
|
+
if not seen.has_point(centre):
|
|
354
|
+
return false
|
|
355
|
+
walking = walking.get_parent()
|
|
356
|
+
return true
|
|
357
|
+
|
|
358
|
+
|
|
302
359
|
static func _scroll_into_view(control: Control) -> bool:
|
|
303
360
|
var moved: bool = false
|
|
304
361
|
var walking: Node = control.get_parent()
|
|
@@ -341,12 +398,17 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
341
398
|
var viewport: Viewport = _clicking_viewport(control)
|
|
342
399
|
var centre: Vector2 = _centre_of(control)
|
|
343
400
|
|
|
344
|
-
# A control
|
|
401
|
+
# A control out of sight inside a ScrollContainer is not out of reach, it is one scroll away,
|
|
345
402
|
# which is what a person does without thinking about it before they click. Refusing it
|
|
346
403
|
# instead sent callers to emit the button's own signal, which presses nothing, runs none of
|
|
347
404
|
# the input path and reports success.
|
|
405
|
+
#
|
|
406
|
+
# Out of sight against what it is clipped to rather than against the viewport: a row scrolled
|
|
407
|
+
# off the top of its container is inside the viewport, behind whatever is drawn up there, so
|
|
408
|
+
# the click went to that instead and said so. Measured on a hall whose staff panel had been
|
|
409
|
+
# scrolled past: the button was at y 69 and its container started at y 166.
|
|
348
410
|
var scrolled: bool = false
|
|
349
|
-
if not viewport
|
|
411
|
+
if not _in_sight(control, viewport, centre):
|
|
350
412
|
scrolled = _scroll_into_view(control)
|
|
351
413
|
if scrolled:
|
|
352
414
|
# A container moves its child on the next layout pass rather than inside the call.
|
|
@@ -174,19 +174,33 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
174
174
|
|
|
175
175
|
## Walks [param node] depth first, which is the order the screen is laid out in and the order a
|
|
176
176
|
## person reads it.
|
|
177
|
+
##
|
|
178
|
+
## Internal children as well, because the number in a SpinBox is one: the field a player reads it
|
|
179
|
+
## in is a LineEdit the engine builds inside the box and leaves out of `get_children()`, so a
|
|
180
|
+
## screen full of forms answered with every label on it and none of the values in it. The hidden
|
|
181
|
+
## check covers a [Window] for the same walk: a dropdown's popup is a child that is not drawn
|
|
182
|
+
## until it is opened, and reading a closed menu would put every item on the screen.
|
|
177
183
|
func _read_into(node: Node, include_hidden: bool, into: PackedStringArray) -> void:
|
|
178
184
|
if into.size() >= READ_LIMIT:
|
|
179
185
|
return
|
|
180
|
-
|
|
181
|
-
if not include_hidden and control != null and not control.visible:
|
|
186
|
+
if not include_hidden and not _drawn(node):
|
|
182
187
|
return
|
|
183
188
|
var said: String = _said_by(node)
|
|
184
189
|
if not said.is_empty():
|
|
185
190
|
into.append(said)
|
|
186
|
-
for child: Node in node.get_children():
|
|
191
|
+
for child: Node in node.get_children(true):
|
|
187
192
|
_read_into(child, include_hidden, into)
|
|
188
193
|
|
|
189
194
|
|
|
195
|
+
## Whether [param node] is on the screen at all, for the two kinds of thing that can be hidden.
|
|
196
|
+
static func _drawn(node: Node) -> bool:
|
|
197
|
+
var control: CanvasItem = node as CanvasItem
|
|
198
|
+
if control != null:
|
|
199
|
+
return control.visible
|
|
200
|
+
var window: Window = node as Window
|
|
201
|
+
return window == null or window.visible
|
|
202
|
+
|
|
203
|
+
|
|
190
204
|
## What one node says, or "" for a node that says nothing. Anything with a `text` property, which
|
|
191
205
|
## is every label, button and field the interface is built out of.
|
|
192
206
|
static func _said_by(node: Node) -> String:
|
|
@@ -9,6 +9,9 @@ const Values = preload("runtime_values.gd")
|
|
|
9
9
|
## since given up on the reply.
|
|
10
10
|
const CEILING_MSEC: int = 120000
|
|
11
11
|
|
|
12
|
+
## The most frames one wait may cover, which is about half a minute of a game drawing slowly.
|
|
13
|
+
const MOST_FRAMES: int = 600
|
|
14
|
+
|
|
12
15
|
var _host: Node
|
|
13
16
|
var _values: Values
|
|
14
17
|
|
|
@@ -18,8 +21,21 @@ func _init(host: Node, values: Values) -> void:
|
|
|
18
21
|
_values = values
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
## Refuses [param asked] rather than bringing it inside [param least] to [param most].
|
|
25
|
+
##
|
|
26
|
+
## A number quietly brought inside the range is a wait that did not last as long as the caller
|
|
27
|
+
## believes and a timeout that gave up sooner: asking for 900 frames and waiting 600 reads as 900
|
|
28
|
+
## frames of the game having passed, and everything measured off it is out by that much.
|
|
29
|
+
static func _out_of_range(named: String, asked: int, least: int, most: int) -> Dictionary:
|
|
30
|
+
return {
|
|
31
|
+
"type": "error", "message": "%s is %d, and %s takes %d to %d." % [named, asked, named, least, most]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
21
35
|
func wait_frames(params: Dictionary) -> Dictionary:
|
|
22
|
-
var frames: int =
|
|
36
|
+
var frames: int = int(params.get("frames", 1))
|
|
37
|
+
if frames < 1 or frames > MOST_FRAMES:
|
|
38
|
+
return _out_of_range("frames", frames, 1, MOST_FRAMES)
|
|
23
39
|
var started: int = Time.get_ticks_msec()
|
|
24
40
|
for _frame: int in frames:
|
|
25
41
|
await _host.get_tree().process_frame
|
|
@@ -30,7 +46,9 @@ func wait_frames(params: Dictionary) -> Dictionary:
|
|
|
30
46
|
func wait_signal(params: Dictionary) -> Dictionary:
|
|
31
47
|
var node_path: String = str(params.get("path", ""))
|
|
32
48
|
var signal_name: String = str(params.get("signal", ""))
|
|
33
|
-
var timeout_ms: int =
|
|
49
|
+
var timeout_ms: int = int(params.get("timeout_ms", 5000))
|
|
50
|
+
if timeout_ms < 1 or timeout_ms > CEILING_MSEC:
|
|
51
|
+
return _out_of_range("timeout_ms", timeout_ms, 1, CEILING_MSEC)
|
|
34
52
|
if node_path.is_empty() or signal_name.is_empty():
|
|
35
53
|
return {"type": "error", "message": "Node path and signal name required"}
|
|
36
54
|
|
|
@@ -65,7 +83,9 @@ func wait_signal(params: Dictionary) -> Dictionary:
|
|
|
65
83
|
func wait_until(params: Dictionary) -> Dictionary:
|
|
66
84
|
var node_path: String = str(params.get("path", ""))
|
|
67
85
|
var property: String = str(params.get("property", ""))
|
|
68
|
-
var timeout_ms: int =
|
|
86
|
+
var timeout_ms: int = int(params.get("timeout_ms", 5000))
|
|
87
|
+
if timeout_ms < 1 or timeout_ms > CEILING_MSEC:
|
|
88
|
+
return _out_of_range("timeout_ms", timeout_ms, 1, CEILING_MSEC)
|
|
69
89
|
if node_path.is_empty() or property.is_empty():
|
|
70
90
|
return {"type": "error", "message": "Node path and property required"}
|
|
71
91
|
if not params.has("value"):
|
package/build/index.js
CHANGED
|
@@ -28339,7 +28339,7 @@ var TOOL_SPECS = [
|
|
|
28339
28339
|
operations: {
|
|
28340
28340
|
tree: { summary: "the live scene tree", requires: [] },
|
|
28341
28341
|
text: {
|
|
28342
|
-
summary: "every line of text under nodePath, in the order somebody reads the screen, leaving out what is hidden and everything under it",
|
|
28342
|
+
summary: "every line of text under nodePath, the values in its fields included, in the order somebody reads the screen, leaving out what is hidden and everything under it",
|
|
28343
28343
|
requires: []
|
|
28344
28344
|
},
|
|
28345
28345
|
find: {
|
|
@@ -28447,7 +28447,7 @@ var TOOL_SPECS = [
|
|
|
28447
28447
|
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
28448
28448
|
key: { summary: "press a key, or hold it", requires: ["keycode"] },
|
|
28449
28449
|
text: {
|
|
28450
|
-
summary: "type a string
|
|
28450
|
+
summary: "type a string into the field being edited, a character at a time, and say what it landed in",
|
|
28451
28451
|
requires: ["text"]
|
|
28452
28452
|
},
|
|
28453
28453
|
mouse_click: { summary: "one mouse button event at a position", requires: ["x", "y"] },
|
|
@@ -28459,14 +28459,17 @@ var TOOL_SPECS = [
|
|
|
28459
28459
|
description: "Lets the running game get on with it and answers when something has happened: a number of frames, a signal, or a property reaching a value. Needs the game running with the runtime addon.",
|
|
28460
28460
|
parameters: {
|
|
28461
28461
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28462
|
-
frames: {
|
|
28462
|
+
frames: {
|
|
28463
|
+
type: "number",
|
|
28464
|
+
description: "frames: how many to let pass, 1 to 600. More than that is refused."
|
|
28465
|
+
},
|
|
28463
28466
|
nodePath: { type: "string", description: "signal, until: the node." },
|
|
28464
28467
|
signal: { type: "string", description: "signal: the signal name." },
|
|
28465
28468
|
property: { type: "string", description: "until: the property name." },
|
|
28466
28469
|
value: { description: "until: the value to wait for, fitted to the property's type." },
|
|
28467
28470
|
timeoutMs: {
|
|
28468
28471
|
type: "number",
|
|
28469
|
-
description: "signal, until: how long to wait before answering anyway. Default 5000."
|
|
28472
|
+
description: "signal, until: how long to wait before answering anyway, 1 to 120000. Default 5000."
|
|
28470
28473
|
}
|
|
28471
28474
|
},
|
|
28472
28475
|
requires: [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
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",
|