gdharness 0.7.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
|
@@ -15783,6 +15783,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
15783
15783
|
description: 'find: a case-insensitive glob on the node name, such as "Enemy*".'
|
|
15784
15784
|
},
|
|
15785
15785
|
group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
|
|
15786
|
+
says: {
|
|
15787
|
+
type: "string",
|
|
15788
|
+
ops: ["find"],
|
|
15789
|
+
description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match."
|
|
15790
|
+
},
|
|
15786
15791
|
limit: {
|
|
15787
15792
|
type: "number",
|
|
15788
15793
|
ops: ["find", "text"],
|
|
@@ -15808,7 +15813,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15808
15813
|
requires: []
|
|
15809
15814
|
},
|
|
15810
15815
|
find: {
|
|
15811
|
-
summary: "the paths of every node matching className, script, namePattern or
|
|
15816
|
+
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each",
|
|
15812
15817
|
requires: []
|
|
15813
15818
|
},
|
|
15814
15819
|
rect: {
|
|
@@ -15874,7 +15879,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
15874
15879
|
ops: ["click", "choose"],
|
|
15875
15880
|
description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
|
|
15876
15881
|
},
|
|
15877
|
-
action: {
|
|
15882
|
+
action: {
|
|
15883
|
+
type: "string",
|
|
15884
|
+
ops: ["action"],
|
|
15885
|
+
description: "action: the InputMap action name. An engine dialog is not answered this way: AcceptDialog reads the Escape key itself and never asks the InputMap, so ui_cancel goes in and the question stays up. Dismiss one with key Escape, or click its button."
|
|
15886
|
+
},
|
|
15878
15887
|
pressed: {
|
|
15879
15888
|
type: "boolean",
|
|
15880
15889
|
ops: ["action", "key", "mouse_click"],
|
|
@@ -15957,7 +15966,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15957
15966
|
},
|
|
15958
15967
|
{
|
|
15959
15968
|
name: "runtime_wait",
|
|
15960
|
-
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.",
|
|
15961
15970
|
parameters: {
|
|
15962
15971
|
projectPath: RUNNING_PROJECT_PATH,
|
|
15963
15972
|
frames: {
|
|
@@ -15969,6 +15978,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
15969
15978
|
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
15970
15979
|
property: { type: "string", ops: ["until"], description: "until: the property name." },
|
|
15971
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
|
+
},
|
|
15972
15986
|
timeoutMs: {
|
|
15973
15987
|
type: "number",
|
|
15974
15988
|
ops: ["signal", "until"],
|
|
@@ -15983,8 +15997,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
15983
15997
|
requires: ["nodePath", "signal"]
|
|
15984
15998
|
},
|
|
15985
15999
|
until: {
|
|
15986
|
-
summary: "wait for a property to read as a value and answer with what it
|
|
15987
|
-
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"]
|
|
15988
16002
|
}
|
|
15989
16003
|
}
|
|
15990
16004
|
},
|
|
@@ -37491,6 +37505,7 @@ class GodotServer {
|
|
|
37491
37505
|
const script = readNonEmptyString(args, "script");
|
|
37492
37506
|
const namePattern = readNonEmptyString(args, "namePattern");
|
|
37493
37507
|
const group = readNonEmptyString(args, "group");
|
|
37508
|
+
const says = readNonEmptyString(args, "says");
|
|
37494
37509
|
if (className !== undefined)
|
|
37495
37510
|
filters["class"] = className;
|
|
37496
37511
|
if (script !== undefined)
|
|
@@ -37499,8 +37514,10 @@ class GodotServer {
|
|
|
37499
37514
|
filters["name"] = namePattern;
|
|
37500
37515
|
if (group !== undefined)
|
|
37501
37516
|
filters["group"] = group;
|
|
37517
|
+
if (says !== undefined)
|
|
37518
|
+
filters["says"] = says;
|
|
37502
37519
|
if (Object.keys(filters).length === 0) {
|
|
37503
|
-
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group.");
|
|
37520
|
+
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group, says.");
|
|
37504
37521
|
}
|
|
37505
37522
|
const property = readNonEmptyString(args, "property");
|
|
37506
37523
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
@@ -37530,6 +37547,7 @@ class GodotServer {
|
|
|
37530
37547
|
path: nodePath,
|
|
37531
37548
|
property: readString(args, "property") ?? "",
|
|
37532
37549
|
value: args["value"],
|
|
37550
|
+
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
37533
37551
|
timeout_ms: timeoutMs
|
|
37534
37552
|
}, patience);
|
|
37535
37553
|
}
|
|
@@ -38905,15 +38923,17 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
38905
38923
|
| Ask | Tool |
|
|
38906
38924
|
| --- | --- |
|
|
38907
38925
|
| What a screen says | \`runtime_inspect text\`, which reads what is drawn and leaves out what is hidden |
|
|
38926
|
+
| Find a control by the word on it | \`runtime_inspect find\` with \`says\`, rather than listing a screen and reading each one |
|
|
38908
38927
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
38909
38928
|
| What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
38910
38929
|
| Press a button | \`runtime_input click\`, which says what was under the pointer |
|
|
38911
38930
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
38912
38931
|
| Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
|
|
38913
38932
|
| Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says |
|
|
38914
|
-
| Answer a dialog, or
|
|
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 |
|
|
38934
|
+
| Press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
|
|
38915
38935
|
| Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
|
|
38916
|
-
| 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 |
|
|
38917
38937
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
38918
38938
|
|
|
38919
38939
|
These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
|
|
@@ -10,6 +10,10 @@ const Values = preload("runtime_values.gd")
|
|
|
10
10
|
const FIND_LIMIT: int = 100
|
|
11
11
|
const FIND_LIMIT_CEILING: int = 1000
|
|
12
12
|
|
|
13
|
+
## What a find can be narrowed by, which is also what it refuses to answer without. Named once so
|
|
14
|
+
## the refusal lists the same set the walk reads.
|
|
15
|
+
const FILTERS: PackedStringArray = ["class", "script", "name", "group", "says"]
|
|
16
|
+
|
|
13
17
|
## The most lines one read answers with, unless asked for fewer. A screen is a few dozen; a
|
|
14
18
|
## thousand is a tree somebody pointed this at by mistake.
|
|
15
19
|
const READ_LIMIT: int = 500
|
|
@@ -39,7 +43,8 @@ func get_tree(params: Dictionary) -> Dictionary:
|
|
|
39
43
|
|
|
40
44
|
## Nodes matching every filter given, as paths, so a caller can name what it wants without
|
|
41
45
|
## reading the whole tree to find it. `class` matches native classes and their subclasses, and
|
|
42
|
-
## the global name of a script class; `name` is a case-insensitive glob; `script` is a path
|
|
46
|
+
## the global name of a script class; `name` is a case-insensitive glob; `script` is a path;
|
|
47
|
+
## `says` is what the node has written on it.
|
|
43
48
|
##
|
|
44
49
|
## `property` names one to read off each of them, which is the difference between one question and
|
|
45
50
|
## one call per answer. A panel of a dozen labels took thirteen calls to read, and a tree that
|
|
@@ -48,22 +53,16 @@ func get_tree(params: Dictionary) -> Dictionary:
|
|
|
48
53
|
## because null is what a node holding null answers.
|
|
49
54
|
func find_nodes(params: Dictionary) -> Dictionary:
|
|
50
55
|
var root_path: String = str(params.get("root", "/root"))
|
|
51
|
-
var
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
var wanted_group: String = str(params.get("group", ""))
|
|
56
|
+
var wanted: Dictionary[String, String] = {}
|
|
57
|
+
for filter: String in FILTERS:
|
|
58
|
+
wanted[filter] = str(params.get(filter, ""))
|
|
55
59
|
var wanted_property: String = str(params.get("property", ""))
|
|
56
60
|
var limit: int = clampi(int(params.get("limit", FIND_LIMIT)), 1, FIND_LIMIT_CEILING)
|
|
57
61
|
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
and wanted_group.is_empty()
|
|
63
|
-
):
|
|
64
|
-
return {"type": "error", "message": "find_nodes needs at least one of class, script, name, group"}
|
|
65
|
-
if not wanted_script.is_empty() and not wanted_script.begins_with("res://"):
|
|
66
|
-
wanted_script = "res://" + wanted_script
|
|
62
|
+
if not _anything_asked(wanted):
|
|
63
|
+
return {"type": "error", "message": "find_nodes needs at least one of " + ", ".join(FILTERS)}
|
|
64
|
+
if not wanted["script"].is_empty() and not wanted["script"].begins_with("res://"):
|
|
65
|
+
wanted["script"] = "res://" + wanted["script"]
|
|
67
66
|
|
|
68
67
|
var root: Node = _host.get_tree().root.get_node_or_null(root_path)
|
|
69
68
|
if root == null:
|
|
@@ -74,7 +73,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
74
73
|
var truncated: bool = false
|
|
75
74
|
while not pending.is_empty():
|
|
76
75
|
var node: Node = pending.pop_front()
|
|
77
|
-
if _matches(node,
|
|
76
|
+
if _matches(node, wanted):
|
|
78
77
|
if found.size() >= limit:
|
|
79
78
|
truncated = true
|
|
80
79
|
break
|
|
@@ -115,25 +114,37 @@ func _found(node: Node, wanted_property: String) -> Dictionary:
|
|
|
115
114
|
return entry
|
|
116
115
|
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
) -> bool:
|
|
121
|
-
|
|
117
|
+
## Whether the caller named anything to match on, since every filter left out matches everything
|
|
118
|
+
## and a find with none of them is the whole tree by another name.
|
|
119
|
+
static func _anything_asked(wanted: Dictionary[String, String]) -> bool:
|
|
120
|
+
for filter: String in wanted:
|
|
121
|
+
if not wanted[filter].is_empty():
|
|
122
|
+
return true
|
|
123
|
+
return false
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
func _matches(node: Node, wanted: Dictionary[String, String]) -> bool:
|
|
127
|
+
if not wanted["group"].is_empty() and not node.is_in_group(wanted["group"]):
|
|
122
128
|
return false
|
|
123
|
-
if not
|
|
129
|
+
if not wanted["name"].is_empty() and not str(node.name).matchn(wanted["name"]):
|
|
130
|
+
return false
|
|
131
|
+
# What this node says, rather than everything said underneath it. A row is then found by the
|
|
132
|
+
# label in it, and the path answered is that label's, which is where the words a caller is
|
|
133
|
+
# looking at actually are: matching every container above it would answer with the screen.
|
|
134
|
+
if not wanted["says"].is_empty() and not said_by(node).containsn(wanted["says"]):
|
|
124
135
|
return false
|
|
125
136
|
var script: Variant = node.get_script()
|
|
126
|
-
if not
|
|
137
|
+
if not wanted["script"].is_empty():
|
|
127
138
|
if not script is Script:
|
|
128
139
|
return false
|
|
129
140
|
var attached: Script = script
|
|
130
|
-
if attached.resource_path !=
|
|
141
|
+
if attached.resource_path != wanted["script"]:
|
|
131
142
|
return false
|
|
132
|
-
if not
|
|
143
|
+
if not wanted["class"].is_empty() and not node.is_class(wanted["class"]):
|
|
133
144
|
if not script is Script:
|
|
134
145
|
return false
|
|
135
146
|
var attached: Script = script
|
|
136
|
-
if attached.get_global_name() !=
|
|
147
|
+
if attached.get_global_name() != wanted["class"]:
|
|
137
148
|
return false
|
|
138
149
|
return true
|
|
139
150
|
|
|
@@ -195,7 +206,7 @@ func _read_into(node: Node, include_hidden: bool, most: int, into: PackedStringA
|
|
|
195
206
|
return
|
|
196
207
|
if not include_hidden and not _drawn(node):
|
|
197
208
|
return
|
|
198
|
-
var said: String =
|
|
209
|
+
var said: String = said_by(node)
|
|
199
210
|
if not said.is_empty():
|
|
200
211
|
into.append(said)
|
|
201
212
|
for child: Node in node.get_children(true):
|
|
@@ -213,7 +224,11 @@ static func _drawn(node: Node) -> bool:
|
|
|
213
224
|
|
|
214
225
|
## What one node says, or "" for a node that says nothing. Anything with a `text` property, which
|
|
215
226
|
## is every label, button and field the interface is built out of.
|
|
216
|
-
|
|
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:
|
|
217
232
|
for property: Dictionary in node.get_property_list():
|
|
218
233
|
if str(property.get("name", "")) == "text" and int(property.get("type", 0)) == TYPE_STRING:
|
|
219
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
|
@@ -28336,6 +28336,11 @@ var TOOL_SPECS = [
|
|
|
28336
28336
|
description: 'find: a case-insensitive glob on the node name, such as "Enemy*".'
|
|
28337
28337
|
},
|
|
28338
28338
|
group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
|
|
28339
|
+
says: {
|
|
28340
|
+
type: "string",
|
|
28341
|
+
ops: ["find"],
|
|
28342
|
+
description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match."
|
|
28343
|
+
},
|
|
28339
28344
|
limit: {
|
|
28340
28345
|
type: "number",
|
|
28341
28346
|
ops: ["find", "text"],
|
|
@@ -28361,7 +28366,7 @@ var TOOL_SPECS = [
|
|
|
28361
28366
|
requires: []
|
|
28362
28367
|
},
|
|
28363
28368
|
find: {
|
|
28364
|
-
summary: "the paths of every node matching className, script, namePattern or
|
|
28369
|
+
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each",
|
|
28365
28370
|
requires: []
|
|
28366
28371
|
},
|
|
28367
28372
|
rect: {
|
|
@@ -28427,7 +28432,11 @@ var TOOL_SPECS = [
|
|
|
28427
28432
|
ops: ["click", "choose"],
|
|
28428
28433
|
description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
|
|
28429
28434
|
},
|
|
28430
|
-
action: {
|
|
28435
|
+
action: {
|
|
28436
|
+
type: "string",
|
|
28437
|
+
ops: ["action"],
|
|
28438
|
+
description: "action: the InputMap action name. An engine dialog is not answered this way: AcceptDialog reads the Escape key itself and never asks the InputMap, so ui_cancel goes in and the question stays up. Dismiss one with key Escape, or click its button."
|
|
28439
|
+
},
|
|
28431
28440
|
pressed: {
|
|
28432
28441
|
type: "boolean",
|
|
28433
28442
|
ops: ["action", "key", "mouse_click"],
|
|
@@ -28510,7 +28519,7 @@ var TOOL_SPECS = [
|
|
|
28510
28519
|
},
|
|
28511
28520
|
{
|
|
28512
28521
|
name: "runtime_wait",
|
|
28513
|
-
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.",
|
|
28514
28523
|
parameters: {
|
|
28515
28524
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28516
28525
|
frames: {
|
|
@@ -28522,6 +28531,11 @@ var TOOL_SPECS = [
|
|
|
28522
28531
|
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
28523
28532
|
property: { type: "string", ops: ["until"], description: "until: the property name." },
|
|
28524
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
|
+
},
|
|
28525
28539
|
timeoutMs: {
|
|
28526
28540
|
type: "number",
|
|
28527
28541
|
ops: ["signal", "until"],
|
|
@@ -28536,8 +28550,8 @@ var TOOL_SPECS = [
|
|
|
28536
28550
|
requires: ["nodePath", "signal"]
|
|
28537
28551
|
},
|
|
28538
28552
|
until: {
|
|
28539
|
-
summary: "wait for a property to read as a value and answer with what it
|
|
28540
|
-
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"]
|
|
28541
28555
|
}
|
|
28542
28556
|
}
|
|
28543
28557
|
},
|
|
@@ -30272,6 +30286,7 @@ class GodotServer {
|
|
|
30272
30286
|
const script = readNonEmptyString(args, "script");
|
|
30273
30287
|
const namePattern = readNonEmptyString(args, "namePattern");
|
|
30274
30288
|
const group = readNonEmptyString(args, "group");
|
|
30289
|
+
const says = readNonEmptyString(args, "says");
|
|
30275
30290
|
if (className !== undefined)
|
|
30276
30291
|
filters["class"] = className;
|
|
30277
30292
|
if (script !== undefined)
|
|
@@ -30280,8 +30295,10 @@ class GodotServer {
|
|
|
30280
30295
|
filters["name"] = namePattern;
|
|
30281
30296
|
if (group !== undefined)
|
|
30282
30297
|
filters["group"] = group;
|
|
30298
|
+
if (says !== undefined)
|
|
30299
|
+
filters["says"] = says;
|
|
30283
30300
|
if (Object.keys(filters).length === 0) {
|
|
30284
|
-
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group.");
|
|
30301
|
+
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group, says.");
|
|
30285
30302
|
}
|
|
30286
30303
|
const property = readNonEmptyString(args, "property");
|
|
30287
30304
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
@@ -30311,6 +30328,7 @@ class GodotServer {
|
|
|
30311
30328
|
path: nodePath,
|
|
30312
30329
|
property: readString(args, "property") ?? "",
|
|
30313
30330
|
value: args["value"],
|
|
30331
|
+
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
30314
30332
|
timeout_ms: timeoutMs
|
|
30315
30333
|
}, patience);
|
|
30316
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",
|