gdharness 1.0.24 → 1.0.26
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
|
@@ -16489,7 +16489,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16489
16489
|
property: {
|
|
16490
16490
|
type: "string",
|
|
16491
16491
|
ops: ["property", "find"],
|
|
16492
|
-
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. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
16492
|
+
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. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
16493
16493
|
},
|
|
16494
16494
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
16495
16495
|
includeProperties: {
|
|
@@ -16779,7 +16779,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16779
16779
|
says: {
|
|
16780
16780
|
type: "string",
|
|
16781
16781
|
ops: ["until"],
|
|
16782
|
-
description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, 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, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
|
|
16782
|
+
description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, 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, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took."
|
|
16783
16783
|
},
|
|
16784
16784
|
includeHidden: {
|
|
16785
16785
|
type: "boolean",
|
|
@@ -48,6 +48,22 @@ const MAP_CALLS: Array[String] = [
|
|
|
48
48
|
"get_typed_value_class_name",
|
|
49
49
|
"get_typed_value_script",
|
|
50
50
|
]
|
|
51
|
+
## The calls a path makes on a packed list, which has fewer than a list: measured on 4.7.2, it has no
|
|
52
|
+
## front(), back(), max(), min() or hash().
|
|
53
|
+
const PACKED_CALLS: Array[String] = ["size", "is_empty", "duplicate", "to_byte_array"]
|
|
54
|
+
## The packed lists, which a game keeps names and points in as often as it keeps them in an Array.
|
|
55
|
+
const PACKED: Array[int] = [
|
|
56
|
+
TYPE_PACKED_BYTE_ARRAY,
|
|
57
|
+
TYPE_PACKED_INT32_ARRAY,
|
|
58
|
+
TYPE_PACKED_INT64_ARRAY,
|
|
59
|
+
TYPE_PACKED_FLOAT32_ARRAY,
|
|
60
|
+
TYPE_PACKED_FLOAT64_ARRAY,
|
|
61
|
+
TYPE_PACKED_STRING_ARRAY,
|
|
62
|
+
TYPE_PACKED_VECTOR2_ARRAY,
|
|
63
|
+
TYPE_PACKED_VECTOR3_ARRAY,
|
|
64
|
+
TYPE_PACKED_COLOR_ARRAY,
|
|
65
|
+
TYPE_PACKED_VECTOR4_ARRAY,
|
|
66
|
+
]
|
|
51
67
|
## The calls an empty list has nothing to answer, which the engine reports as an error when asked.
|
|
52
68
|
const NEEDS_AN_ELEMENT: Array[String] = ["front", "back", "pick_random"]
|
|
53
69
|
|
|
@@ -142,7 +158,13 @@ static func object_at(root: Node, node: Node, node_path: String, given: Variant)
|
|
|
142
158
|
## Whether a step can be taken into [param value] at all: an object, a list or a map can be walked
|
|
143
159
|
## into, and a number or a string is where a path ends whether the caller meant it to or not.
|
|
144
160
|
static func _can_hold(value: Variant) -> bool:
|
|
145
|
-
return value is Object or value is Array or value is Dictionary
|
|
161
|
+
return value is Object or value is Array or value is Dictionary or packed(value)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## Whether [param value] is a packed list. Stepped into the way a list is, by index, and a write into
|
|
165
|
+
## one reaches the game's own copy, since packed lists share their storage until one is duplicated.
|
|
166
|
+
static func packed(value: Variant) -> bool:
|
|
167
|
+
return typeof(value) in PACKED
|
|
146
168
|
|
|
147
169
|
|
|
148
170
|
## What is wrong with reading [param named] off [param holder], or "" when nothing is.
|
|
@@ -168,6 +190,10 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
168
190
|
if not list_call.is_empty():
|
|
169
191
|
return list_call in LIST_CALLS and not (items.is_empty() and list_call in NEEDS_AN_ELEMENT)
|
|
170
192
|
return _index_in(named, items.size()) != -1
|
|
193
|
+
if packed(holder):
|
|
194
|
+
if not method_of(named).is_empty():
|
|
195
|
+
return method_of(named) in PACKED_CALLS
|
|
196
|
+
return _index_in(named, len(holder)) != -1
|
|
171
197
|
if holder is Dictionary:
|
|
172
198
|
var map: Dictionary = holder
|
|
173
199
|
if not method_of(named).is_empty():
|
|
@@ -209,6 +235,10 @@ static func write(holder: Variant, named: String, value: Variant) -> void:
|
|
|
209
235
|
var items: Array = holder
|
|
210
236
|
items[_index_in(named, items.size())] = value
|
|
211
237
|
return
|
|
238
|
+
if packed(holder):
|
|
239
|
+
var list: Variant = holder
|
|
240
|
+
list[_index_in(named, len(holder))] = value
|
|
241
|
+
return
|
|
212
242
|
if holder is Dictionary:
|
|
213
243
|
var map: Dictionary = holder
|
|
214
244
|
# Written back under the key it was found under, since a map keyed by StringName and one
|
|
@@ -225,11 +255,14 @@ static func write(holder: Variant, named: String, value: Variant) -> void:
|
|
|
225
255
|
## What [param holder] holds under [param named], or answers to it when [param named] is a call.
|
|
226
256
|
## Only ever called once [method can_read] agrees.
|
|
227
257
|
static func read_under(holder: Variant, named: String) -> Variant:
|
|
228
|
-
if (holder is Array or holder is Dictionary) and not method_of(named).is_empty():
|
|
258
|
+
if (holder is Array or holder is Dictionary or packed(holder)) and not method_of(named).is_empty():
|
|
229
259
|
return Callable.create(holder, method_of(named)).call()
|
|
230
260
|
if holder is Array:
|
|
231
261
|
var items: Array = holder
|
|
232
262
|
return items[_index_in(named, items.size())]
|
|
263
|
+
if packed(holder):
|
|
264
|
+
var list: Variant = holder
|
|
265
|
+
return list[_index_in(named, len(holder))]
|
|
233
266
|
if holder is Dictionary:
|
|
234
267
|
var map: Dictionary = holder
|
|
235
268
|
if map.has(named):
|
|
@@ -269,6 +302,10 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
269
302
|
if not container_call.is_empty():
|
|
270
303
|
return container_calls_refused(called_as, "a list", container_call)
|
|
271
304
|
return "%s is a list of %d, so there is no %s in it" % [called_as, items.size(), named]
|
|
305
|
+
if packed(holder):
|
|
306
|
+
if not container_call.is_empty():
|
|
307
|
+
return container_calls_refused(called_as, "a packed list", container_call)
|
|
308
|
+
return "%s is a packed list of %d, so there is no %s in it" % [called_as, len(holder), named]
|
|
272
309
|
if holder is Dictionary:
|
|
273
310
|
if not container_call.is_empty():
|
|
274
311
|
return container_calls_refused(called_as, "a map", container_call)
|
|
@@ -303,10 +340,14 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
303
340
|
return "%s has no property %s" % [called_as, named]
|
|
304
341
|
|
|
305
342
|
|
|
306
|
-
## Why [param method] is not a call a path makes on [param sort] ("a list"
|
|
307
|
-
## the calls it does make.
|
|
343
|
+
## Why [param method] is not a call a path makes on [param sort] ("a list", "a packed list" or "a
|
|
344
|
+
## map"), naming the calls it does make.
|
|
308
345
|
static func container_calls_refused(called_as: String, sort: String, method: String) -> String:
|
|
309
|
-
var calls: Array[String] =
|
|
346
|
+
var calls: Array[String] = MAP_CALLS
|
|
347
|
+
if sort == "a list":
|
|
348
|
+
calls = LIST_CALLS
|
|
349
|
+
elif sort == "a packed list":
|
|
350
|
+
calls = PACKED_CALLS
|
|
310
351
|
var spelled: Array[String] = []
|
|
311
352
|
for each: String in calls:
|
|
312
353
|
spelled.append(each + "()")
|
|
@@ -379,11 +379,17 @@ static func _drawn(node: Node) -> bool:
|
|
|
379
379
|
## Public because three questions are the same question: what a screen reads as, which nodes say a
|
|
380
380
|
## given word, and whether anything has come to say it yet. Two copies of what a node says is how
|
|
381
381
|
## the three of them come to disagree about a SpinBox.
|
|
382
|
+
##
|
|
383
|
+
## Read rather than looked up in the property list, which the engine builds afresh on every call: a
|
|
384
|
+
## wait asks this of every node on the screen every frame, and on a hall of 3,500 nodes the lookup
|
|
385
|
+
## took the game it was watching from 60 frames a second to 11. A node without the property reads
|
|
386
|
+
## as null, which is not a string.
|
|
382
387
|
static func said_by(node: Node) -> String:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
388
|
+
var text: Variant = node.get("text")
|
|
389
|
+
if not text is String:
|
|
390
|
+
return ""
|
|
391
|
+
var words: String = text
|
|
392
|
+
return words.strip_edges()
|
|
387
393
|
|
|
388
394
|
|
|
389
395
|
func get_rect(params: Dictionary) -> Dictionary:
|
|
@@ -699,7 +705,11 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
699
705
|
# rather than as "has no method", which reads as a misspelling of a method that was never going
|
|
700
706
|
# to be there, and with the element spelled out, since a method on what it holds is the usual aim.
|
|
701
707
|
if not reached["holder"] is Object:
|
|
702
|
-
var sort: String = "a list"
|
|
708
|
+
var sort: String = "a list"
|
|
709
|
+
if reached["holder"] is Dictionary:
|
|
710
|
+
sort = "a map"
|
|
711
|
+
elif Paths.packed(reached["holder"]):
|
|
712
|
+
sort = "a packed list"
|
|
703
713
|
var spelled: String = str(reached["name"])
|
|
704
714
|
if Paths.method_of(spelled).is_empty():
|
|
705
715
|
spelled += "()"
|
|
@@ -15,6 +15,10 @@ const CEILING_MSEC: int = 120000
|
|
|
15
15
|
## The most frames one wait may cover, which is about half a minute of a game drawing slowly.
|
|
16
16
|
const MOST_FRAMES: int = 600
|
|
17
17
|
|
|
18
|
+
## How long a wait for words leaves between looks, as a multiple of what the last look took: nine
|
|
19
|
+
## gives looking about a tenth of the time.
|
|
20
|
+
const LOOKS_APART: int = 9
|
|
21
|
+
|
|
18
22
|
var _host: Node
|
|
19
23
|
var _values: Values
|
|
20
24
|
|
|
@@ -140,6 +144,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
140
144
|
return {"type": "error", "message": refused}
|
|
141
145
|
|
|
142
146
|
var started: int = Time.get_ticks_msec()
|
|
147
|
+
var frames: int = 0
|
|
143
148
|
# The type is checked every time round, not only at the top: a property that holds an object a
|
|
144
149
|
# frame later is the same error arriving late.
|
|
145
150
|
while (
|
|
@@ -148,6 +153,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
148
153
|
and Time.get_ticks_msec() - started < timeout_ms
|
|
149
154
|
):
|
|
150
155
|
await _host.get_tree().process_frame
|
|
156
|
+
frames += 1
|
|
151
157
|
if not is_instance_valid(node):
|
|
152
158
|
return {"type": "error", "message": "%s was freed while waiting" % node_path}
|
|
153
159
|
watched = _watched(node, node_path, property)
|
|
@@ -162,6 +168,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
162
168
|
"met": Values.comparable(current, wanted) and current == wanted,
|
|
163
169
|
"value": _values.serialize(current),
|
|
164
170
|
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
171
|
+
"frames": frames,
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
|
|
@@ -232,13 +239,36 @@ static func _watched(node: Node, node_path: String, property: String) -> Diction
|
|
|
232
239
|
## answered met in two milliseconds while the sweep was still animating, and every wait on that
|
|
233
240
|
## screen fell back to counting frames. [param include_hidden] asks the other question, for a
|
|
234
241
|
## caller waiting on words a hidden node holds.
|
|
242
|
+
##
|
|
243
|
+
## Paced by what a look costs, so the watching leaves the game its speed: every frame while a look is
|
|
244
|
+
## cheap, and on a screen large enough for one to cost a real share of a frame, often enough that
|
|
245
|
+
## looking takes about a tenth of the time. Every frame on a hall of 3,500 nodes held the game at a
|
|
246
|
+
## sixth of its frame rate, and its clock, which advances by the frame's delta, lost three quarters
|
|
247
|
+
## of its time, so a wait for a date timed out on a clock that turned a day every thirty seconds.
|
|
248
|
+
## The answer says how many frames the wait spanned and how many looks it took, which is how a
|
|
249
|
+
## caller sees what it cost.
|
|
235
250
|
func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_hidden: bool) -> Dictionary:
|
|
236
251
|
var started: int = Time.get_ticks_msec()
|
|
237
252
|
var words: String = Queries.as_said(said)
|
|
238
|
-
var
|
|
239
|
-
|
|
253
|
+
var frames: int = 0
|
|
254
|
+
var looks: int = 0
|
|
255
|
+
var next_look: int = 0
|
|
256
|
+
var found: bool = false
|
|
257
|
+
while true:
|
|
258
|
+
if Time.get_ticks_usec() >= next_look:
|
|
259
|
+
var looking: int = Time.get_ticks_usec()
|
|
260
|
+
found = _anything_says(node_path, words, include_hidden)
|
|
261
|
+
looks += 1
|
|
262
|
+
next_look = Time.get_ticks_usec() + (Time.get_ticks_usec() - looking) * LOOKS_APART
|
|
263
|
+
if found or Time.get_ticks_msec() - started >= timeout_ms:
|
|
264
|
+
break
|
|
240
265
|
await _host.get_tree().process_frame
|
|
266
|
+
frames += 1
|
|
267
|
+
# One more look at the end, so words that arrived between two paced looks are not answered as
|
|
268
|
+
# never having come.
|
|
269
|
+
if not found:
|
|
241
270
|
found = _anything_says(node_path, words, include_hidden)
|
|
271
|
+
looks += 1
|
|
242
272
|
|
|
243
273
|
return {
|
|
244
274
|
"type": "condition",
|
|
@@ -247,6 +277,8 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
|
|
|
247
277
|
"met": found,
|
|
248
278
|
"include_hidden": include_hidden,
|
|
249
279
|
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
280
|
+
"frames": frames,
|
|
281
|
+
"looks": looks,
|
|
250
282
|
}
|
|
251
283
|
|
|
252
284
|
|
package/build/index.js
CHANGED
|
@@ -30496,7 +30496,7 @@ var TOOL_SPECS = [
|
|
|
30496
30496
|
property: {
|
|
30497
30497
|
type: "string",
|
|
30498
30498
|
ops: ["property", "find"],
|
|
30499
|
-
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. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
30499
|
+
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. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
30500
30500
|
},
|
|
30501
30501
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
30502
30502
|
includeProperties: {
|
|
@@ -30786,7 +30786,7 @@ var TOOL_SPECS = [
|
|
|
30786
30786
|
says: {
|
|
30787
30787
|
type: "string",
|
|
30788
30788
|
ops: ["until"],
|
|
30789
|
-
description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, 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, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
|
|
30789
|
+
description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, 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, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took."
|
|
30790
30790
|
},
|
|
30791
30791
|
includeHidden: {
|
|
30792
30792
|
type: "boolean",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.26",
|
|
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",
|