gdharness 1.2.4 → 1.2.5

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
@@ -18898,6 +18898,12 @@ var init_tool_definitions = __esm(() => {
18898
18898
  ops: ["until"],
18899
18899
  description: "until: the value to wait for, fitted to the property's type and compared at the precision the property keeps, so -1.1 meets a 32-bit float holding -1.10000002. A value the property would have to cut is refused: a fraction on a whole number, a number other than 0 or 1 on a bool. On a property holding an object, whether declared to or untyped and holding one or null, the text null means null. One declared to hold an object refuses any value other than null, since it can never hold it."
18900
18900
  },
18901
+ compare: {
18902
+ type: "string",
18903
+ enum: ["==", "!=", ">=", "<=", ">", "<"],
18904
+ ops: ["until"],
18905
+ description: "until, with a property: how what the property reads stands to value before the wait is met. Default ==. != waits for it to read as anything else. >=, <=, > and < put a number in order, met on the first look past the value or level with it at the precision the property keeps, which is how a counter that steps rather than lands is waited on: a day counter on a fast clock went from 31 to 34 between two looks, and a wait for 32 ran out. For these the value is the number it is rather than fitted to the property, so a fraction on a whole-number property is a threshold rather than refused, and a property holding anything but a number is refused."
18906
+ },
18901
18907
  says: {
18902
18908
  type: "string",
18903
18909
  ops: ["until"],
@@ -48698,6 +48704,7 @@ ${staleNote}`}`
48698
48704
  path: nodePath,
48699
48705
  property: readString(args, "property") ?? "",
48700
48706
  value: args["value"],
48707
+ ...readString(args, "compare") === undefined ? {} : { compare: readString(args, "compare") },
48701
48708
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
48702
48709
  ...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
48703
48710
  timeout_ms: timeoutMs
@@ -21,6 +21,11 @@ const MOST_FRAMES: int = 600
21
21
  ## gives looking about a tenth of the time.
22
22
  const LOOKS_APART: int = 9
23
23
 
24
+ ## How a property wait compares what it reads with the value: equality by default, and order for a
25
+ ## number that steps rather than lands, as a day counter on a fast clock goes from 31 to 34 between
26
+ ## two looks and a wait for 32 never sees it.
27
+ const COMPARES: Array[String] = ["==", "!=", ">=", "<=", ">", "<"]
28
+
24
29
  var _host: Node
25
30
  var _values: Values
26
31
 
@@ -107,8 +112,10 @@ func wait_until(params: Dictionary) -> Dictionary:
107
112
  # path, a property compares one value on one node. Taking says and dropping the other left a
108
113
  # caller watching a screen while believing they were watching a property, and the answer says
109
114
  # which of them it is about only if you already know that says wins.
110
- if not says.is_empty() and (not property.is_empty() or params.has("value")):
111
- var other: String = property if not property.is_empty() else "value"
115
+ if not says.is_empty() and (not property.is_empty() or params.has("value") or params.has("compare")):
116
+ var other: String = (
117
+ property if not property.is_empty() else ("value" if params.has("value") else "compare")
118
+ )
112
119
  return {
113
120
  "type": "error",
114
121
  "message":
@@ -123,10 +130,10 @@ func wait_until(params: Dictionary) -> Dictionary:
123
130
  if not says.is_empty():
124
131
  var include_hidden: bool = Read.as_bool(params.get("include_hidden", false))
125
132
  return await _wait_until_said(node_path, says, timeout_ms, include_hidden)
126
- if property.is_empty():
127
- return {"type": "error", "message": "A property and a value, or says, are required"}
128
- if not params.has("value"):
129
- return {"type": "error", "message": "A value to wait for is required"}
133
+ var unasked: String = _property_wait_unasked(property, params)
134
+ if not unasked.is_empty():
135
+ return {"type": "error", "message": unasked}
136
+ var compare: String = str(params.get("compare", "=="))
130
137
 
131
138
  var node: Node = standing["node"]
132
139
 
@@ -139,6 +146,8 @@ func wait_until(params: Dictionary) -> Dictionary:
139
146
  return watched
140
147
 
141
148
  var current: Variant = watched["value"]
149
+ if compare in [">=", "<=", ">", "<"]:
150
+ return await _wait_in_order(node, params, current, timeout_ms)
142
151
  var wanted: Variant = _values.fitted(params["value"], typeof(current))
143
152
  # Read against what the property is declared as, not only against what it holds this frame: an
144
153
  # object property holding null holds nothing typed, so any value was taken as it came and compared
@@ -173,9 +182,10 @@ func wait_until(params: Dictionary) -> Dictionary:
173
182
  # At the precision the property keeps, as set compares: a float arrives as 64 bits and most
174
183
  # engine properties keep 32, so h_offset waited on as -1.1 read -1.10000002384186 every frame and
175
184
  # the wait ran out on a value the game had reached.
185
+ var differs: bool = compare == "!="
176
186
  while (
177
187
  Values.comparable(current, wanted)
178
- and not Changes.held_already(wanted, current)
188
+ and Changes.held_already(wanted, current) == differs
179
189
  and Time.get_ticks_msec() - started < timeout_ms
180
190
  ):
181
191
  await _host.get_tree().process_frame
@@ -191,13 +201,111 @@ func wait_until(params: Dictionary) -> Dictionary:
191
201
  "type": "condition",
192
202
  "path": node_path,
193
203
  "property": property,
194
- "met": Changes.held_already(wanted, current),
204
+ "compare": compare,
205
+ "met": Values.comparable(current, wanted) and Changes.held_already(wanted, current) != differs,
195
206
  "value": _values.serialize(current),
196
207
  "elapsed_ms": Time.get_ticks_msec() - started,
197
208
  "frames": frames,
198
209
  }
199
210
 
200
211
 
212
+ ## What a property wait is missing or was given wrongly, or "" when it can go ahead.
213
+ static func _property_wait_unasked(property: String, params: Dictionary) -> String:
214
+ if property.is_empty():
215
+ return "A property and a value, or says, are required"
216
+ if not params.has("value"):
217
+ return "A value to wait for is required"
218
+ var compare: String = str(params.get("compare", "=="))
219
+ if not COMPARES.has(compare):
220
+ return 'compare is one of %s, not "%s"' % [", ".join(COMPARES), compare]
221
+ return ""
222
+
223
+
224
+ ## [method wait_until] for a number compared by order: met on the first look at which the property
225
+ ## is past the value, or level with it at the precision the property keeps, as `==` compares.
226
+ ##
227
+ ## The value is read as the number it is rather than fitted to the property's type, since fitting
228
+ ## truncates: 31.5 on a whole-number property became 31, and a wait for "at least 31.5" was met at 31.
229
+ func _wait_in_order(node: Node, params: Dictionary, current: Variant, timeout_ms: int) -> Dictionary:
230
+ var node_path: String = str(params.get("path", ""))
231
+ var property: String = str(params.get("property", ""))
232
+ var compare: String = str(params.get("compare", ""))
233
+ var given: Variant = params["value"]
234
+ # The property first: a string waited on in order is refused for being a string, whatever value
235
+ # came with it, since no number would make the wait answerable.
236
+ var unordered: String = _no_order(current, node_path, property, compare)
237
+ if not unordered.is_empty():
238
+ return {"type": "error", "message": unordered}
239
+ var number: Variant = given
240
+ if given is String:
241
+ var text: String = given
242
+ number = Read.json_or_null(text)
243
+ if typeof(number) != TYPE_INT and typeof(number) != TYPE_FLOAT:
244
+ return {
245
+ "type": "error",
246
+ "message":
247
+ (
248
+ "compare %s puts a number in order, and the value to wait for is %s: give a number."
249
+ % [compare, str(given)]
250
+ )
251
+ }
252
+ var limit: float = Read.as_float(number)
253
+
254
+ var started: int = Time.get_ticks_msec()
255
+ var frames: int = 0
256
+ while not _in_order(compare, limit, current) and Time.get_ticks_msec() - started < timeout_ms:
257
+ await _host.get_tree().process_frame
258
+ frames += 1
259
+ if not is_instance_valid(node):
260
+ return {"type": "error", "message": "%s was freed while waiting" % node_path}
261
+ var watched: Dictionary = _watched(node, node_path, property)
262
+ if watched.has("message"):
263
+ return watched
264
+ current = watched["value"]
265
+ unordered = _no_order(current, node_path, property, compare)
266
+ if not unordered.is_empty():
267
+ return {"type": "error", "message": unordered}
268
+
269
+ return {
270
+ "type": "condition",
271
+ "path": node_path,
272
+ "property": property,
273
+ "compare": compare,
274
+ "met": _in_order(compare, limit, current),
275
+ "value": _values.serialize(current),
276
+ "elapsed_ms": Time.get_ticks_msec() - started,
277
+ "frames": frames,
278
+ }
279
+
280
+
281
+ ## Why [param current] cannot be put in order, or "" when it is a number.
282
+ static func _no_order(current: Variant, node_path: String, property: String, compare: String) -> String:
283
+ if typeof(current) == TYPE_INT or typeof(current) == TYPE_FLOAT:
284
+ return ""
285
+ return (
286
+ "%s.%s holds %s, which has no order, so compare %s cannot be asked of it: use == or !=."
287
+ % [node_path, property, type_string(typeof(current)), compare]
288
+ )
289
+
290
+
291
+ ## Whether [param current], a number, stands to [param limit] as [param compare] says. Level counts
292
+ ## at the precision the property keeps, so a 32-bit float holding 0.30000001 is at least 0.3.
293
+ static func _in_order(compare: String, limit: float, current: Variant) -> bool:
294
+ var reading: float = Read.as_float(current)
295
+ var level: bool = (
296
+ reading == limit or (typeof(current) == TYPE_FLOAT and Changes.held_already(limit, current))
297
+ )
298
+ match compare:
299
+ ">=":
300
+ return level or reading > limit
301
+ "<=":
302
+ return level or reading < limit
303
+ ">":
304
+ return not level and reading > limit
305
+ _:
306
+ return not level and reading < limit
307
+
308
+
201
309
  ## Why [param given] would have to be cut to be waited on a whole number or a truth, or "" when not.
202
310
  ##
203
311
  ## A number arrives from JSON as a float, so one is converted to what the property holds, and the
package/build/index.js CHANGED
@@ -35025,6 +35025,12 @@ var TOOL_SPECS = [
35025
35025
  ops: ["until"],
35026
35026
  description: "until: the value to wait for, fitted to the property's type and compared at the precision the property keeps, so -1.1 meets a 32-bit float holding -1.10000002. A value the property would have to cut is refused: a fraction on a whole number, a number other than 0 or 1 on a bool. On a property holding an object, whether declared to or untyped and holding one or null, the text null means null. One declared to hold an object refuses any value other than null, since it can never hold it."
35027
35027
  },
35028
+ compare: {
35029
+ type: "string",
35030
+ enum: ["==", "!=", ">=", "<=", ">", "<"],
35031
+ ops: ["until"],
35032
+ description: "until, with a property: how what the property reads stands to value before the wait is met. Default ==. != waits for it to read as anything else. >=, <=, > and < put a number in order, met on the first look past the value or level with it at the precision the property keeps, which is how a counter that steps rather than lands is waited on: a day counter on a fast clock went from 31 to 34 between two looks, and a wait for 32 ran out. For these the value is the number it is rather than fitted to the property, so a fraction on a whole-number property is a threshold rather than refused, and a property holding anything but a number is refused."
35033
+ },
35028
35034
  says: {
35029
35035
  type: "string",
35030
35036
  ops: ["until"],
@@ -41195,6 +41201,7 @@ ${staleNote}`}`
41195
41201
  path: nodePath,
41196
41202
  property: readString(args, "property") ?? "",
41197
41203
  value: args["value"],
41204
+ ...readString(args, "compare") === undefined ? {} : { compare: readString(args, "compare") },
41198
41205
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
41199
41206
  ...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
41200
41207
  timeout_ms: timeoutMs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
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",