gdharness 1.0.31 → 1.1.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
|
@@ -16546,7 +16546,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16546
16546
|
says: {
|
|
16547
16547
|
type: "string",
|
|
16548
16548
|
ops: ["find"],
|
|
16549
|
-
description: "find: part of what the node has written on it as drawn, the way the text op reads 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. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
|
|
16549
|
+
description: "find: part of what the node has written on it as drawn, the way the text op reads 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. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
|
|
16550
16550
|
},
|
|
16551
16551
|
limit: {
|
|
16552
16552
|
type: "integer",
|
|
@@ -16575,7 +16575,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16575
16575
|
operations: {
|
|
16576
16576
|
tree: { summary: "the live scene tree", requires: [] },
|
|
16577
16577
|
text: {
|
|
16578
|
-
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. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items read a line each",
|
|
16578
|
+
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. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items and the titles of its separators read a line each",
|
|
16579
16579
|
requires: []
|
|
16580
16580
|
},
|
|
16581
16581
|
find: {
|
|
@@ -16596,7 +16596,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16596
16596
|
},
|
|
16597
16597
|
{
|
|
16598
16598
|
name: "runtime_invoke",
|
|
16599
|
-
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
|
|
16599
|
+
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
|
|
16600
16600
|
parameters: {
|
|
16601
16601
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16602
16602
|
pid: RUNNING_PID,
|
|
@@ -664,7 +664,19 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
664
664
|
"message": "%s has no such item. It holds: %s" % [node_path, ", ".join(_items_of(menu))]
|
|
665
665
|
}
|
|
666
666
|
if menu.is_item_separator(index):
|
|
667
|
-
|
|
667
|
+
var heading: String = Words.item_says(menu, index).strip_edges()
|
|
668
|
+
return {
|
|
669
|
+
"type": "error",
|
|
670
|
+
"message":
|
|
671
|
+
(
|
|
672
|
+
"%s item %d is a separator, not a choice" % [node_path, index]
|
|
673
|
+
if heading.is_empty()
|
|
674
|
+
else (
|
|
675
|
+
"%s item %d, %s, is a separator heading the items below it, not a choice"
|
|
676
|
+
% [node_path, index, heading]
|
|
677
|
+
)
|
|
678
|
+
)
|
|
679
|
+
}
|
|
668
680
|
if menu.is_item_disabled(index):
|
|
669
681
|
return {
|
|
670
682
|
"type": "error",
|
|
@@ -732,26 +744,39 @@ static func _wanted_item(menu: PopupMenu, params: Dictionary) -> int:
|
|
|
732
744
|
var wanted: String = str(params.get("text", ""))
|
|
733
745
|
if wanted.is_empty():
|
|
734
746
|
return -1
|
|
747
|
+
# The items before the separators, because a heading can carry the same words as an item under
|
|
748
|
+
# it, and finding the heading first refused the choice the caller meant. A separator is still
|
|
749
|
+
# looked for after, so a heading asked for by name is refused as one rather than as missing.
|
|
750
|
+
var order: Array[int] = []
|
|
751
|
+
for separators: bool in [false, true]:
|
|
752
|
+
for index: int in menu.get_item_count():
|
|
753
|
+
if menu.is_item_separator(index) == separators:
|
|
754
|
+
order.append(index)
|
|
735
755
|
var shown: Array[String] = []
|
|
736
756
|
var held: Array[String] = []
|
|
737
|
-
for index: int in
|
|
757
|
+
for index: int in order:
|
|
738
758
|
shown.append(Words.item_says(menu, index))
|
|
739
759
|
held.append(menu.get_item_text(index))
|
|
740
760
|
for words: Array[String] in [shown, held]:
|
|
741
|
-
for
|
|
742
|
-
if words[
|
|
743
|
-
return
|
|
744
|
-
for
|
|
745
|
-
if words[
|
|
746
|
-
return
|
|
761
|
+
for at: int in words.size():
|
|
762
|
+
if words[at] == wanted:
|
|
763
|
+
return order[at]
|
|
764
|
+
for at: int in words.size():
|
|
765
|
+
if words[at].nocasecmp_to(wanted) == 0:
|
|
766
|
+
return order[at]
|
|
747
767
|
return -1
|
|
748
768
|
|
|
749
769
|
|
|
750
|
-
## What the menu shows, for a refusal that names the choices rather than the miss
|
|
770
|
+
## What the menu shows, for a refusal that names the choices rather than the miss: a titled
|
|
771
|
+
## separator marked as the heading it is, and one with no title left out, since it shows nothing.
|
|
751
772
|
static func _items_of(menu: PopupMenu) -> Array[String]:
|
|
752
773
|
var said: Array[String] = []
|
|
753
774
|
for index: int in menu.get_item_count():
|
|
754
|
-
|
|
775
|
+
var words: String = Words.item_says(menu, index)
|
|
776
|
+
if not menu.is_item_separator(index):
|
|
777
|
+
said.append("%d: %s" % [index, words])
|
|
778
|
+
elif not words.strip_edges().is_empty():
|
|
779
|
+
said.append("%d: %s (a heading)" % [index, words])
|
|
755
780
|
return said
|
|
756
781
|
|
|
757
782
|
|
|
@@ -104,6 +104,15 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
104
104
|
# find would have matched if the name had been read the way it was probably meant.
|
|
105
105
|
var literal: bool = _is_literal(wanted["name"])
|
|
106
106
|
var nearly: int = 0
|
|
107
|
+
# The same for the words: a glob is matched against the whole text, and one written as a prefix
|
|
108
|
+
# by habit, "Taken together*", missed the label whose sentence those words are in the middle of.
|
|
109
|
+
# Nothing found then read as the words not being on screen, which is the wrong conclusion when
|
|
110
|
+
# checking that a change to the text landed.
|
|
111
|
+
var widened_says: String = _widened(wanted["says"])
|
|
112
|
+
var widened: Dictionary[String, String] = {}
|
|
113
|
+
for filter: String in wanted:
|
|
114
|
+
widened[filter] = widened_says if filter == "says" else wanted[filter]
|
|
115
|
+
var nearly_said: int = 0
|
|
107
116
|
var hidden: int = 0
|
|
108
117
|
while not pending.is_empty():
|
|
109
118
|
var node: Node = pending.pop_front()
|
|
@@ -118,6 +127,12 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
118
127
|
found.append(_found(node, wanted_property))
|
|
119
128
|
elif rest and literal and str(node.name).containsn(wanted["name"]):
|
|
120
129
|
nearly += 1
|
|
130
|
+
elif (
|
|
131
|
+
not widened_says.is_empty()
|
|
132
|
+
and _named(node, wanted["name"])
|
|
133
|
+
and _matches_apart_from_name(node, widened)
|
|
134
|
+
):
|
|
135
|
+
nearly_said += 1
|
|
121
136
|
# Internal children included, which they were not. A ConfirmationDialog builds its Yes and
|
|
122
137
|
# its No as internal nodes, and a ScrollContainer its bars, so a find over a screen for
|
|
123
138
|
# every Button came back without the two buttons the player is being asked to press:
|
|
@@ -141,6 +156,16 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
141
156
|
% [nearly, holding, wanted["name"], wanted["name"]]
|
|
142
157
|
)
|
|
143
158
|
)
|
|
159
|
+
if found.is_empty() and nearly_said > 0:
|
|
160
|
+
(
|
|
161
|
+
notes
|
|
162
|
+
. append(
|
|
163
|
+
(
|
|
164
|
+
'says with a wildcard is matched as a glob against a node\'s whole text; "%s" would find %d node%s'
|
|
165
|
+
% [widened_says, nearly_said, "" if nearly_said == 1 else "s"]
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
)
|
|
144
169
|
# How many the filter took out, so a short answer is not read as a small screen. Left out
|
|
145
170
|
# entirely when nothing was hidden, which keeps a plain answer plain.
|
|
146
171
|
if hidden > 0:
|
|
@@ -234,6 +259,15 @@ static func _says(said: String, wanted: String) -> bool:
|
|
|
234
259
|
return said.containsn(words) if _is_literal(words) else said.matchn(words)
|
|
235
260
|
|
|
236
261
|
|
|
262
|
+
## [param wanted], a glob, open at both ends so it matches its words anywhere in a text; "" when
|
|
263
|
+
## it is not a glob, or already open at both ends, and so has nothing to suggest.
|
|
264
|
+
static func _widened(wanted: String) -> String:
|
|
265
|
+
if wanted.is_empty() or _is_literal(wanted):
|
|
266
|
+
return ""
|
|
267
|
+
var open: String = "*" + wanted.lstrip("*").rstrip("*") + "*"
|
|
268
|
+
return "" if open == wanted else open
|
|
269
|
+
|
|
270
|
+
|
|
237
271
|
## [param wanted] as words on a screen: a backslash followed by n is a line break.
|
|
238
272
|
##
|
|
239
273
|
## A button with two lines on it was asked for with the break written as the two characters, the
|
|
@@ -624,7 +658,10 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
624
658
|
"%s.%s is a typed %s: %s." % [reached["called"], named, _sort_of(old_value), typed["message"]]
|
|
625
659
|
}
|
|
626
660
|
given = typed["value"]
|
|
661
|
+
# The write alone, setter included, which is where a property's cost is.
|
|
662
|
+
var began: int = Time.get_ticks_usec()
|
|
627
663
|
Paths.write(holder, named, given)
|
|
664
|
+
var elapsed: int = Time.get_ticks_usec() - began
|
|
628
665
|
var now: Variant = Paths.read_under(holder, named)
|
|
629
666
|
# Read back rather than trusted. The engine drops a write it will not take without a word, and
|
|
630
667
|
# the answer then showed the old value as the new one, shaped as a success: a typed container
|
|
@@ -658,7 +695,8 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
658
695
|
"path": node_path,
|
|
659
696
|
"property": property,
|
|
660
697
|
"old_value": _values.serialize(old_value),
|
|
661
|
-
"new_value": _values.serialize(now)
|
|
698
|
+
"new_value": _values.serialize(now),
|
|
699
|
+
"elapsed_usec": elapsed
|
|
662
700
|
}
|
|
663
701
|
|
|
664
702
|
|
|
@@ -695,12 +733,15 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
695
733
|
if Paths.method_of(spelled).is_empty():
|
|
696
734
|
spelled += "()"
|
|
697
735
|
if args.is_empty() and Paths.can_read(reached["holder"], spelled):
|
|
736
|
+
var read_from: int = Time.get_ticks_usec()
|
|
698
737
|
var answered: Variant = Paths.read_under(reached["holder"], spelled)
|
|
738
|
+
var read_for: int = Time.get_ticks_usec() - read_from
|
|
699
739
|
return {
|
|
700
740
|
"type": "method_result",
|
|
701
741
|
"path": node_path,
|
|
702
742
|
"method": method,
|
|
703
743
|
"result": _values.serialize(answered),
|
|
744
|
+
"elapsed_usec": read_for,
|
|
704
745
|
}
|
|
705
746
|
var why: String = (
|
|
706
747
|
"%s:%s takes no arguments" % [reached["called"], spelled]
|
|
@@ -772,9 +813,19 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
772
813
|
given = typed["value"]
|
|
773
814
|
deserialized_args.append(given)
|
|
774
815
|
|
|
816
|
+
# Timed around the call alone, since the bridge's round trip is a second or more and swamps what
|
|
817
|
+
# one click's work costs; the frame metrics read whichever frame is last, which is not this one.
|
|
818
|
+
var began: int = Time.get_ticks_usec()
|
|
775
819
|
var result: Variant = holder.callv(named, deserialized_args)
|
|
820
|
+
var elapsed: int = Time.get_ticks_usec() - began
|
|
776
821
|
|
|
777
|
-
return {
|
|
822
|
+
return {
|
|
823
|
+
"type": "method_result",
|
|
824
|
+
"path": node_path,
|
|
825
|
+
"method": method,
|
|
826
|
+
"result": _values.serialize(result),
|
|
827
|
+
"elapsed_usec": elapsed
|
|
828
|
+
}
|
|
778
829
|
|
|
779
830
|
|
|
780
831
|
## [method _object_named] with the path first, which is the order [method Values.typed_like] calls
|
|
@@ -49,10 +49,11 @@ static func lines_said_by(node: Node) -> Array[String]:
|
|
|
49
49
|
for item: int in list.item_count:
|
|
50
50
|
_keep(_as_drawn(node, list.get_item_text(item), list.get_item_auto_translate_mode(item)), lines)
|
|
51
51
|
elif node is PopupMenu:
|
|
52
|
+
# A separator too, by its title: the popup draws a titled one as a heading over the items
|
|
53
|
+
# below it, and one without a title says nothing and is dropped as an empty line.
|
|
52
54
|
var menu: PopupMenu = node
|
|
53
55
|
for item: int in menu.item_count:
|
|
54
|
-
|
|
55
|
-
_keep(item_says(menu, item), lines)
|
|
56
|
+
_keep(item_says(menu, item), lines)
|
|
56
57
|
elif node is Tree:
|
|
57
58
|
var tree: Tree = node
|
|
58
59
|
if tree.column_titles_visible:
|
package/build/index.js
CHANGED
|
@@ -30672,7 +30672,7 @@ var TOOL_SPECS = [
|
|
|
30672
30672
|
says: {
|
|
30673
30673
|
type: "string",
|
|
30674
30674
|
ops: ["find"],
|
|
30675
|
-
description: "find: part of what the node has written on it as drawn, the way the text op reads 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. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
|
|
30675
|
+
description: "find: part of what the node has written on it as drawn, the way the text op reads 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. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
|
|
30676
30676
|
},
|
|
30677
30677
|
limit: {
|
|
30678
30678
|
type: "integer",
|
|
@@ -30701,7 +30701,7 @@ var TOOL_SPECS = [
|
|
|
30701
30701
|
operations: {
|
|
30702
30702
|
tree: { summary: "the live scene tree", requires: [] },
|
|
30703
30703
|
text: {
|
|
30704
|
-
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. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items read a line each",
|
|
30704
|
+
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. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items and the titles of its separators read a line each",
|
|
30705
30705
|
requires: []
|
|
30706
30706
|
},
|
|
30707
30707
|
find: {
|
|
@@ -30722,7 +30722,7 @@ var TOOL_SPECS = [
|
|
|
30722
30722
|
},
|
|
30723
30723
|
{
|
|
30724
30724
|
name: "runtime_invoke",
|
|
30725
|
-
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
|
|
30725
|
+
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
|
|
30726
30726
|
parameters: {
|
|
30727
30727
|
projectPath: RUNNING_PROJECT_PATH,
|
|
30728
30728
|
pid: RUNNING_PID,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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",
|