gdharness 1.1.21 → 1.1.23

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.
Files changed (51) hide show
  1. package/build/cli.js +1470 -726
  2. package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
  3. package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
  4. package/build/godot/addons/gdharness_editor/reading.gd +17 -0
  5. package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
  6. package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
  7. package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
  8. package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
  9. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
  10. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
  11. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
  12. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
  13. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
  14. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
  15. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
  16. package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
  17. package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +15 -5
  20. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
  21. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
  22. package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
  23. package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
  24. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -16
  25. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +107 -343
  26. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
  27. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
  28. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
  29. package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
  30. package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
  31. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +61 -28
  32. package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
  33. package/build/godot/operations/audio_buses.gd +47 -11
  34. package/build/godot/operations/class_cache.gd +18 -2
  35. package/build/godot/operations/classdb_queries.gd +28 -17
  36. package/build/godot/operations/dependencies.gd +128 -17
  37. package/build/godot/operations/file_walk.gd +20 -0
  38. package/build/godot/operations/gdscript_analysis.gd +59 -20
  39. package/build/godot/operations/gdscript_authoring.gd +127 -60
  40. package/build/godot/operations/godot_operations.gd +24 -5
  41. package/build/godot/operations/import_pipeline.gd +179 -19
  42. package/build/godot/operations/logger.gd +2 -2
  43. package/build/godot/operations/plugins.gd +88 -70
  44. package/build/godot/operations/project_config.gd +41 -6
  45. package/build/godot/operations/project_diagnostics.gd +27 -11
  46. package/build/godot/operations/reading.gd +17 -0
  47. package/build/godot/operations/resource_files.gd +60 -17
  48. package/build/godot/operations/serialisation.gd +116 -7
  49. package/build/index.js +1385 -553
  50. package/build/keeper.js +4 -0
  51. package/package.json +1 -1
@@ -94,12 +94,17 @@ func parameter_class(object: Object, method: String, index: int) -> String:
94
94
 
95
95
 
96
96
  ## What the slot [param named] on [param holder] declares it holds, as `{"type": int, "class":
97
- ## String}`: a property's declaration, or the element type of a typed list or the value type of a
98
- ## typed map. TYPE_NIL when nothing is declared.
97
+ ## String, "script": Script or null}`: a property's declaration, or the element type of a typed list
98
+ ## or the value type of a typed map. TYPE_NIL when nothing is declared, which is also what a
99
+ ## property declared as Variant or left untyped reads as: either way it takes anything.
99
100
  ##
100
101
  ## Asked rather than read off the value in the slot, because an object slot can be empty: a
101
102
  ## property typed `Gear` holding null reads as TYPE_NIL, and anything written into it passed the
102
103
  ## check and was then dropped by the engine, which answered with the slot still null.
104
+ ##
105
+ ## The script only for a typed container, the one place the engine hands it over. A property typed
106
+ ## as a script class with no `class_name` is declared under the engine class the script extends,
107
+ ## measured on 4.7.2: `var quarrel: Walker` with Walker a preloaded Node3D script lists as Node3D.
103
108
  static func slot_declared(holder: Variant, named: String) -> Dictionary:
104
109
  if holder is Array:
105
110
  var items: Array = holder
@@ -107,6 +112,7 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
107
112
  return {
108
113
  "type": items.get_typed_builtin(),
109
114
  "class": _typed_class(items.get_typed_script(), items.get_typed_class_name()),
115
+ "script": _script_or_null(items.get_typed_script()),
110
116
  }
111
117
  elif holder is Dictionary:
112
118
  var map: Dictionary = holder
@@ -114,6 +120,7 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
114
120
  return {
115
121
  "type": map.get_typed_value_builtin(),
116
122
  "class": _typed_class(map.get_typed_value_script(), map.get_typed_value_class_name()),
123
+ "script": _script_or_null(map.get_typed_value_script()),
117
124
  }
118
125
  elif holder is Object:
119
126
  var object: Object = holder
@@ -122,8 +129,13 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
122
129
  return {
123
130
  "type": Read.as_int(entry.get("type", TYPE_NIL), TYPE_NIL),
124
131
  "class": str(entry.get("class_name", "")),
132
+ "script": null,
125
133
  }
126
- return {"type": TYPE_NIL, "class": ""}
134
+ return {"type": TYPE_NIL, "class": "", "script": null}
135
+
136
+
137
+ static func _script_or_null(typed: Variant) -> Script:
138
+ return typed if typed is Script else null
127
139
 
128
140
 
129
141
  ## The class a typed container names: its script's global name where it has one, since the engine
