gdharness 1.3.8 → 1.3.10
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 +4668 -3229
- package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
- package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
- package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
- package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
- package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
- package/build/godot/addons/gdharness_editor/reading.gd +112 -6
- package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
- package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
- package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
- package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
- package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
- package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
- package/build/godot/operations/animation_reads.gd +115 -4
- package/build/godot/operations/audio_buses.gd +93 -7
- package/build/godot/operations/classdb_queries.gd +119 -7
- package/build/godot/operations/declared_values.gd +272 -0
- package/build/godot/operations/dependencies.gd +154 -41
- package/build/godot/operations/editor_tools.gd +29 -13
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +115 -18
- package/build/godot/operations/gdscript_analysis.gd +14 -0
- package/build/godot/operations/gdscript_authoring.gd +12 -2
- package/build/godot/operations/godot_operations.gd +18 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +127 -51
- package/build/godot/operations/project_config.gd +113 -68
- package/build/godot/operations/project_diagnostics.gd +4 -0
- package/build/godot/operations/project_validation.gd +266 -0
- package/build/godot/operations/property_paths.gd +89 -16
- package/build/godot/operations/property_values.gd +180 -121
- package/build/godot/operations/reading.gd +112 -6
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +19 -29
- package/build/godot/operations/setting_reads.gd +121 -0
- package/build/godot/operations/value_shapes.gd +128 -1
- package/build/godot/tests/listed.gd +20 -13
- package/build/keeper.js +1402 -707
- package/package.json +1 -1
|
@@ -6,6 +6,36 @@ extends RefCounted
|
|
|
6
6
|
|
|
7
7
|
const GdhRead = preload("reading.gd")
|
|
8
8
|
|
|
9
|
+
## The section of layer_names in project.godot naming the layers of each layers hint.
|
|
10
|
+
const LAYER_NAMES: Dictionary[int, String] = {
|
|
11
|
+
PROPERTY_HINT_LAYERS_2D_PHYSICS: "2d_physics",
|
|
12
|
+
PROPERTY_HINT_LAYERS_2D_RENDER: "2d_render",
|
|
13
|
+
PROPERTY_HINT_LAYERS_2D_NAVIGATION: "2d_navigation",
|
|
14
|
+
PROPERTY_HINT_LAYERS_3D_PHYSICS: "3d_physics",
|
|
15
|
+
PROPERTY_HINT_LAYERS_3D_RENDER: "3d_render",
|
|
16
|
+
PROPERTY_HINT_LAYERS_3D_NAVIGATION: "3d_navigation",
|
|
17
|
+
PROPERTY_HINT_LAYERS_AVOIDANCE: "avoidance",
|
|
18
|
+
}
|
|
19
|
+
## How many layers a mask holds, one bit each, whatever the editor names.
|
|
20
|
+
const MASK_LAYERS: int = 32
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## The layers [param mask] holds under a layers [param hint], each with the name project.godot
|
|
24
|
+
## gives it where it gives one.
|
|
25
|
+
static func layers_in(hint: int, mask: int) -> Array[Dictionary]:
|
|
26
|
+
var layers: Array[Dictionary] = []
|
|
27
|
+
for layer: int in range(1, MASK_LAYERS + 1):
|
|
28
|
+
if mask & (1 << (layer - 1)) == 0:
|
|
29
|
+
continue
|
|
30
|
+
var entry: Dictionary = {"layer": layer}
|
|
31
|
+
var name: String = str(
|
|
32
|
+
ProjectSettings.get_setting("layer_names/%s/layer_%d" % [LAYER_NAMES[hint], layer], "")
|
|
33
|
+
)
|
|
34
|
+
if not name.is_empty():
|
|
35
|
+
entry["name"] = name
|
|
36
|
+
layers.append(entry)
|
|
37
|
+
return layers
|
|
38
|
+
|
|
9
39
|
|
|
10
40
|
## The values an enum hint names, by name, from its hint string: "SWORD:0,SHIELD:1" as a script
|
|
11
41
|
## declares one, or "Add,Subtract" as an engine property does, each name then taking the number
|
|
@@ -127,6 +157,77 @@ static func enum_refusal(
|
|
|
127
157
|
return said + ". The nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
128
158
|
|
|
129
159
|
|
|
160
|
+
## Why [param word], given as argument [param index] of [param called] for the enum [param named],
|
|
161
|
+
## names none of its [param choices].
|
|
162
|
+
static func enum_argument_refusal(
|
|
163
|
+
called: String, index: int, named: String, word: String, choices: Dictionary
|
|
164
|
+
) -> String:
|
|
165
|
+
var said: String = (
|
|
166
|
+
"%s takes the enum %s as argument %d, and %s is none of its values: send %s by name or by number"
|
|
167
|
+
% [called, named, index + 1, word, _listed(choices)]
|
|
168
|
+
)
|
|
169
|
+
var close: PackedStringArray = GdhRead.nearest(word, PackedStringArray(choices.keys()))
|
|
170
|
+
if close.is_empty():
|
|
171
|
+
return said + "."
|
|
172
|
+
return said + ". The nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## Why [param value], given as argument [param index] for the enum [param named], is none of its
|
|
176
|
+
## [param choices], or "" when it is one. The engine passes any number on.
|
|
177
|
+
static func outside_enum_argument(index: int, named: String, value: Variant, choices: Dictionary) -> String:
|
|
178
|
+
if typeof(value) != TYPE_INT or choices.values().has(value):
|
|
179
|
+
return ""
|
|
180
|
+
return (
|
|
181
|
+
"argument %d, %d, is none of the values %s declares: %s."
|
|
182
|
+
% [index + 1, value, named, _listed(choices)]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
## The values of the enum the engine names [param qualified], by name: an engine class's, as
|
|
187
|
+
## Node.ProcessMode, or a script's, as Named.Size for a class_name, res://main.gd.Mood for a script
|
|
188
|
+
## without one, and either followed by inner classes. {} when it names nothing this can read.
|
|
189
|
+
static func enum_declared_as(qualified: String) -> Dictionary:
|
|
190
|
+
var cut: int = qualified.rfind(".")
|
|
191
|
+
if cut <= 0:
|
|
192
|
+
return {}
|
|
193
|
+
var owner: String = qualified.substr(0, cut)
|
|
194
|
+
var named: String = qualified.substr(cut + 1)
|
|
195
|
+
if ClassDB.class_exists(owner):
|
|
196
|
+
var constants: Dictionary = {}
|
|
197
|
+
for constant: String in ClassDB.class_get_enum_constants(owner, named):
|
|
198
|
+
constants[constant] = ClassDB.class_get_integer_constant(owner, constant)
|
|
199
|
+
return constants
|
|
200
|
+
var script: Script = _script_named_as(owner)
|
|
201
|
+
if script == null:
|
|
202
|
+
return {}
|
|
203
|
+
var found: Variant = script.get_script_constant_map().get(named)
|
|
204
|
+
if not found is Dictionary:
|
|
205
|
+
return {}
|
|
206
|
+
var values: Dictionary = found
|
|
207
|
+
return values
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
## The script [param owner] names: a class_name, a res:// path, or an inner class of either after a
|
|
211
|
+
## dot, or null.
|
|
212
|
+
static func _script_named_as(owner: String) -> Script:
|
|
213
|
+
var by_name: Script = script_class(owner)
|
|
214
|
+
if by_name != null:
|
|
215
|
+
return by_name
|
|
216
|
+
if owner.begins_with("res://") and ResourceLoader.exists(owner):
|
|
217
|
+
return load(owner) as Script
|
|
218
|
+
var cut: int = owner.rfind(".")
|
|
219
|
+
if cut <= 0:
|
|
220
|
+
return null
|
|
221
|
+
var outer: Script = _script_named_as(owner.substr(0, cut))
|
|
222
|
+
if outer == null:
|
|
223
|
+
return null
|
|
224
|
+
var inner: Variant = outer.get_script_constant_map().get(owner.substr(cut + 1))
|
|
225
|
+
if not inner is Script:
|
|
226
|
+
return null
|
|
227
|
+
var inner_script: Script = inner
|
|
228
|
+
return inner_script
|
|
229
|
+
|
|
230
|
+
|
|
130
231
|
## Why [param value], shown as [param shown], lies outside what [param declared] allows, an export
|
|
131
232
|
## range or an enum's values, or "" when it does not. A range marked or_greater or or_less is open
|
|
132
233
|
## on that side.
|
|
@@ -153,6 +254,19 @@ static func outside(target: Object, declared: Dictionary, value: Variant, shown:
|
|
|
153
254
|
if choices.is_empty() or choices.values().has(value):
|
|
154
255
|
return ""
|
|
155
256
|
return "%d is none of the values %s declares: %s." % [value, property, _listed(choices)]
|
|
257
|
+
if hint == PROPERTY_HINT_FLAGS and typeof(value) == TYPE_INT:
|
|
258
|
+
var flags: Dictionary = flags_of(hint_string)
|
|
259
|
+
var known: int = 0
|
|
260
|
+
for bit: int in flags.values():
|
|
261
|
+
known |= bit
|
|
262
|
+
var whole: int = value
|
|
263
|
+
var stray: int = whole & ~known
|
|
264
|
+
if flags.is_empty() or stray == 0:
|
|
265
|
+
return ""
|
|
266
|
+
return (
|
|
267
|
+
"%d holds bits no flag of %s has (%d): its flags are %s; the engine stores it as given."
|
|
268
|
+
% [whole, property, stray, _listed(flags)]
|
|
269
|
+
)
|
|
156
270
|
if hint != PROPERTY_HINT_RANGE:
|
|
157
271
|
return ""
|
|
158
272
|
var parts: PackedStringArray = []
|
|
@@ -173,6 +287,20 @@ static func outside(target: Object, declared: Dictionary, value: Variant, shown:
|
|
|
173
287
|
)
|
|
174
288
|
|
|
175
289
|
|
|
290
|
+
## The flags a flags hint names, each with its bit: "Fill:1,Expand:2" gives its values, and a name
|
|
291
|
+
## with none takes the bit of its place, as @export_flags("A", "B") does.
|
|
292
|
+
static func flags_of(hint_string: String) -> Dictionary:
|
|
293
|
+
var flags: Dictionary = {}
|
|
294
|
+
var parts: PackedStringArray = hint_string.split(",", false)
|
|
295
|
+
for index: int in parts.size():
|
|
296
|
+
var part: String = parts[index].strip_edges()
|
|
297
|
+
if ":" in part:
|
|
298
|
+
flags[part.get_slice(":", 0)] = part.get_slice(":", 1).to_int()
|
|
299
|
+
else:
|
|
300
|
+
flags[part] = 1 << index
|
|
301
|
+
return flags
|
|
302
|
+
|
|
303
|
+
|
|
176
304
|
static func _listed(choices: Dictionary) -> String:
|
|
177
305
|
var listed: PackedStringArray = []
|
|
178
306
|
for name: String in choices:
|
|
@@ -291,3 +419,147 @@ static func is_a(value: Object, name: String) -> bool:
|
|
|
291
419
|
return true
|
|
292
420
|
script = script.get_base_script()
|
|
293
421
|
return false
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
## Whether writing [param name] on [param target] runs code of its script: the script itself, a
|
|
425
|
+
## property it declares, or any property once it takes sets through [code]_set[/code].
|
|
426
|
+
static func runs_script_code(target: Object, name: String) -> bool:
|
|
427
|
+
var script: Script = target.get_script()
|
|
428
|
+
if name == "script":
|
|
429
|
+
return true
|
|
430
|
+
if script == null:
|
|
431
|
+
return false
|
|
432
|
+
for method: Dictionary in script.get_script_method_list():
|
|
433
|
+
if str(method.get("name", "")) == "_set":
|
|
434
|
+
return true
|
|
435
|
+
for declared: Dictionary in script.get_script_property_list():
|
|
436
|
+
if str(declared.get("name", "")) == name:
|
|
437
|
+
return true
|
|
438
|
+
return false
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
## Why [param property] of a [param holder], an [param engine_class], declared as [param declared],
|
|
442
|
+
## holds [param held], as shown, after it was given [param given], the value and how it is shown. A
|
|
443
|
+
## text property with a list of names holds only one of them, and its getter answers its default
|
|
444
|
+
## for any other. A player's bus is one, though the engine fills its list only in the inspector, so
|
|
445
|
+
## the buses are read from the AudioServer, which holds the project's layout; "the engine changed or
|
|
446
|
+
## refused the value" left a caller to guess the bus did not exist.
|
|
447
|
+
static func not_held(
|
|
448
|
+
holder: String, engine_class: String, property: String, declared: Dictionary, given: Array, held: String
|
|
449
|
+
) -> String:
|
|
450
|
+
var value: Variant = given[0]
|
|
451
|
+
var said: String = "%s.%s was given %s and holds %s after it was set" % [holder, property, given[1], held]
|
|
452
|
+
var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL))
|
|
453
|
+
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
454
|
+
if hint in LAYER_NAMES and typeof(value) == TYPE_INT:
|
|
455
|
+
return (
|
|
456
|
+
(
|
|
457
|
+
"%s: it is a mask of the %s layers, one bit each from layer 1 (1) to layer %d (%d), so it "
|
|
458
|
+
+ "takes 0 to %d and the engine keeps only those bits. Nothing was written."
|
|
459
|
+
)
|
|
460
|
+
% [
|
|
461
|
+
said,
|
|
462
|
+
LAYER_NAMES[hint],
|
|
463
|
+
MASK_LAYERS,
|
|
464
|
+
1 << (MASK_LAYERS - 1),
|
|
465
|
+
(1 << MASK_LAYERS) - 1,
|
|
466
|
+
]
|
|
467
|
+
)
|
|
468
|
+
var names: PackedStringArray = []
|
|
469
|
+
if hint == PROPERTY_HINT_ENUM and (type == TYPE_STRING or type == TYPE_STRING_NAME):
|
|
470
|
+
names = PackedStringArray(enum_values(str(declared.get("hint_string", ""))).keys())
|
|
471
|
+
var bus: bool = property == "bus" and engine_class.begins_with("AudioStreamPlayer")
|
|
472
|
+
if bus:
|
|
473
|
+
names = []
|
|
474
|
+
for index: int in AudioServer.bus_count:
|
|
475
|
+
var _added: bool = names.append(AudioServer.get_bus_name(index))
|
|
476
|
+
if names.is_empty() or names.has(str(value)):
|
|
477
|
+
return said + ": the engine changed or refused the value, so nothing was written."
|
|
478
|
+
var close: PackedStringArray = GdhRead.nearest(str(value), names)
|
|
479
|
+
var nearest: String = (
|
|
480
|
+
"%s %s" % ["is" if close.size() == 1 else "are", ", ".join(close)] if not close.is_empty() else ""
|
|
481
|
+
)
|
|
482
|
+
if bus:
|
|
483
|
+
# Not "holds Master after it was set": the getter answers the first bus for any name the
|
|
484
|
+
# layout lacks, and nothing was kept.
|
|
485
|
+
return (
|
|
486
|
+
(
|
|
487
|
+
"%s.bus takes the name of one of the project's audio buses, and %s is none of them, so it "
|
|
488
|
+
+ "would play on %s; %s. project_info include audio_buses lists them, and "
|
|
489
|
+
+ "project_settings add_audio_bus adds one. Nothing was written."
|
|
490
|
+
)
|
|
491
|
+
% [
|
|
492
|
+
holder,
|
|
493
|
+
given[1],
|
|
494
|
+
AudioServer.get_bus_name(0),
|
|
495
|
+
"the buses are " + ", ".join(names) if nearest.is_empty() else "the nearest " + nearest
|
|
496
|
+
]
|
|
497
|
+
)
|
|
498
|
+
return (
|
|
499
|
+
"%s: it takes one of the names it lists, and %s is none of them; %s. Nothing was written."
|
|
500
|
+
% [
|
|
501
|
+
said,
|
|
502
|
+
given[1],
|
|
503
|
+
"it lists " + ", ".join(names) if nearest.is_empty() else "the nearest it lists " + nearest
|
|
504
|
+
]
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
## The sentence naming the children of [param node] that have [param property], or "" when none
|
|
509
|
+
## does. A property of a helper node set on the body it serves, as navigation_layers on the
|
|
510
|
+
## CharacterBody2D rather than its NavigationAgent2D, was refused with the nearest physics layer.
|
|
511
|
+
static func held_by_children(node: Node, property: String) -> String:
|
|
512
|
+
var root: Node = node.owner if node.owner != null else node
|
|
513
|
+
var holders: PackedStringArray = []
|
|
514
|
+
for child: Node in node.get_children():
|
|
515
|
+
if property in child:
|
|
516
|
+
var _added: bool = holders.append("%s (a %s)" % [root.get_path_to(child), child.get_class()])
|
|
517
|
+
if holders.is_empty():
|
|
518
|
+
return ""
|
|
519
|
+
if holders.size() == 1:
|
|
520
|
+
return " The node at %s has it." % holders[0]
|
|
521
|
+
var last: String = holders[holders.size() - 1]
|
|
522
|
+
return " The nodes at %s and %s have it." % [", ".join(holders.slice(0, -1)), last]
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
## A sentence for each node path in [param given], by property, that names no node from
|
|
526
|
+
## [param node]. A path is written whatever it names, as the inspector writes one, since the node may
|
|
527
|
+
## be added next; a joint whose node_a named nothing then joined nothing, and no answer said so.
|
|
528
|
+
static func naming_nothing(node: Node, given: Dictionary) -> Dictionary:
|
|
529
|
+
var said: Dictionary = {}
|
|
530
|
+
for property: String in given:
|
|
531
|
+
if "/" in property or ":" in property:
|
|
532
|
+
continue
|
|
533
|
+
# What the node holds, since a path given as text is held as text until the engine stores it.
|
|
534
|
+
var path: Variant = node.get(property)
|
|
535
|
+
if not path is NodePath:
|
|
536
|
+
continue
|
|
537
|
+
var named: NodePath = path
|
|
538
|
+
if named.is_empty() or node.get_node_or_null(named) != null:
|
|
539
|
+
continue
|
|
540
|
+
said[property] = (
|
|
541
|
+
(
|
|
542
|
+
"%s names no node from %s. It is written, as the editor writes one, since that node may "
|
|
543
|
+
+ "be added next; until it is, %s finds nothing."
|
|
544
|
+
)
|
|
545
|
+
% [str(named), node.name, property]
|
|
546
|
+
)
|
|
547
|
+
return said
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
## Why [param material] has no [param property]: a ShaderMaterial's parameters are its shader's
|
|
551
|
+
## uniforms, so the shader is what lacks one, and "ShaderMaterial has no property" named the
|
|
552
|
+
## material.
|
|
553
|
+
static func no_uniform(material: ShaderMaterial, property: String) -> String:
|
|
554
|
+
var uniform: String = property.trim_prefix("shader_parameter/")
|
|
555
|
+
if material.shader == null:
|
|
556
|
+
return "The ShaderMaterial has no shader, so it has no parameter %s yet; set shader first." % uniform
|
|
557
|
+
var shader: String = material.shader.resource_path
|
|
558
|
+
var named: String = shader if not shader.is_empty() else "Its shader"
|
|
559
|
+
var uniforms: Array = []
|
|
560
|
+
for entry: Dictionary in material.shader.get_shader_uniform_list():
|
|
561
|
+
uniforms.append(str(entry["name"]))
|
|
562
|
+
var said: String = "%s declares no uniform %s, which %s would set" % [named, uniform, property]
|
|
563
|
+
if uniforms.is_empty():
|
|
564
|
+
return said + "; it declares no uniforms."
|
|
565
|
+
return GdhRead.offered(said, uniform, uniforms) + "."
|
|
@@ -24,6 +24,23 @@ extends RefCounted
|
|
|
24
24
|
|
|
25
25
|
## How many of a node's children a refusal for a path missing below it names.
|
|
26
26
|
const NAMED_CHILDREN: int = 12
|
|
27
|
+
## The types holding their floats as the engine's real numbers, 32-bit unless it was built for 64.
|
|
28
|
+
const SHAPES_OF_REALS: Array[int] = [
|
|
29
|
+
TYPE_VECTOR2,
|
|
30
|
+
TYPE_VECTOR3,
|
|
31
|
+
TYPE_VECTOR4,
|
|
32
|
+
TYPE_RECT2,
|
|
33
|
+
TYPE_PLANE,
|
|
34
|
+
TYPE_QUATERNION,
|
|
35
|
+
TYPE_AABB,
|
|
36
|
+
TYPE_BASIS,
|
|
37
|
+
TYPE_TRANSFORM2D,
|
|
38
|
+
TYPE_TRANSFORM3D,
|
|
39
|
+
TYPE_PROJECTION,
|
|
40
|
+
TYPE_PACKED_VECTOR2_ARRAY,
|
|
41
|
+
TYPE_PACKED_VECTOR3_ARRAY,
|
|
42
|
+
TYPE_PACKED_VECTOR4_ARRAY,
|
|
43
|
+
]
|
|
27
44
|
|
|
28
45
|
|
|
29
46
|
static func as_int(value: Variant, fallback: int = 0) -> int:
|
|
@@ -176,7 +193,13 @@ static func nearest(word: String, names: PackedStringArray) -> PackedStringArray
|
|
|
176
193
|
func(a: String, b: String) -> bool:
|
|
177
194
|
return a.to_lower().similarity(wanted) > b.to_lower().similarity(wanted)
|
|
178
195
|
)
|
|
179
|
-
|
|
196
|
+
# Every name that only adds to the word stays, since cutting at three named three corners of
|
|
197
|
+
# corner_radius and left out the fourth as if it were not there.
|
|
198
|
+
var named: PackedStringArray = PackedStringArray(close.slice(0, 3))
|
|
199
|
+
for name: String in close.slice(3):
|
|
200
|
+
if name.to_lower().begins_with(wanted):
|
|
201
|
+
var _added: bool = named.append(name)
|
|
202
|
+
return named
|
|
180
203
|
|
|
181
204
|
|
|
182
205
|
## [param said], the refusal for [param word], which is not among [param names], with those nearest
|
|
@@ -190,18 +213,70 @@ static func offered(said: String, word: String, names: Array) -> String:
|
|
|
190
213
|
|
|
191
214
|
|
|
192
215
|
## [param said], the refusal for [param property], which [param target] does not declare, with the
|
|
193
|
-
## names it declares nearest it when any are, as a misspelt one is usually one of them
|
|
194
|
-
##
|
|
216
|
+
## names it declares nearest it when any are, as a misspelt one is usually one of them, or else the
|
|
217
|
+
## variables its script declares. The engine's own are not listed when none is near, since an object
|
|
218
|
+
## has a hundred, while a script's are the few a caller most likely meant.
|
|
195
219
|
static func near_property(said: String, target: Object, property: String) -> String:
|
|
196
220
|
var headings: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
197
221
|
var names: Array[String] = []
|
|
222
|
+
var scripted: Array[String] = []
|
|
198
223
|
for declared: Dictionary in target.get_property_list():
|
|
199
|
-
|
|
224
|
+
var usage: int = as_int(declared.get("usage", 0))
|
|
225
|
+
if usage & PROPERTY_USAGE_ARRAY:
|
|
226
|
+
var listed: String = _list_called(target, declared, property)
|
|
227
|
+
if not listed.is_empty():
|
|
228
|
+
return "%s; %s" % [said, listed]
|
|
229
|
+
if not usage & headings:
|
|
200
230
|
names.append(str(declared.get("name", "")))
|
|
231
|
+
if usage & PROPERTY_USAGE_SCRIPT_VARIABLE and not usage & headings:
|
|
232
|
+
scripted.append(str(declared.get("name", "")))
|
|
201
233
|
var close: PackedStringArray = nearest(property, PackedStringArray(names))
|
|
202
|
-
if close.is_empty():
|
|
234
|
+
if not close.is_empty():
|
|
235
|
+
return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
236
|
+
if scripted.is_empty():
|
|
203
237
|
return said
|
|
204
|
-
|
|
238
|
+
scripted.sort()
|
|
239
|
+
return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(scripted))]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
## How [param target] keeps the list [param property] names, when that is the label of one of the
|
|
243
|
+
## engine's numbered lists, such as an OptionButton's Items; empty when it is not. The engine has no
|
|
244
|
+
## property by that name, only a count and each entry's fields under a numbered prefix, so "items"
|
|
245
|
+
## was refused with nothing near it to offer.
|
|
246
|
+
static func _list_called(target: Object, count: Dictionary, property: String) -> String:
|
|
247
|
+
var parts: PackedStringArray = str(count.get("class_name", "")).split(",")
|
|
248
|
+
if parts.size() != 2 or _bare(parts[0]) != _bare(property):
|
|
249
|
+
return ""
|
|
250
|
+
var counted: String = str(count.get("name", ""))
|
|
251
|
+
var first: String = parts[1] + "0/"
|
|
252
|
+
var fields: PackedStringArray = _fields_at(target, first)
|
|
253
|
+
if fields.is_empty() and ClassDB.can_instantiate(target.get_class()):
|
|
254
|
+
var fresh: Object = ClassDB.instantiate(target.get_class())
|
|
255
|
+
fresh.set(counted, 1)
|
|
256
|
+
fields = _fields_at(fresh, first)
|
|
257
|
+
if not fresh is RefCounted:
|
|
258
|
+
fresh.free()
|
|
259
|
+
var each: String = "each one's fields" if fields.is_empty() else "each one's %s" % ", ".join(fields)
|
|
260
|
+
return (
|
|
261
|
+
(
|
|
262
|
+
"it keeps its %s as %s, how many there are, and %s under %s for the first, %s for the second and so on: "
|
|
263
|
+
+ "give %s before the fields of the entries it makes"
|
|
264
|
+
)
|
|
265
|
+
% [parts[0].to_lower(), counted, each, first, parts[1] + "1/", counted]
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
static func _fields_at(target: Object, prefix: String) -> PackedStringArray:
|
|
270
|
+
var fields: PackedStringArray = PackedStringArray()
|
|
271
|
+
for declared: Dictionary in target.get_property_list():
|
|
272
|
+
var name: String = str(declared.get("name", ""))
|
|
273
|
+
if name.begins_with(prefix):
|
|
274
|
+
var _added: bool = fields.append(name.trim_prefix(prefix))
|
|
275
|
+
return fields
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
static func _bare(word: String) -> String:
|
|
279
|
+
return word.to_lower().replace("_", "").replace(" ", "")
|
|
205
280
|
|
|
206
281
|
|
|
207
282
|
## The signals [param object] has that a caller can connect, its own class's first: the engine
|
|
@@ -219,3 +294,34 @@ static func signals_of(object: Object) -> Array:
|
|
|
219
294
|
## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
|
|
220
295
|
static func with_article(word: String) -> String:
|
|
221
296
|
return ("an " if word.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + word
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
## [param value], which the engine holds as a 32-bit float, as the shortest decimal that is still
|
|
300
|
+
## that float. JSON carries a double, and the double a 32-bit 0.2 widens to is 0.200000002980232:
|
|
301
|
+
## the same number, but not the one written or the one the file shows, so a caller comparing what it
|
|
302
|
+
## set against what it read saw them differ. The shorter one, sent back, stores the same 32 bits. A
|
|
303
|
+
## plain float is left alone by the serialiser, since nothing says whether the engine holds it in
|
|
304
|
+
## 32 bits or 64.
|
|
305
|
+
static func single(value: float) -> float:
|
|
306
|
+
if not is_finite(value) or value == 0.0:
|
|
307
|
+
return value
|
|
308
|
+
var held: float = PackedFloat32Array([value])[0]
|
|
309
|
+
var magnitude: int = floori(log(absf(held)) / log(10.0))
|
|
310
|
+
for digits: int in range(1, 10):
|
|
311
|
+
var shift: int = digits - 1 - magnitude
|
|
312
|
+
var tried: float = (
|
|
313
|
+
roundf(held * pow(10.0, shift)) / pow(10.0, shift)
|
|
314
|
+
if shift >= 0
|
|
315
|
+
else roundf(held / pow(10.0, -shift)) * pow(10.0, -shift)
|
|
316
|
+
)
|
|
317
|
+
if PackedFloat32Array([tried])[0] == held:
|
|
318
|
+
return tried
|
|
319
|
+
return held
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
## Whether a value of [param type] holds its floats in 32 bits: a colour always, and the vectors and
|
|
323
|
+
## the shapes built of them unless the engine was built for 64.
|
|
324
|
+
static func holds_singles(type: int) -> bool:
|
|
325
|
+
if type in [TYPE_COLOR, TYPE_PACKED_COLOR_ARRAY, TYPE_PACKED_FLOAT32_ARRAY]:
|
|
326
|
+
return true
|
|
327
|
+
return not OS.has_feature("double") and type in SHAPES_OF_REALS
|
|
@@ -80,6 +80,12 @@ const SCENE_EXTENSIONS: PackedStringArray = ["tscn", "scn", "escn", "res", "tres
|
|
|
80
80
|
## `EDITOR_PID_VARIABLE` in the editor addon's bridge client.
|
|
81
81
|
const EDITOR_PID_VARIABLE: String = "GDHARNESS_EDITOR_PID"
|
|
82
82
|
|
|
83
|
+
## The directory the keeper holding this run reads, once the game has exited, to learn whether it
|
|
84
|
+
## set a restart with OS.set_restart_on_exit. The engine starts the process it restarts into after
|
|
85
|
+
## the tree has gone, and from outside that looks like a worker the game started in its last moments,
|
|
86
|
+
## so only the game can say which it was. Kept in step with `EXIT_NOTES_VARIABLE` in src/keeper.ts.
|
|
87
|
+
const EXIT_NOTES_VARIABLE: String = "GDHARNESS_EXIT_NOTES"
|
|
88
|
+
|
|
83
89
|
## The file the engine's error reports go to, beside the announcement and named the same way,
|
|
84
90
|
## which is where the server reads them for a run the editor plays. Kept in step with
|
|
85
91
|
## `errorReportPath` in src/runtime-client.ts.
|
|
@@ -165,6 +171,7 @@ func _init() -> void:
|
|
|
165
171
|
"wait_frames": _waits.wait_frames,
|
|
166
172
|
"wait_signal": _waits.wait_signal,
|
|
167
173
|
"watch_signal": _waits.watch_signal,
|
|
174
|
+
"watch_recorded": _waits.recorded,
|
|
168
175
|
"wait_until": _waits.wait_until,
|
|
169
176
|
"send_profile": _send_profile_now,
|
|
170
177
|
"close_game": _close_game,
|
|
@@ -245,10 +252,25 @@ func _ready() -> void:
|
|
|
245
252
|
var watched: int = get_tree().root.tree_exiting.connect(_send_the_profile)
|
|
246
253
|
if watched != OK:
|
|
247
254
|
push_error("gdharness could not watch for the game quitting: %d" % watched)
|
|
255
|
+
if not OS.get_environment(EXIT_NOTES_VARIABLE).is_empty():
|
|
256
|
+
var noted: int = get_tree().root.tree_exited.connect(_note_the_exit)
|
|
257
|
+
if noted != OK:
|
|
258
|
+
push_error("gdharness could not watch for the game's tree ending: %d" % noted)
|
|
248
259
|
_report_errors()
|
|
249
260
|
_start_server()
|
|
250
261
|
|
|
251
262
|
|
|
263
|
+
## Says whether this game restarts as it exits, once the whole tree has gone, so a game's own
|
|
264
|
+
## _exit_tree and every autoload's have had their say.
|
|
265
|
+
func _note_the_exit() -> void:
|
|
266
|
+
var path: String = OS.get_environment(EXIT_NOTES_VARIABLE).path_join("exit-%d.json" % OS.get_process_id())
|
|
267
|
+
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
|
268
|
+
if file == null:
|
|
269
|
+
return
|
|
270
|
+
var _written: bool = file.store_string(JSON.stringify({"restart": OS.is_restart_on_exit_set()}))
|
|
271
|
+
file.close()
|
|
272
|
+
|
|
273
|
+
|
|
252
274
|
func _exit_tree() -> void:
|
|
253
275
|
if _errors != null:
|
|
254
276
|
OS.remove_logger(_errors)
|
|
@@ -601,6 +623,9 @@ func _identity(bind_address: String) -> Dictionary:
|
|
|
601
623
|
"path": ProjectSettings.globalize_path("res://").rstrip("/")
|
|
602
624
|
},
|
|
603
625
|
"godot": Engine.get_version_info().get("string", ""),
|
|
626
|
+
# Where user:// is for this run, which only the engine can say: it follows the project's
|
|
627
|
+
# name and settings and the environment the run was started in.
|
|
628
|
+
"user_dir": OS.get_user_data_dir(),
|
|
604
629
|
"addon_version": _loaded_version,
|
|
605
630
|
"addon_digest": _loaded_digest,
|
|
606
631
|
}
|
|
@@ -670,6 +695,7 @@ func _execute_command(command: String, params: Dictionary) -> Dictionary:
|
|
|
670
695
|
"type": "error",
|
|
671
696
|
"message": "Unknown command: %s. Commands: %s" % [command, ", ".join(_commands.keys())]
|
|
672
697
|
}
|
|
698
|
+
_waits.saw_call(command, params)
|
|
673
699
|
var answer: Variant = await handler.call(params)
|
|
674
700
|
return answer
|
|
675
701
|
|
|
@@ -723,13 +749,19 @@ func _send_error(client: StreamPeerTCP, request_id: Variant, message: String) ->
|
|
|
723
749
|
## auto_accept_quit off and decides for itself, and one whose player answered No went on running
|
|
724
750
|
## with no runtime and no announcement, so every later call said no game was running. A quit it
|
|
725
751
|
## does take ends the tree, which [method _exit_tree] cleans up after.
|
|
752
|
+
##
|
|
753
|
+
## One line for every notification, and the last this node runs in a frame, after [method _process]:
|
|
754
|
+
## a pause that stops in this addon is let go to a breakpoint here and stepped once into the game's
|
|
755
|
+
## own script, rather than stepped through the frame a line at a time.
|
|
726
756
|
func _notification(what: int) -> void:
|
|
727
|
-
if what
|
|
728
|
-
return
|
|
729
|
-
if not is_inside_tree() or get_tree().auto_accept_quit:
|
|
757
|
+
if what == NOTIFICATION_WM_CLOSE_REQUEST and _takes_the_close():
|
|
730
758
|
_cleanup()
|
|
731
759
|
|
|
732
760
|
|
|
761
|
+
func _takes_the_close() -> bool:
|
|
762
|
+
return not is_inside_tree() or get_tree().auto_accept_quit
|
|
763
|
+
|
|
764
|
+
|
|
733
765
|
func _cleanup() -> void:
|
|
734
766
|
for client: StreamPeerTCP in _clients:
|
|
735
767
|
client.disconnect_from_host()
|
|
@@ -172,6 +172,11 @@ func _cut_asked(params: Dictionary, in_window: bool) -> Dictionary:
|
|
|
172
172
|
return _refused(shape % JSON.stringify(region_given))
|
|
173
173
|
asked["region"] = region
|
|
174
174
|
if not node_path.is_empty():
|
|
175
|
+
# Before any refusal of the window: a headless game refused a path that named nothing for
|
|
176
|
+
# having no window, and the wrong path was found only after a restart with one.
|
|
177
|
+
var standing: Dictionary = GdhValues.node_at(_host.get_tree().root, node_path)
|
|
178
|
+
if standing.has("message"):
|
|
179
|
+
return standing
|
|
175
180
|
asked["node_path"] = node_path
|
|
176
181
|
return asked
|
|
177
182
|
|