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.
- package/build/cli.js +1470 -726
- package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +15 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -16
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +107 -343
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +61 -28
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/classdb_queries.gd +28 -17
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +27 -11
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +1385 -553
- package/build/keeper.js +4 -0
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
228
|
-
## answers the way an object argument does. Anything that is not a typed
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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":
|
|
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],
|
|
251
|
-
##
|
|
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
|
|
301
|
+
if not Paths.method_of(named).is_empty():
|
|
254
302
|
return TYPE_NIL
|
|
255
|
-
|
|
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.
|
|
344
|
-
##
|
|
345
|
-
##
|
|
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 =
|
|
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
|
-
|
|
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
|
-
##
|
|
197
|
-
##
|
|
198
|
-
##
|
|
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
|
|
280
|
+
if left == right or left == TYPE_NIL or right == TYPE_NIL:
|
|
203
281
|
return true
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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:
|