@@ -138,7 +150,20 @@ static func _typed_class(script: Variant, engine_class: StringName) -> String:
138
150
 
139
151
  ## Whether [param object] is a [param declared]: an engine class, or a script class by its global
140
152
  ## name anywhere along the script's inheritance.
141
- static func is_a(object: Object, declared: String) -> bool:
153
+ ##
154
+ ## Against [param declared_script] itself when there is one. A script with no global name is named by
155
+ ## the engine class it extends, so an object of that class with another script, or none, passes the
156
+ ## name, and a typed list built with it drops the element: measured on 4.7.2, an Array[Walker] given
157
+ ## a plain Node3D comes out empty, the engine's errors going to the game's output alone.
158
+ static func is_a(object: Object, declared: String, declared_script: Script = null) -> bool:
159
+ if declared_script != null:
160
+ var own: Variant = object.get_script()
161
+ while own is Script:
162
+ if own == declared_script:
163
+ return true
164
+ var along: Script = own
165
+ own = along.get_base_script()
166
+ return false
142
167
  if object.is_class(declared):
143
168
  return true
144
169
  var script: Variant = object.get_script()
@@ -150,16 +175,41 @@ static func is_a(object: Object, declared: String) -> bool:
150
175
  return false
151
176
 
152
177
 
153
- ## What [param object] is, by its script class where it has one.
178
+ ## What became of [param node] once an input it was given has run: still in the tree, taken out of
179
+ ## it, or freed. Untyped, because a freed reference handed to a typed parameter is itself the engine
180
+ ## error this is here to avoid.
181
+ static func afterwards(node: Variant) -> String:
182
+ if not is_instance_valid(node):
183
+ return "freed"
184
+ if not node is Node:
185
+ return "removed"
186
+ var placed: Node = node
187
+ return "in_tree" if placed.is_inside_tree() else "removed"
188
+
189
+
190
+ ## What [param object] is, by its script class where it has one, and by its script's file where
191
+ ## that has no global name, since the engine class alone is what a refusal about the script has
192
+ ## just said it matched.
154
193
  static func class_of(object: Object) -> String:
155
194
  var script: Variant = object.get_script()
156
195
  if script is Script:
157
196
  var current: Script = script
158
197
  if not current.get_global_name().is_empty():
159
198
  return str(current.get_global_name())
199
+ return "%s with the script %s" % [object.get_class(), script_named(current)]
160
200
  return object.get_class()
161
201
 
162
202
 
203
+ ## How a refusal names [param script]: its global name, or its file. A class declared inside another
204
+ ## script has neither, measured on 4.7.2, so it is called an inner class and no more.
205
+ static func script_named(script: Script) -> String:
206
+ if not script.get_global_name().is_empty():
207
+ return str(script.get_global_name())
208
+ if not script.resource_path.is_empty():
209
+ return script.resource_path
210
+ return "an inner class"
211
+
212
+
163
213
  ## A value from the wire fitted to the type a property or parameter declares.
164
214
  func fitted(value: Variant, type: int) -> Variant:
165
215
  return _shared.fitted(value, type)
@@ -224,9 +274,13 @@ static func _element_type(named: String) -> Dictionary:
224
274
  ## A typed container refuses a plain one, and the engine says nothing about it: an Array[int]
225
275
  ## property written with the list JSON carries kept what it held, and the answer showed the write
226
276
  ## as done. Each element is fitted the way a single value is, and an object element is named by
227
- ## its path through [param resolve], which is called with the path and the class declared and
228
- ## answers the way an object argument does. Anything that is not a typed container comes back as
229
- ## it was given.
277
+ ## its path through [param resolve], which is called with the path, the class declared and the
278
+ ## script declared, and answers the way an object argument does. Anything that is not a typed
279
+ ## container comes back as it was given.
280
+ ##
281
+ ## The container built is counted against what went into it. The engine drops an element it will
282
+ ## not take and says so only in the game's output, so a list that came out shorter is refused here
283
+ ## rather than written, which would empty the game's own.
230
284
  func typed_like(given: Variant, like: Variant, resolve: Callable) -> Dictionary:
231
285
  if like is Array and given is Array:
232
286
  var template: Array = like
@@ -239,20 +293,18 @@ func typed_like(given: Variant, like: Variant, resolve: Callable) -> Dictionary:
239
293
  items[index],
240
294
  template.get_typed_builtin(),
241
295
  _typed_class(template.get_typed_script(), template.get_typed_class_name()),
296
+ _script_or_null(template.get_typed_script()),
242
297
  resolve
243
298
  )
