gdharness 1.0.24 → 1.0.25

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
@@ -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",
@@ -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
- for property: Dictionary in node.get_property_list():
384
- if str(property.get("name", "")) == "text" and Read.as_int(property.get("type", 0)) == TYPE_STRING:
385
- return str(node.get("text")).strip_edges()
386
- return ""
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:
@@ -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 found: bool = _anything_says(node_path, words, include_hidden)
239
- while not found and Time.get_ticks_msec() - started < timeout_ms:
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
@@ -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.24",
3
+ "version": "1.0.25",
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",