gdharness 1.2.29 → 1.2.30

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
@@ -12417,7 +12417,7 @@ var init_tool_definitions = __esm(() => {
12417
12417
  value: {
12418
12418
  ops: ["set"],
12419
12419
  blank: true,
12420
- description: `set: the value, fitted to the property's type. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
12420
+ description: `set: the value, fitted to the property's type. A number the property would have to cut is refused rather than truncated: a fraction into an int, a number other than 0 or 1 into a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent, a fraction into one holding an int included. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
12421
12421
  },
12422
12422
  method: {
12423
12423
  type: "string",
@@ -12427,7 +12427,7 @@ var init_tool_definitions = __esm(() => {
12427
12427
  args: {
12428
12428
  type: "array",
12429
12429
  ops: ["call"],
12430
- description: `call: the arguments, fitted to the method's parameter types. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
12430
+ description: `call: the arguments, fitted to the method's parameter types. A number a parameter would have to cut is refused rather than truncated: a fraction for an int, a number other than 0 or 1 for a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
12431
12431
  },
12432
12432
  properties: {
12433
12433
  type: ["boolean", "array"],
@@ -61,65 +61,13 @@ func set_property(params: Dictionary) -> Dictionary:
61
61
  if not Paths.can_read(holder, named):
62
62
  return {"type": "error", "message": Paths.nothing_there(holder, named, str(reached["called"]))}
63
63
  var old_value: Variant = Paths.read_under(holder, named)
64
- # The same rule the call path holds: a value that cannot become what the property holds is
65
- # refused rather than written. Writing it means the engine picks something, and what it picks
66
- # for a word where a number goes is zero, which the answer then reports as the new value.
67
- #
68
- # A slot declaring nothing, an untyped or Variant property or an element of an untyped list, takes
69
- # anything, and the engine stores what it is given there. What it holds now still says how a value
70
- # of the same kind is meant, 5 into an int staying an int, and whether a path names an object, but
71
- # a value of another kind is stored as sent: a word into one holding a number is the word, and a
72
- # number into one holding a word is the number, not its text. The text null empties it when it
73
- # holds an object or nothing, as a wait reads it on the same slot.
74
64
  var slot: Dictionary = Values.slot_declared(holder, named)
75
65
  var declared: int = slot["type"]
76
66
  var declared_script: Script = slot["script"]
77
- var wanted: int = declared if declared != TYPE_NIL else typeof(old_value)
78
- var holds_objects: bool = (
79
- declared == TYPE_OBJECT or (declared == TYPE_NIL and (old_value == null or old_value is Object))
80
- )
81
- var given: Variant
82
- if holds_objects and names_no_object(value):
83
- given = null
84
- elif wanted == TYPE_OBJECT and (value is String or value is Dictionary):
85
- var named_object: Dictionary = _object_named(
86
- node, node_path, value, str(slot["class"]), declared_script
87
- )
88
- if named_object.has("message"):
89
- return {
90
- "type": "error",
91
- "message": "%s.%s holds an object: %s" % [reached["called"], named, named_object["message"]]
92
- }
93
- given = named_object["object"]
94
- else:
95
- var as_sent: Variant = _values.fitted(value, TYPE_NIL)
96
- if declared == TYPE_NIL and not Values.acceptable(as_sent, wanted):
97
- given = as_sent
98
- else:
99
- given = _values.fitted(value, wanted)
100
- if not Values.acceptable(given, wanted):
101
- return {
102
- "type": "error",
103
- "message":
104
- (
105
- "%s.%s holds %s and the value given is %s, which cannot become one.%s"
106
- % [
107
- reached["called"],
108
- named,
109
- type_string(wanted),
110
- type_string(typeof(given)),
111
- _written_as(wanted)
112
- ]
113
- )
114
- }
115
- var typed: Dictionary = _values.typed_like(given, old_value, _element_object.bind(node, node_path))
116
- if typed.has("message"):
117
- return {
118
- "type": "error",
119
- "message":
120
- "%s.%s is a typed %s: %s." % [reached["called"], named, _sort_of(old_value), typed["message"]]
121
- }
122
- given = typed["value"]
67
+ var fitting: Dictionary = _value_for(value, slot, old_value, reached, node, node_path)
68
+ if fitting.has("message"):
69
+ return fitting
70
+ var given: Variant = fitting["value"]
123
71
  # A component of a vector or a transform is written into a copy, which is put back where it was
124
72
  # read from, setter and all; the read back is then taken off the game again, since the copy here
125
73
  # says only what was written into it.
@@ -201,6 +149,131 @@ func set_property(params: Dictionary) -> Dictionary:
201
149
  return answer
202
150
 
203
151
 
152
+ ## What [param sent] becomes as argument [param index] of the method [param named] on the holder
153
+ ## [param reached] ends at, as [method Paths.walk_to] answered it, as `{"value": ...}`, or the refusal
154
+ ## saying why it cannot become one: a number the parameter would have to cut, a value that cannot be
155
+ ## converted, or a list or map that cannot be built as the typed one the parameter takes.
156
+ func _argument_for(
157
+ sent: Variant, index: int, reached: Dictionary, named: String, node: Node, node_path: String
158
+ ) -> Dictionary:
159
+ var holder: Object = reached["holder"]
160
+ var called: String = reached["called"]
161
+ var wants: int = _values.parameter_type(holder, named, index)
162
+ if Values.cuts(sent, wants):
163
+ return {
164
+ "type": "error",
165
+ "message":
166
+ (
167
+ "%s.%s takes %s as argument %d and was given %s, which it would have to be cut to fit"
168
+ % [called, named, type_string(wants), index + 1, JSON.stringify(sent)]
169
+ )
170
+ }
171
+ var given: Variant = _values.fitted(sent, wants)
172
+ if not Values.acceptable(given, wants):
173
+ return {
174
+ "type": "error",
175
+ "message":
176
+ (
177
+ "%s.%s takes %s as argument %d and was given %s, which cannot be converted.%s"
178
+ % [
179
+ called,
180
+ named,
181
+ type_string(wants),
182
+ index + 1,
183
+ type_string(typeof(given)),
184
+ _written_as(wants)
185
+ ]
186
+ )
187
+ }
188
+ # A typed list or map is built as one, since a plain one handed to such a parameter raises
189
+ # inside the game, which is what every check here exists to stop.
190
+ var container: Variant = _values.parameter_container(holder, named, index)
191
+ if container == null:
192
+ return {"value": given}
193
+ var typed: Dictionary = _values.typed_like(given, container, _element_object.bind(node, node_path))
194
+ if typed.has("message"):
195
+ return {
196
+ "type": "error",
197
+ "message":
198
+ (
199
+ "%s.%s takes a typed %s as argument %d: %s."
200
+ % [called, named, _sort_of(container), index + 1, typed["message"]]
201
+ )
202
+ }
203
+ return {"value": typed["value"]}
204
+
205
+
206
+ ## What [param value] becomes written into the slot [param reached] ends on, as
207
+ ## [method Paths.walk_to] answered it, as `{"value": ...}`, or the refusal saying why it cannot become
208
+ ## one.
209
+ ##
210
+ ## The same rule the call path holds: a value that cannot become what the property holds is
211
+ ## refused rather than written. Writing it means the engine picks something, and what it picks
212
+ ## for a word where a number goes is zero, which the answer then reports as the new value. So is a
213
+ ## number a whole number or a truth would have to cut, since converting one truncates.
214
+ ##
215
+ ## A slot declaring nothing, an untyped or Variant property or an element of an untyped list, takes
216
+ ## anything, and the engine stores what it is given there. What it holds now still says how a value
217
+ ## of the same kind is meant, 5 into an int staying an int, and whether a path names an object, but
218
+ ## a value of another kind is stored as sent: a word into one holding a number is the word, a
219
+ ## fraction into one holding a whole number is the fraction, and a number into one holding a word is
220
+ ## the number, not its text. The text null empties it when it holds an object or nothing, as a wait
221
+ ## reads it on the same slot.
222
+ func _value_for(
223
+ value: Variant, slot: Dictionary, old_value: Variant, reached: Dictionary, node: Node, node_path: String
224
+ ) -> Dictionary:
225
+ var named: String = reached["name"]
226
+ var called: String = reached["called"]
227
+ var declared: int = slot["type"]
228
+ var declared_script: Script = slot["script"]
229
+ var wanted: int = declared if declared != TYPE_NIL else typeof(old_value)
230
+ var holds_objects: bool = (
231
+ declared == TYPE_OBJECT or (declared == TYPE_NIL and (old_value == null or old_value is Object))
232
+ )
233
+ if holds_objects and names_no_object(value):
234
+ return {"value": null}
235
+ if wanted == TYPE_OBJECT and (value is String or value is Dictionary):
236
+ var named_object: Dictionary = _object_named(
237
+ node, node_path, value, str(slot["class"]), declared_script
238
+ )
239
+ if named_object.has("message"):
240
+ return {
241
+ "type": "error",
242
+ "message": "%s.%s holds an object: %s" % [called, named, named_object["message"]]
243
+ }
244
+ return {"value": named_object["object"]}
245
+ var as_sent: Variant = _values.fitted(value, TYPE_NIL)
246
+ var cut: bool = Values.cuts(value, wanted)
247
+ var given: Variant = as_sent
248
+ if declared != TYPE_NIL and cut:
249
+ return {
250
+ "type": "error",
251
+ "message":
252
+ (
253
+ "%s.%s holds %s and the value given is %s, which it would have to be cut to fit"
254
+ % [called, named, type_string(wanted), JSON.stringify(value)]
255
+ )
256
+ }
257
+ if declared != TYPE_NIL or (not cut and Values.acceptable(as_sent, wanted)):
258
+ given = _values.fitted(value, wanted)
259
+ if not Values.acceptable(given, wanted):
260
+ return {
261
+ "type": "error",
262
+ "message":
263
+ (
264
+ "%s.%s holds %s and the value given is %s, which cannot become one.%s"
265
+ % [called, named, type_string(wanted), type_string(typeof(given)), _written_as(wanted)]
266
+ )
267
+ }
268
+ var typed: Dictionary = _values.typed_like(given, old_value, _element_object.bind(node, node_path))
269
+ if typed.has("message"):
270
+ return {
271
+ "type": "error",
272
+ "message": "%s.%s is a typed %s: %s." % [called, named, _sort_of(old_value), typed["message"]]
273
+ }
274
+ return {"value": typed["value"]}
275
+
276
+
204
277
  ## Whether [param given] is the value [param held] already is, at the precision the property keeps.
205
278
  ##
206
279
  ## A float arrives as 64 bits and most engine properties keep 32: Camera3D.h_offset given -1.1
@@ -254,6 +327,12 @@ func call_method(params: Dictionary) -> Dictionary:
254
327
  var spelled: String = str(reached["name"])
255
328
  if Paths.method_of(spelled).is_empty():
256
329
  spelled += "()"
330
+ # A vector or a transform has no element to call a method on: it is read by its components.
331
+ if Paths.composite(reached["holder"]):
332
+ return {
333
+ "type": "error",
334
+ "message": Paths.nothing_there(reached["holder"], spelled, str(reached["called"]))
335
+ }
257
336
  if args.is_empty() and Paths.can_read(reached["holder"], spelled):
258
337
  var read_from: int = Time.get_ticks_usec()
259
338
  var answered: Variant = Paths.read_under(reached["holder"], spelled)
@@ -320,41 +399,10 @@ func call_method(params: Dictionary) -> Dictionary:
320
399
  by_engine_class[index + 1] = [declared, Values.class_of(resolved)]
321
400
  deserialized_args.append(named_object["object"])
322
401
  continue
323
- var given: Variant = _values.fitted(args[index], wants)
324
- if not Values.acceptable(given, wants):
325
- return {
326
- "type": "error",
327
- "message":
328
- (
329
- "%s.%s takes %s as argument %d and was given %s, which cannot be converted.%s"
330
- % [
331
- reached["called"],
332
- named,
333
- type_string(wants),
334
- index + 1,
335
- type_string(typeof(given)),
336
- _written_as(wants)
337
- ]
338
- )
339
- }
340
- # A typed list or map is built as one, since a plain one handed to such a parameter raises
341
- # inside the game, which is what every check here exists to stop.
342
- var container: Variant = _values.parameter_container(holder, named, index)
343
- if container != null:
344
- var typed: Dictionary = _values.typed_like(
345
- given, container, _element_object.bind(node, node_path)
346
- )
347
- if typed.has("message"):
348
- return {
349
- "type": "error",
350
- "message":
351
- (
352
- "%s.%s takes a typed %s as argument %d: %s."
353
- % [reached["called"], named, _sort_of(container), index + 1, typed["message"]]
354
- )
355
- }
356
- given = typed["value"]
357
- deserialized_args.append(given)
402
+ var argument: Dictionary = _argument_for(args[index], index, reached, named, node, node_path)
403
+ if argument.has("message"):
404
+ return argument
405
+ deserialized_args.append(argument["value"])
358
406
 
359
407
  # Timed around the call alone, since the bridge's round trip is a second or more and swamps what
360
408
  # one click's work costs; the frame metrics read whichever frame is last, which is not this one.
@@ -134,6 +134,28 @@ static func acceptable(value: Variant, type: int) -> bool:
134
134
  return Serialisation.acceptable(value, type)
135
135
 
136
136
 
137
+ ## Whether [param given] would have to be cut to become a [param type]: a fraction made a whole
138
+ ## number, or a number other than 0 or 1 made a truth.
139
+ ##
140
+ ## A number arrives from JSON as a float, so one is converted to what the slot holds, and the
141
+ ## conversion truncates: 2.5 written into an int was stored as 2 and answered as written, and 0.2 on
142
+ ## a bool became true. A number written as text counts as the number.
143
+ static func cuts(given: Variant, type: int) -> bool:
144
+ if type != TYPE_INT and type != TYPE_BOOL:
145
+ return false
146
+ var number: Variant = given
147
+ if given is String:
148
+ var text: String = given
149
+ number = Read.json_or_null(text)
150
+ if typeof(number) != TYPE_FLOAT and typeof(number) != TYPE_INT:
151
+ return false
152
+ var value: float = Read.as_float(number)
153
+ var whole: bool = value == floorf(value)
154
+ if type == TYPE_INT:
155
+ return not whole
156
+ return not (whole and (value == 0.0 or value == 1.0))
157
+
158
+
137
159
  ## How a value of [param type] is written to be read as one, or empty when it is not built from fields.
138
160
  static func written_as(type: int) -> String:
139
161
  return Serialisation.written_as(type)
@@ -312,19 +312,7 @@ static func _in_order(compare: String, limit: float, current: Variant) -> bool:
312
312
  ## conversion truncates: 3.5 waited on an int became 3 and 0.2 on a bool became true, and a property
313
313
  ## already holding that answered met at once, about a state nobody asked for.
314
314
  static func _cut_to_fit(given: Variant, type: int, node_path: String, property: String) -> String:
315
- if type != TYPE_INT and type != TYPE_BOOL:
316
- return ""
317
- var number: Variant = given
318
- if given is String:
319
- var text: String = given
320
- number = Read.json_or_null(text)
321
- if typeof(number) != TYPE_FLOAT and typeof(number) != TYPE_INT:
322
- return ""
323
- var value: float = Read.as_float(number)
324
- var whole: bool = value == floorf(value)
325
- if type == TYPE_INT and whole:
326
- return ""
327
- if type == TYPE_BOOL and whole and (value == 0.0 or value == 1.0):
315
+ if not Values.cuts(given, type):
328
316
  return ""
329
317
  return (
330
318
  (
package/build/index.js CHANGED
@@ -35562,7 +35562,7 @@ var TOOL_SPECS = [
35562
35562
  value: {
35563
35563
  ops: ["set"],
35564
35564
  blank: true,
35565
- description: `set: the value, fitted to the property's type. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
35565
+ description: `set: the value, fitted to the property's type. A number the property would have to cut is refused rather than truncated: a fraction into an int, a number other than 0 or 1 into a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent, a fraction into one holding an int included. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
35566
35566
  },
35567
35567
  method: {
35568
35568
  type: "string",
@@ -35572,7 +35572,7 @@ var TOOL_SPECS = [
35572
35572
  args: {
35573
35573
  type: "array",
35574
35574
  ops: ["call"],
35575
- description: `call: the arguments, fitted to the method's parameter types. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
35575
+ description: `call: the arguments, fitted to the method's parameter types. A number a parameter would have to cut is refused rather than truncated: a fraction for an int, a number other than 0 or 1 for a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
35576
35576
  },
35577
35577
  properties: {
35578
35578
  type: ["boolean", "array"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.29",
3
+ "version": "1.2.30",
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",