244
299
  if element.has("message"):
245
300
  return {"message": "element %d %s" % [index, element["message"]]}
246
301
  built.append(element["value"])
247
- return {
248
- "value":
249
- Array(
250
- built,
251
- template.get_typed_builtin(),
252
- template.get_typed_class_name(),
253
- template.get_typed_script()
254
- )
255
- }
302
+ var typed: Array = Array(
303
+ built, template.get_typed_builtin(), template.get_typed_class_name(), template.get_typed_script()
304
+ )
305
+ if typed.size() != built.size():
306
+ return {"message": "the engine would not take its elements as what the list holds"}
307
+ return {"value": typed}
256
308
  if like is Dictionary and given is Dictionary:
257
309
  var map_like: Dictionary = like
258
310
  if not map_like.is_typed():
@@ -264,6 +316,7 @@ func typed_like(given: Variant, like: Variant, resolve: Callable) -> Dictionary:
264
316
  key,
265
317
  map_like.get_typed_key_builtin(),
266
318
  _typed_class(map_like.get_typed_key_script(), map_like.get_typed_key_class_name()),
319
+ _script_or_null(map_like.get_typed_key_script()),
267
320
  resolve
268
321
  )
269
322
  if key_fitted.has("message"):
@@ -272,33 +325,37 @@ func typed_like(given: Variant, like: Variant, resolve: Callable) -> Dictionary:
272
325
  entries[key],
273
326
  map_like.get_typed_value_builtin(),
274
327
  _typed_class(map_like.get_typed_value_script(), map_like.get_typed_value_class_name()),
328
+ _script_or_null(map_like.get_typed_value_script()),
275
329
  resolve
276
330
  )
277
331
  if value_fitted.has("message"):
278
332
  return {"message": "the value under %s %s" % [str(key), value_fitted["message"]]}
279
333
  rebuilt[key_fitted["value"]] = value_fitted["value"]
280
- return {
281
- "value":
282
- Dictionary(
283
- rebuilt,
284
- map_like.get_typed_key_builtin(),
285
- map_like.get_typed_key_class_name(),
286
- map_like.get_typed_key_script(),
287
- map_like.get_typed_value_builtin(),
288
- map_like.get_typed_value_class_name(),
289
- map_like.get_typed_value_script()
290
- )
291
- }
334
+ var typed_map: Dictionary = Dictionary(
335
+ rebuilt,
336
+ map_like.get_typed_key_builtin(),
337
+ map_like.get_typed_key_class_name(),
338
+ map_like.get_typed_key_script(),
339
+ map_like.get_typed_value_builtin(),
340
+ map_like.get_typed_value_class_name(),
341
+ map_like.get_typed_value_script()
342
+ )
343
+ if typed_map.size() != rebuilt.size():
344
+ return {"message": "the engine would not take its entries as what the map holds"}
345
+ return {"value": typed_map}
292
346
  return {"value": given}
293
347
 
294
348
 
295
349
  ## One element fitted to a container's element type, as `{"value"}` or `{"message"}` saying what it
296
350
  ## was and what goes there. The message is the part after "element N".
297
- func _element(item: Variant, builtin: int, declared: String, resolve: Callable) -> Dictionary:
351
+ ##
352
+ ## An object element given as a record goes to [param resolve] as a path would, which says how an
353
+ ## object is named, rather than being refused as a map that cannot become the class.
354
+ func _element(item: Variant, builtin: int, declared: String, script: Script, resolve: Callable) -> Dictionary:
298
355
  if builtin == TYPE_NIL:
299
356
  return {"value": item}
300
- if builtin == TYPE_OBJECT and item is String:
301
- var named: Dictionary = resolve.call(item, declared)
357
+ if builtin == TYPE_OBJECT and (item is String or item is Dictionary):
358
+ var named: Dictionary = resolve.call(item, declared, script)
302
359
  if named.has("message"):
303
360
  return {"message": str(named["message"])}
304
361
  return {"value": named["object"]}
@@ -306,6 +363,8 @@ func _element(item: Variant, builtin: int, declared: String, resolve: Callable)
306
363
  if not acceptable(fitted_item, builtin):
307
364
  # The value as it was sent rather than its engine type: JSON numbers arrive as floats, so a 7
308
365
  # the caller wrote was named "float".
309
- var wanted: String = declared if builtin == TYPE_OBJECT else type_string(builtin)
366
+ var wanted: String = type_string(builtin)
367
+ if builtin == TYPE_OBJECT:
368
+ wanted = declared if script == null else script_named(script)
310
369
  return {"message": "is %s, which cannot become %s" % [JSON.stringify(serialize(item)), wanted]}
