gdharness 0.8.0 → 0.9.0
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
|
@@ -15966,7 +15966,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15966
15966
|
},
|
|
15967
15967
|
{
|
|
15968
15968
|
name: "runtime_wait",
|
|
15969
|
-
description: "Lets the running game get on with it and answers when something has happened: a number of frames, a signal,
|
|
15969
|
+
description: "Lets the running game get on with it and answers when something has happened: a number of frames, a signal, a property reaching a value, or words appearing on a screen. Needs the game running with the runtime addon.",
|
|
15970
15970
|
parameters: {
|
|
15971
15971
|
projectPath: RUNNING_PROJECT_PATH,
|
|
15972
15972
|
frames: {
|
|
@@ -15978,6 +15978,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
15978
15978
|
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
15979
15979
|
property: { type: "string", ops: ["until"], description: "until: the property name." },
|
|
15980
15980
|
value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
|
|
15981
|
+
says: {
|
|
15982
|
+
type: "string",
|
|
15983
|
+
ops: ["until"],
|
|
15984
|
+
description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, hidden nodes included."
|
|
15985
|
+
},
|
|
15981
15986
|
timeoutMs: {
|
|
15982
15987
|
type: "number",
|
|
15983
15988
|
ops: ["signal", "until"],
|
|
@@ -15992,8 +15997,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
15992
15997
|
requires: ["nodePath", "signal"]
|
|
15993
15998
|
},
|
|
15994
15999
|
until: {
|
|
15995
|
-
summary: "wait for a property to read as a value and answer with what it
|
|
15996
|
-
requires: ["nodePath"
|
|
16000
|
+
summary: "wait for a property to read as a value, or for words to appear under a node, and answer with what it found",
|
|
16001
|
+
requires: ["nodePath"]
|
|
15997
16002
|
}
|
|
15998
16003
|
}
|
|
15999
16004
|
},
|
|
@@ -37542,6 +37547,7 @@ class GodotServer {
|
|
|
37542
37547
|
path: nodePath,
|
|
37543
37548
|
property: readString(args, "property") ?? "",
|
|
37544
37549
|
value: args["value"],
|
|
37550
|
+
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
37545
37551
|
timeout_ms: timeoutMs
|
|
37546
37552
|
}, patience);
|
|
37547
37553
|
}
|
|
@@ -38927,7 +38933,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
38927
38933
|
| Answer a dialog | \`runtime_input click\` on its button, or \`key\` Escape to dismiss it: an [AcceptDialog] reads that key itself and never asks the InputMap, so \`ui_cancel\` leaves it standing |
|
|
38928
38934
|
| Press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
|
|
38929
38935
|
| Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
|
|
38930
|
-
| Wait for something | \`runtime_wait\`, never a sleep |
|
|
38936
|
+
| Wait for something | \`runtime_wait\`, never a sleep; \`until\` with \`says\` for a panel that rebuilds its own labels |
|
|
38931
38937
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
38932
38938
|
|
|
38933
38939
|
These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
|
|
@@ -131,7 +131,7 @@ func _matches(node: Node, wanted: Dictionary[String, String]) -> bool:
|
|
|
131
131
|
# What this node says, rather than everything said underneath it. A row is then found by the
|
|
132
132
|
# label in it, and the path answered is that label's, which is where the words a caller is
|
|
133
133
|
# looking at actually are: matching every container above it would answer with the screen.
|
|
134
|
-
if not wanted["says"].is_empty() and not
|
|
134
|
+
if not wanted["says"].is_empty() and not said_by(node).containsn(wanted["says"]):
|
|
135
135
|
return false
|
|
136
136
|
var script: Variant = node.get_script()
|
|
137
137
|
if not wanted["script"].is_empty():
|
|
@@ -206,7 +206,7 @@ func _read_into(node: Node, include_hidden: bool, most: int, into: PackedStringA
|
|
|
206
206
|
return
|
|
207
207
|
if not include_hidden and not _drawn(node):
|
|
208
208
|
return
|
|
209
|
-
var said: String =
|
|
209
|
+
var said: String = said_by(node)
|
|
210
210
|
if not said.is_empty():
|
|
211
211
|
into.append(said)
|
|
212
212
|
for child: Node in node.get_children(true):
|
|
@@ -224,7 +224,11 @@ static func _drawn(node: Node) -> bool:
|
|
|
224
224
|
|
|
225
225
|
## What one node says, or "" for a node that says nothing. Anything with a `text` property, which
|
|
226
226
|
## is every label, button and field the interface is built out of.
|
|
227
|
-
|
|
227
|
+
##
|
|
228
|
+
## Public because three questions are the same question: what a screen reads as, which nodes say a
|
|
229
|
+
## given word, and whether anything has come to say it yet. Two copies of what a node says is how
|
|
230
|
+
## the three of them come to disagree about a SpinBox.
|
|
231
|
+
static func said_by(node: Node) -> String:
|
|
228
232
|
for property: Dictionary in node.get_property_list():
|
|
229
233
|
if str(property.get("name", "")) == "text" and int(property.get("type", 0)) == TYPE_STRING:
|
|
230
234
|
return str(node.get("text")).strip_edges()
|
|
@@ -3,6 +3,7 @@ extends RefCounted
|
|
|
3
3
|
## The commands that take time: they let the game run and answer when what was waited for has
|
|
4
4
|
## happened, or when the time ran out, so the caller never sleeps for a guessed length.
|
|
5
5
|
|
|
6
|
+
const Queries = preload("runtime_queries.gd")
|
|
6
7
|
const Values = preload("runtime_values.gd")
|
|
7
8
|
|
|
8
9
|
## The longest one wait may last, whatever the request says: past this the server has long
|
|
@@ -78,22 +79,27 @@ func wait_signal(params: Dictionary) -> Dictionary:
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
|
|
81
|
-
## Waits until a property reads as the value given, or
|
|
82
|
-
## what it read last either way.
|
|
82
|
+
## Waits until a property reads as the value given, or until something under the path says what
|
|
83
|
+
## was given, or the time runs out, and answers with what it read last either way.
|
|
83
84
|
func wait_until(params: Dictionary) -> Dictionary:
|
|
84
85
|
var node_path: String = str(params.get("path", ""))
|
|
85
86
|
var property: String = str(params.get("property", ""))
|
|
87
|
+
var says: String = str(params.get("says", ""))
|
|
86
88
|
var timeout_ms: int = int(params.get("timeout_ms", 5000))
|
|
87
89
|
if timeout_ms < 1 or timeout_ms > CEILING_MSEC:
|
|
88
90
|
return _out_of_range("timeout_ms", timeout_ms, 1, CEILING_MSEC)
|
|
89
|
-
if node_path.is_empty()
|
|
90
|
-
return {"type": "error", "message": "Node path
|
|
91
|
+
if node_path.is_empty():
|
|
92
|
+
return {"type": "error", "message": "Node path required"}
|
|
93
|
+
if _host.get_tree().root.get_node_or_null(node_path) == null:
|
|
94
|
+
return {"type": "error", "message": "Node not found: " + node_path}
|
|
95
|
+
if not says.is_empty():
|
|
96
|
+
return await _wait_until_said(node_path, says, timeout_ms)
|
|
97
|
+
if property.is_empty():
|
|
98
|
+
return {"type": "error", "message": "A property and a value, or says, are required"}
|
|
91
99
|
if not params.has("value"):
|
|
92
100
|
return {"type": "error", "message": "A value to wait for is required"}
|
|
93
101
|
|
|
94
102
|
var node: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
95
|
-
if node == null:
|
|
96
|
-
return {"type": "error", "message": "Node not found: " + node_path}
|
|
97
103
|
|
|
98
104
|
var current: Variant = node.get(property)
|
|
99
105
|
var wanted: Variant = _values.fitted(params["value"], typeof(current))
|
|
@@ -114,6 +120,45 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
|
|
123
|
+
## Waits until something under [param node_path] has [param said] written on it.
|
|
124
|
+
##
|
|
125
|
+
## A screen rather than one node, because a panel following a clock builds its labels again every
|
|
126
|
+
## time it redraws and the engine names those `@Label@1163`. A wait holding one of them is waiting
|
|
127
|
+
## on a node that was freed a frame later, and that is what it answered: the date along the top of a
|
|
128
|
+
## hall could not be waited on at all. What a caller is watching for is a word arriving on a screen,
|
|
129
|
+
## and the screen is the part that stays put.
|
|
130
|
+
func _wait_until_said(node_path: String, said: String, timeout_ms: int) -> Dictionary:
|
|
131
|
+
var started: int = Time.get_ticks_msec()
|
|
132
|
+
var found: bool = _anything_says(node_path, said)
|
|
133
|
+
while not found and Time.get_ticks_msec() - started < timeout_ms:
|
|
134
|
+
await _host.get_tree().process_frame
|
|
135
|
+
found = _anything_says(node_path, said)
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"type": "condition",
|
|
139
|
+
"path": node_path,
|
|
140
|
+
"says": said,
|
|
141
|
+
"met": found,
|
|
142
|
+
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
## Whether anything under [param node_path] says [param said], the node itself included. Hidden
|
|
147
|
+
## nodes count, for the reason a find answers off them: a caller may be waiting for a dialog that
|
|
148
|
+
## is built before it is shown.
|
|
149
|
+
func _anything_says(node_path: String, said: String) -> bool:
|
|
150
|
+
var root: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
151
|
+
if root == null:
|
|
152
|
+
return false
|
|
153
|
+
var pending: Array[Node] = [root]
|
|
154
|
+
while not pending.is_empty():
|
|
155
|
+
var node: Node = pending.pop_back()
|
|
156
|
+
if Queries.said_by(node).containsn(said):
|
|
157
|
+
return true
|
|
158
|
+
pending.append_array(node.get_children(true))
|
|
159
|
+
return false
|
|
160
|
+
|
|
161
|
+
|
|
117
162
|
func _signal_arity(node: Node, signal_name: String) -> int:
|
|
118
163
|
for entry: Dictionary in node.get_signal_list():
|
|
119
164
|
if entry.get("name", "") == signal_name:
|
package/build/index.js
CHANGED
|
@@ -28519,7 +28519,7 @@ var TOOL_SPECS = [
|
|
|
28519
28519
|
},
|
|
28520
28520
|
{
|
|
28521
28521
|
name: "runtime_wait",
|
|
28522
|
-
description: "Lets the running game get on with it and answers when something has happened: a number of frames, a signal,
|
|
28522
|
+
description: "Lets the running game get on with it and answers when something has happened: a number of frames, a signal, a property reaching a value, or words appearing on a screen. Needs the game running with the runtime addon.",
|
|
28523
28523
|
parameters: {
|
|
28524
28524
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28525
28525
|
frames: {
|
|
@@ -28531,6 +28531,11 @@ var TOOL_SPECS = [
|
|
|
28531
28531
|
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
28532
28532
|
property: { type: "string", ops: ["until"], description: "until: the property name." },
|
|
28533
28533
|
value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
|
|
28534
|
+
says: {
|
|
28535
|
+
type: "string",
|
|
28536
|
+
ops: ["until"],
|
|
28537
|
+
description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, hidden nodes included."
|
|
28538
|
+
},
|
|
28534
28539
|
timeoutMs: {
|
|
28535
28540
|
type: "number",
|
|
28536
28541
|
ops: ["signal", "until"],
|
|
@@ -28545,8 +28550,8 @@ var TOOL_SPECS = [
|
|
|
28545
28550
|
requires: ["nodePath", "signal"]
|
|
28546
28551
|
},
|
|
28547
28552
|
until: {
|
|
28548
|
-
summary: "wait for a property to read as a value and answer with what it
|
|
28549
|
-
requires: ["nodePath"
|
|
28553
|
+
summary: "wait for a property to read as a value, or for words to appear under a node, and answer with what it found",
|
|
28554
|
+
requires: ["nodePath"]
|
|
28550
28555
|
}
|
|
28551
28556
|
}
|
|
28552
28557
|
},
|
|
@@ -30323,6 +30328,7 @@ class GodotServer {
|
|
|
30323
30328
|
path: nodePath,
|
|
30324
30329
|
property: readString(args, "property") ?? "",
|
|
30325
30330
|
value: args["value"],
|
|
30331
|
+
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
30326
30332
|
timeout_ms: timeoutMs
|
|
30327
30333
|
}, patience);
|
|
30328
30334
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|