311
370
  return {"value": fitted_item}
@@ -3,6 +3,7 @@ extends RefCounted
3
3
  ## The commands that take time: they let the game run and answer when what was waited for has
4
4
  ## happened, or when the time ran out, so the caller never sleeps for a guessed length.
5
5
 
6
+ const Changes = preload("runtime_changes.gd")
6
7
  const Paths = preload("runtime_paths.gd")
7
8
  const Queries = preload("runtime_queries.gd")
8
9
  const Read = preload("reading.gd")
@@ -68,7 +69,6 @@ func wait_signal(params: Dictionary) -> Dictionary:
68
69
  return {"type": "error", "message": "%s has no signal %s" % [node_path, signal_name]}
69
70
 
70
71
  var catcher: SignalCatcher = SignalCatcher.new()
71
- catcher.arity = _signal_arity(node, signal_name)
72
72
  var callable: Callable = catcher._on_fired
73
73
  var listening: Error = node.connect(signal_name, callable, CONNECT_ONE_SHOT)
74
74
  if listening != OK:
@@ -149,9 +149,11 @@ func wait_until(params: Dictionary) -> Dictionary:
149
149
  # two ops would disagree about one property, and the wait would run out on the text null.
150
150
  var untyped_object: bool = declared == TYPE_NIL and (current == null or typeof(current) == TYPE_OBJECT)
151
151
  var holds_objects: bool = declared == TYPE_OBJECT or untyped_object
152
- if holds_objects and wanted is String and str(wanted).strip_edges() == "null":
152
+ if holds_objects and Changes.names_no_object(wanted):
153
153
  wanted = null
154
154
  var refused: String = _not_comparable(current, wanted, node_path, property)
155
+ if refused.is_empty():
156
+ refused = _cut_to_fit(params["value"], typeof(current), node_path, property)
155
157
  var never_held: bool = typeof(wanted) != TYPE_NIL and typeof(wanted) != TYPE_OBJECT
156
158
  if refused.is_empty() and declared == TYPE_OBJECT and never_held:
157
159
  refused = (
@@ -168,9 +170,12 @@ func wait_until(params: Dictionary) -> Dictionary:
168
170
  var frames: int = 0
169
171
  # The type is checked every time round, not only at the top: a property that holds an object a
170
172
  # frame later is the same error arriving late.
173
+ # At the precision the property keeps, as set compares: a float arrives as 64 bits and most
174
+ # engine properties keep 32, so h_offset waited on as -1.1 read -1.10000002384186 every frame and
175
+ # the wait ran out on a value the game had reached.
171
176
  while (
172
177
  Values.comparable(current, wanted)
173
- and current != wanted
178
+ and not Changes.held_already(wanted, current)
174
179
  and Time.get_ticks_msec() - started < timeout_ms
175
180
  ):
176
181
  await _host.get_tree().process_frame
@@ -186,13 +191,42 @@ func wait_until(params: Dictionary) -> Dictionary:
186
191
  "type": "condition",
187
192
  "path": node_path,
188
193
  "property": property,
189
- "met": Values.comparable(current, wanted) and current == wanted,
194
+ "met": Changes.held_already(wanted, current),
190
195
  "value": _values.serialize(current),
191
196
  "elapsed_ms": Time.get_ticks_msec() - started,
192
197
  "frames": frames,
193
198
  }
194
199
 
195
200
 
201
+ ## Why [param given] would have to be cut to be waited on a whole number or a truth, or "" when not.
202
+ ##
203
+ ## A number arrives from JSON as a float, so one is converted to what the property holds, and the
204
+ ## conversion truncates: 3.5 waited on an int became 3 and 0.2 on a bool became true, and a property
205
+ ## already holding that answered met at once, about a state nobody asked for.
206
+ static func _cut_to_fit(given: Variant, type: int, node_path: String, property: String) -> String:
207
+ if type != TYPE_INT and type != TYPE_BOOL:
208
+ return ""
209
+ var number: Variant = given
210
+ if given is String:
211
+ var text: String = given
212
+ number = Read.json_or_null(text)
213
+ if typeof(number) != TYPE_FLOAT and typeof(number) != TYPE_INT:
214
+ return ""
215
+ var value: float = Read.as_float(number)
216
+ var whole: bool = value == floorf(value)
217
+ if type == TYPE_INT and whole:
218
+ return ""
219
+ if type == TYPE_BOOL and whole and (value == 0.0 or value == 1.0):
220
+ return ""
221
+ return (
222
+ (
223
+ "%s.%s holds %s and the value to wait for is %s, which it would have to be cut to fit:"
224
+ + " waiting on it would answer about a state nobody asked for."
225
+ )
226
+ % [node_path, property, type_string(type), str(given)]
227
+ )
228
+
229
+
196
230
  ## Why [param wanted] cannot be waited for against [param current], or "" when it can.
197
231
  ##
198
232
  ## Refused before anything is evaluated, because the evaluation is what does the damage: an object
@@ -208,6 +242,17 @@ static func _not_comparable(current: Variant, wanted: Variant, node_path: String
208
242
  var types: Array[String] = [
209
243
  node_path, property, type_string(typeof(current)), type_string(typeof(wanted))
210
244
  ]
245
+ # A value that cannot become what the property holds is said as that, which names the remedy,
246
+ # before the comparison it would also fail; an object on either side is said as the comparison.
247
+ var objects: bool = typeof(current) == TYPE_OBJECT or typeof(wanted) == TYPE_OBJECT
248
+ if not objects and not Values.acceptable(wanted, typeof(current)):
249
+ return (
250
+ (
251
+ "%s.%s holds %s and the value to wait for is %s, which cannot become one:"
252
+ + " waiting on it would answer about a state nobody asked for."
253
+ )
254
+ % types
255
+ )
211
256
  if not Values.comparable(current, wanted):
212
257
  return (
213
258
  (
@@ -247,16 +292,15 @@ static func _watched(node: Node, node_path: String, property: String) -> Diction
247
292
  return {"value": Paths.read_under(holder, named), "declared": _declared_type(holder, named)}
248
293
 
249
294
 
250
- ## The type [param named] is declared with on [param holder], or TYPE_NIL for anything that declares
251
- ## none: an untyped property, a step into a list or map, or a call.
295
+ ## The type [param named] is declared with on [param holder], read the way a set reads it, so the two
296
+ ## refuse the same values: a property's declaration, or a typed list's or map's element type. Read
297
+ ## off properties alone, an element of an Array[Node] holding null declares nothing, and a wait for 5
298
+ ## on it runs out the timeout while a set of 5 on it is refused at once. TYPE_NIL for anything
299
+ ## declaring none, and for a call, whose answer nothing declares.
252
300
  static func _declared_type(holder: Variant, named: String) -> int:
253
- if typeof(holder) != TYPE_OBJECT or not Paths.method_of(named).is_empty():
301
+ if not Paths.method_of(named).is_empty():
254
302
  return TYPE_NIL
255
- var object: Object = holder
256
- for entry: Dictionary in object.get_property_list():
257
- if str(entry.get("name", "")) == named:
258
- return Read.as_int(entry.get("type", TYPE_NIL), TYPE_NIL)
259
- return TYPE_NIL
303
+ return Values.slot_declared(holder, named)["type"]
260
304
 
261
305
 
262
306
  ## Waits until something under [param node_path] has [param said] written on it.
@@ -331,26 +375,15 @@ func _anything_says(node_path: String, said: String, include_hidden: bool) -> bo
331
375
  return false
332
376
 
333
377
 
334
- func _signal_arity(node: Node, signal_name: String) -> int:
335
- for entry: Dictionary in node.get_signal_list():
336
- if entry.get("name", "") == signal_name:
337
- var declared: Array = entry.get("args", [])
338
- return declared.size()
339
- return 0
340
-
341
-
342
378
  ## Remembers that a signal fired and what it carried, for a wait that polls rather than
343
- ## awaits the signal directly, so the wait can also give up. The handler accepts up to five
344
- ## arguments, which covers every signal the engine declares, and records as many as the signal
345
- ## has.
379
+ ## awaits the signal directly, so the wait can also give up. Takes any number of arguments: a
380
+ ## handler of five fixed ones covered the engine's signals, and a game's own signal with six was
381
+ ## never delivered to it, so the wait ran out and said the signal had not fired.
346
382
  class SignalCatcher:
347
383
  extends RefCounted
348
384
  var fired: bool = false
349
- var arity: int = 0
350
385
  var args: Array = []
351
386
 
352
- func _on_fired(
353
- a: Variant = null, b: Variant = null, c: Variant = null, d: Variant = null, e: Variant = null
354
- ) -> void:
387
+ func _on_fired(...carried: Array) -> void:
355
388
  fired = true
356
- args = [a, b, c, d, e].slice(0, mini(arity, 5))
389
+ args = carried
@@ -53,6 +53,9 @@ const LIST_TYPES: Array[int] = [
53
53
  # type it wanted.
54
54
  const SERIALISERS: Dictionary = {
55
55
  TYPE_NIL: "_serialize_nil",
56
+ TYPE_FLOAT: "_serialize_float",
57
+ TYPE_PACKED_FLOAT32_ARRAY: "_serialize_floats",
58
+ TYPE_PACKED_FLOAT64_ARRAY: "_serialize_floats",
56
59
  TYPE_VECTOR2: "_serialize_vector2",
57
60
  TYPE_VECTOR2I: "_serialize_vector2i",
58
61
  TYPE_VECTOR3: "_serialize_vector3",
@@ -105,6 +108,8 @@ const BUILDERS: Dictionary = {
105
108
  "Color": "_build_color",
106
109
  "NodePath": "_build_node_path",
107
110
  "PackedByteArray": "_build_bytes",
111
+ "float": "_build_float",
112
+ "Dictionary": "_build_dictionary",
108
113
  }
109
114
 
110
115
  # What each tag has to carry before it is built. A tag naming a type whose keys are not there is a
@@ -130,6 +135,8 @@ const REQUIRED: Dictionary = {
130
135
  "Color": [["r", "g", "b"]],
131
136
  "NodePath": [["path"]],
132
137
  "PackedByteArray": [["base64"]],
138
+ "float": [["value"]],
139
+ "Dictionary": [["entries"]],
133
140
  }
134
141
 
135
142
 
@@ -145,7 +152,58 @@ func serialize_value(value: Variant) -> Variant:
145
152
  if typeof(value) == TYPE_OBJECT and not is_instance_valid(value):
146
153
  return null
147
154
  var serialiser: String = SERIALISERS.get(typeof(value), "")
148
- return call(serialiser, value) if not serialiser.is_empty() else value
155
+ if serialiser.is_empty():
156
+ return value
157
+ var carried: Variant = call(serialiser, value)
158
+ # The fixed shapes carry their components as bare floats, so an infinite one in a Vector2 is
159
+ # tagged here; containers reach their members through this function already.
160
+ return carried if typeof(value) in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried)
161
+
162
+
163
+ # [param carried] with every non-finite float in it tagged. JSON has no spelling for infinity or
164
+ # NaN: the engine writes 1e99999 and null, and the server, reading 1e99999 as Infinity, writes it
165
+ # back out as null, so a property holding INF was answered as holding nothing.
166
+ func _finite(carried: Variant) -> Variant:
167
+ if carried is float:
168
+ var number: float = carried
169
+ return _serialize_float(number)
170
+ if carried is Dictionary:
171
+ var fields: Dictionary = carried
172
+ var tagged: Dictionary = {}
173
+ for key: Variant in fields:
174
+ tagged[key] = _finite(fields[key])
175
+ return tagged
176
+ if carried is Array:
177
+ var items: Array = carried
178
+ var tagged_items: Array = []
179
+ for item: Variant in items:
180
+ tagged_items.append(_finite(item))
181
+ return tagged_items
182
+ return carried
183
+
184
+
185
+ func _serialize_float(value: float) -> Variant:
186
+ if is_finite(value):
187
+ return value
188
+ return {"_type": "float", "value": "nan" if is_nan(value) else ("inf" if value > 0 else "-inf")}
189
+
190
+
191
+ # A packed float array stays the JSON list it is unless something in it has no JSON spelling.
192
+ func _serialize_floats(value: Variant) -> Variant:
193
+ var numbers: Array = []
194
+ if value is PackedFloat32Array:
195
+ var single: PackedFloat32Array = value
196
+ numbers = Array(single)
197
+ else:
198
+ var double: PackedFloat64Array = value
199
+ numbers = Array(double)
200
+ for number: float in numbers:
201
+ if not is_finite(number):
202
+ var items: Array = []
203
+ for each: float in numbers:
204
+ items.append(_serialize_float(each))
205
+ return items
206
+ return value
149
207
 
150
208
 
151
209
  # Rebuilds a Godot value from the shape serialize_value gave it.
@@ -181,9 +239,27 @@ func deserialize_value(value: Variant) -> Variant:
181
239
  var needed: Array = REQUIRED.get(tag, [])
182
240
  if builder.is_empty() or not _carries(fields, needed):
183
241
  return fields
242
+ if tag == "float" and str(fields["value"]) not in ["inf", "-inf", "nan"]:
243
+ return fields
244
+ if tag == "Dictionary" and not pairs(fields["entries"]):
245
+ return fields
184
246
  return call(builder, fields)
185
247
 
186
248
 
249
+ ## Whether [param entries] is a list of [key, value] pairs, as a Dictionary tag carries them.
250
+ static func pairs(entries: Variant) -> bool:
251
+ if not entries is Array:
252
+ return false
253
+ var listed: Array = entries
254
+ for entry: Variant in listed:
255
+ if not entry is Array:
256
+ return false
257
+ var pair: Array = entry
258
+ if pair.size() != 2:
259
+ return false
260
+ return true
261
+
262
+
187
263
  ## Whether two values can be compared without the comparison itself failing.
188
264
  ##
189
265
  ## GDScript's `==` is not total. An Object against a String is a hard error rather than false, and
@@ -193,16 +269,19 @@ func deserialize_value(value: Variant) -> Variant:
193
269
  ## nothing anywhere naming the argument that did it. A wrong argument costs a refusal, never the
194
270
  ## session's game.
195
271
  ##
196
- ## Only an object is special. Every other pair the engine answers, false where they differ, and
197
- ## refusing those would refuse calls that work: an int against a float is the same number to a
198
- ## caller waiting for 1 on a property holding 1.0.
272
+ ## Measured on 4.7.2 over every pair of built-in types: two values of different types compare only
273
+ ## as an int against a float, a String against a StringName, or anything against null. Every other
274
+ ## pair is that error, a word against a number as much as an object against a word, and so is a bool
275
+ ## against an int and a NodePath against a String. An int against a float is kept because it is the
276
+ ## same number to a caller waiting for 1 on a property holding 1.0.
199
277
  static func comparable(one: Variant, other: Variant) -> bool:
200
278
  var left: int = typeof(one)
201
279
  var right: int = typeof(other)
202
- if left != TYPE_OBJECT and right != TYPE_OBJECT:
280
+ if left == right or left == TYPE_NIL or right == TYPE_NIL:
203
281
  return true
204
- # An object compares with another object and with null. Anything else is the error above.
205
- return (left == TYPE_OBJECT or left == TYPE_NIL) and (right == TYPE_OBJECT or right == TYPE_NIL)
282
+ var numbers: Array[int] = [TYPE_INT, TYPE_FLOAT]
283
+ var texts: Array[int] = [TYPE_STRING, TYPE_STRING_NAME]
284
+ return (left in numbers and right in numbers) or (left in texts and right in texts)
206
285
 
207
286
 
208
287
  ## Whether [param value] can be given to something declared as [param type] without damage.
@@ -400,7 +479,16 @@ func _serialize_signal(value: Signal) -> Dictionary:
400
479
  return described
401
480
 
402
481
 
482
+ ## A dictionary keyed by text as itself, and any other as its entries. JSON keys are text, so an
483
+ ## int or Vector2 key was answered as its text, and a dictionary read and written back had its keys
484
+ ## replaced by words: `{1: "a"}` came back as `{"1": "a"}`.
403
485
  func _serialize_dictionary(value: Dictionary) -> Dictionary:
486
+ var texts: bool = value.keys().all(func(key: Variant) -> bool: return key is String or key is StringName)
487
+ if not texts:
488
+ var entries: Array = []
489
+ for key: Variant in value:
490
+ entries.append([serialize_value(key), serialize_value(value[key])])
491
+ return {"_type": "Dictionary", "entries": entries}
404
492
  var serialised: Dictionary = {}
405
493
  for key: Variant in value:
406
494
  serialised[str(key)] = serialize_value(value[key])
@@ -621,6 +709,27 @@ func _build_bytes(fields: Dictionary) -> PackedByteArray:
621
709
  return Marshalls.base64_to_raw(str(fields.get("base64", "")))
622
710
 
623
711
 
712
+ func _build_float(fields: Dictionary) -> float:
713
+ return Read.as_float(fields)
714
+
715
+
716
+ ## The entries of a dictionary whose keys are not all text. JSON carries every number as a float,
717
+ ## and a dictionary keys 1.0 apart from 1, so a whole-number key is built as an int: the key a
718
+ ## game uses far more often, and one JSON cannot tell from the float.
719
+ func _build_dictionary(fields: Dictionary) -> Dictionary:
720
+ var built: Dictionary = {}
721
+ var entries: Array = fields["entries"]
722
+ for entry: Variant in entries:
723
+ var pair: Array = entry
724
+ var key: Variant = deserialize_value(pair[0])
725
+ if key is float:
726
+ var number: float = key
727
+ if is_finite(number) and number == floorf(number):
728
+ key = int(number)
729
+ built[key] = deserialize_value(pair[1])
730
+ return built
731
+
732
+
624
733
  ## One component of a compound value, as the type its constructor wants.
625
734
  ##
626
735
  ## Their own functions because a constructor refuses a Variant under `unsafe_call_argument`, and
@@ -20,17 +20,28 @@ func create_audio_bus(params: Dictionary) -> Dictionary:
20
20
  var parent_idx: int = Read.as_int(params.get("parent_bus_index", 0))
21
21
  if parent_idx < 0 or parent_idx >= AudioServer.bus_count:
22
22
  return _log.failure("No bus at index " + str(parent_idx))
23
-
24
- AudioServer.add_bus(parent_idx + 1)
25
- var new_idx: int = AudioServer.bus_count - 1
23
+ # Sends are by name, so a second bus with one would leave every send to it meaning either.
24
+ var existing: int = AudioServer.get_bus_index(bus_name)
25
+ if existing != -1:
26
+ return _log.failure("A bus named " + bus_name + " already exists, at index " + str(existing))
27
+
28
+ # Right after the bus it sends to, because a bus can only send to one before it. The engine
29
+ # inserts there and moves every later bus down one, so the new bus is at that index: taking the
30
+ # last index named and rewired whichever bus had been last, and answered with it.
31
+ var new_idx: int = parent_idx + 1
32
+ AudioServer.add_bus(new_idx)
26
33
  AudioServer.set_bus_name(new_idx, bus_name)
27
- if parent_idx > 0:
28
- AudioServer.set_bus_send(new_idx, AudioServer.get_bus_name(parent_idx))
34
+ AudioServer.set_bus_send(new_idx, AudioServer.get_bus_name(parent_idx))
29
35
 
30
36
  var layout: String = _save_layout()
31
37
  if layout.is_empty():
32
38
  return {}
33
- return {"bus": _bus(new_idx), "layout": layout}
39
+ # Every bus, because the ones after the new one have moved and a caller holding their old
40
+ # indices would address the wrong bus next.
41
+ var buses: Array[Dictionary] = []
42
+ for i: int in range(AudioServer.bus_count):
43
+ buses.append(_bus(i))
44
+ return {"bus": _bus(new_idx), "buses": buses, "layout": layout}
34
45
 
35
46
 
36
47
  func get_audio_buses(_params: Dictionary) -> Dictionary:
@@ -52,17 +63,42 @@ func set_audio_bus_effect(params: Dictionary) -> Dictionary:
52
63
  if effect == null:
53
64
  return _log.failure("Unknown effect type: " + effect_type)
54
65
 
55
- # The slot has to exist before an effect can be placed at that index.
56
- while AudioServer.get_bus_effect_count(bus_idx) <= effect_idx:
57
- AudioServer.add_bus_effect(bus_idx, AudioEffectAmplify.new())
58
-
66
+ # The slot holds this effect afterwards: the one there is replaced, and one past the last is
67
+ # added. The engine's add inserts, so the effect in the slot was pushed along rather than
68
+ # replaced, and a slot past the end was reached by padding the bus with Amplify effects nobody
69
+ # asked for.
70
+ var count: int = AudioServer.get_bus_effect_count(bus_idx)
71
+ if effect_idx < 0 or effect_idx > count:
72
+ return _log.failure(
73
+ (
74
+ "Bus "
75
+ + str(bus_idx)
76
+ + " has "
77
+ + str(count)
78
+ + (" effect" if count == 1 else " effects")
79
+ + ", so effect_index can be 0 to "
80
+ + str(count)
81
+ + ", where "
82
+ + str(count)
83
+ + " adds one after the last"
84
+ )
85
+ )
86
+ var replaced: String = ""
87
+ if effect_idx < count:
88
+ replaced = AudioServer.get_bus_effect(bus_idx, effect_idx).get_class()
89
+ AudioServer.remove_bus_effect(bus_idx, effect_idx)
59
90
  AudioServer.add_bus_effect(bus_idx, effect, effect_idx)
60
91
  AudioServer.set_bus_effect_enabled(bus_idx, effect_idx, enabled)
61
92
 
62
93
  var layout: String = _save_layout()
63
94
  if layout.is_empty():
64
95
  return {}
65
- return {"bus": _bus(bus_idx), "effect_index": effect_idx, "effect_type": effect_type, "layout": layout}
96
+ var answer: Dictionary = {
97
+ "bus": _bus(bus_idx), "effect_index": effect_idx, "effect_type": effect.get_class(), "layout": layout
98
+ }
99
+ if not replaced.is_empty():
100
+ answer["replaced"] = replaced
101
+ return answer
66
102
 
67
103
 
68
104
  func set_audio_bus_volume(params: Dictionary) -> Dictionary: