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.
Files changed (78) hide show
  1. package/build/cli.js +4668 -3229
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
  7. package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
  8. package/build/godot/addons/gdharness_editor/reading.gd +112 -6
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
  10. package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
  11. package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
  13. package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
  14. package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
  17. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  20. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  23. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
  25. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  26. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
  27. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  28. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  29. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
  30. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  31. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
  32. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  33. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  35. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  36. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  37. package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
  38. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  40. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
  41. package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
  42. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  43. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  44. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
  45. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
  46. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  47. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
  49. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  50. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  51. package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
  52. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  53. package/build/godot/operations/animation_reads.gd +115 -4
  54. package/build/godot/operations/audio_buses.gd +93 -7
  55. package/build/godot/operations/classdb_queries.gd +119 -7
  56. package/build/godot/operations/declared_values.gd +272 -0
  57. package/build/godot/operations/dependencies.gd +154 -41
  58. package/build/godot/operations/editor_tools.gd +29 -13
  59. package/build/godot/operations/export_presets.gd +602 -4
  60. package/build/godot/operations/file_reads.gd +115 -18
  61. package/build/godot/operations/gdscript_analysis.gd +14 -0
  62. package/build/godot/operations/gdscript_authoring.gd +12 -2
  63. package/build/godot/operations/godot_operations.gd +18 -2
  64. package/build/godot/operations/import_pipeline.gd +20 -221
  65. package/build/godot/operations/input_actions.gd +127 -51
  66. package/build/godot/operations/project_config.gd +113 -68
  67. package/build/godot/operations/project_diagnostics.gd +4 -0
  68. package/build/godot/operations/project_validation.gd +266 -0
  69. package/build/godot/operations/property_paths.gd +89 -16
  70. package/build/godot/operations/property_values.gd +180 -121
  71. package/build/godot/operations/reading.gd +112 -6
  72. package/build/godot/operations/scene_connections.gd +222 -0
  73. package/build/godot/operations/serialisation.gd +19 -29
  74. package/build/godot/operations/setting_reads.gd +121 -0
  75. package/build/godot/operations/value_shapes.gd +128 -1
  76. package/build/godot/tests/listed.gd +20 -13
  77. package/build/keeper.js +1402 -707
  78. package/package.json +1 -1
@@ -5,6 +5,7 @@ extends RefCounted
5
5
  ## stores, which runs no script of the scene's, so a read answers the same either way.
6
6
 
7
7
  const GdhFileReads = preload("file_reads.gd")
8
+ const GdhRead = preload("reading.gd")
8
9
  const GdhSerialisation = preload("serialisation.gd")
9
10
 
10
11
  const LOOP_MODE_NAMES: Dictionary[int, String] = {
@@ -28,6 +29,11 @@ const SWITCH_MODE_NAMES: Dictionary[int, String] = {
28
29
  AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC: "sync",
29
30
  AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END: "at_end",
30
31
  }
32
+ const ADVANCE_MODE_NAMES: Dictionary[int, String] = {
33
+ AnimationNodeStateMachineTransition.ADVANCE_MODE_DISABLED: "disabled",
34
+ AnimationNodeStateMachineTransition.ADVANCE_MODE_ENABLED: "enabled",
35
+ AnimationNodeStateMachineTransition.ADVANCE_MODE_AUTO: "auto",
36
+ }
31
37
 
32
38
 
33
39
  ## [method animations] of the AnimationPlayer at [param player_path] in the file [param scene_path].
@@ -47,8 +53,21 @@ static func scene_states(scene_path: String, tree_path: String, state_machine_pa
47
53
  if made[0] == null:
48
54
  return made[1]
49
55
  var tree: AnimationTree = made[0]
50
- var answer: Dictionary = states(tree, state_machine_path)
56
+ # The player the tree plays from, read from the file the same way, since a bare tree has no
57
+ # siblings to find it among.
58
+ # A path above the root names no node the file has, and finds none, as the editor finds none.
59
+ var player: AnimationPlayer = null
60
+ var player_path: String = ""
61
+ if not str(tree.anim_player).is_empty():
62
+ player_path = (tree_path + "/" + str(tree.anim_player)).simplify_path()
63
+ if player_path.is_empty():
64
+ player_path = "."
65
+ var found: Array = _bare(scene_path, player_path, "AnimationPlayer")
66
+ player = found[0]
67
+ var answer: Dictionary = states(tree, state_machine_path, player, player_path)
51
68
  tree.free()
69
+ if player != null:
70
+ player.free()
52
71
  return answer
53
72
 
54
73
 
@@ -200,8 +219,28 @@ static func no_machine(tree: AnimationTree, state_machine_path: String) -> Strin
200
219
  return "No state machine at " + state_machine_path
201
220
 
202
221
 
222
+ ## Why a state playing [param animation_name] from [param player], at [param player_path] from the
223
+ ## scene root as playerNodePath takes it, plays nothing yet. anim_player is relative to the tree
224
+ ## ("../Anim"), which no argument takes.
225
+ static func missing_animation(player: AnimationPlayer, player_path: String, animation_name: String) -> String:
226
+ var said: String = (
227
+ "The AnimationPlayer at %s has no animation %s yet, so the state plays nothing until it has one."
228
+ % [player_path, animation_name]
229
+ )
230
+ var close: PackedStringArray = GdhRead.nearest(animation_name, player.get_animation_list())
231
+ if close.is_empty():
232
+ return said
233
+ return (
234
+ said + " The nearest %s %s." % ["it has is" if close.size() == 1 else "it has are", ", ".join(close)]
235
+ )
236
+
237
+
203
238
  ## The states and transitions of the state machine at [param state_machine_path] in [param tree].
204
- static func states(tree: AnimationTree, state_machine_path: String) -> Dictionary:
239
+ ## A state playing an animation says whether [param player], at [param player_path], has it, as
240
+ ## add_state answers: a state naming one it lacks plays nothing, and says so only in a log.
241
+ static func states(
242
+ tree: AnimationTree, state_machine_path: String, player: AnimationPlayer, player_path: String
243
+ ) -> Dictionary:
205
244
  var sm: AnimationNodeStateMachine = state_machine(tree, state_machine_path)
206
245
  if sm == null:
207
246
  return {"ok": false, "error": no_machine(tree, state_machine_path)}
@@ -212,6 +251,10 @@ static func states(tree: AnimationTree, state_machine_path: String) -> Dictionar
212
251
  if node is AnimationNodeAnimation:
213
252
  var plays: AnimationNodeAnimation = node
214
253
  entry["animationName"] = str(plays.animation)
254
+ var found: bool = player != null and player.has_animation(plays.animation)
255
+ entry["animationFound"] = found
256
+ if player != null and not found:
257
+ entry["animationMissing"] = missing_animation(player, player_path, str(plays.animation))
215
258
  listed.append(entry)
216
259
  var transitions: Array[Dictionary] = []
217
260
  for index: int in sm.get_transition_count():
@@ -220,8 +263,76 @@ static func states(tree: AnimationTree, state_machine_path: String) -> Dictionar
220
263
  "from": str(sm.get_transition_from(index)),
221
264
  "to": str(sm.get_transition_to(index)),
222
265
  "transitionType": SWITCH_MODE_NAMES.get(transition.switch_mode, str(transition.switch_mode)),
266
+ "advanceMode": ADVANCE_MODE_NAMES.get(transition.advance_mode, str(transition.advance_mode)),
223
267
  }
224
- if not str(transition.advance_condition).is_empty():
225
- shaped["advanceCondition"] = str(transition.advance_condition)
268
+ var condition: String = str(transition.advance_condition)
269
+ if not condition.is_empty():
270
+ shaped["advanceCondition"] = condition
271
+ # A condition is checked only on an auto transition, and the default is enabled: a machine
272
+ # with its conditions set true stayed where it was, and nothing said why.
273
+ if not condition.is_empty() and transition.advance_mode != transition.ADVANCE_MODE_AUTO:
274
+ shaped["conditionNote"] = (
275
+ (
276
+ "advanceMode is %s, so %s never advances it: the engine checks a condition only on an"
277
+ + " auto transition%s. disconnect_states and connect_states again with the condition"
278
+ + " write it as auto"
279
+ )
280
+ % [
281
+ shaped["advanceMode"],
282
+ condition,
283
+ (
284
+ ", and travel() still takes this one"
285
+ if transition.advance_mode == transition.ADVANCE_MODE_ENABLED
286
+ else ""
287
+ )
288
+ ]
289
+ )
226
290
  transitions.append(shaped)
227
291
  return {"ok": true, "states": listed, "transitions": transitions}
292
+
293
+
294
+ ## The states of the AnimationTrees under [param root] that play [param animation_name] from
295
+ ## [param player], as [[node, {animTreePath, state}]], a state named by the path of the nodes leading
296
+ ## to it.
297
+ static func states_playing(root: Node, player: AnimationPlayer, animation_name: String) -> Array:
298
+ var found: Array = []
299
+ var trees: Array[Node] = root.find_children("*", "AnimationTree", true, false)
300
+ if root is AnimationTree:
301
+ trees.append(root)
302
+ for node: Node in trees:
303
+ var tree: AnimationTree = node
304
+ if tree.tree_root == null or tree.get_node_or_null(tree.anim_player) != player:
305
+ continue
306
+ var tree_path: String = str(root.get_path_to(tree))
307
+ _collect_playing(tree.tree_root, "", tree_path, animation_name, found)
308
+ return found
309
+
310
+
311
+ static func _collect_playing(
312
+ node: AnimationNode, at: String, tree_path: String, animation_name: String, found: Array
313
+ ) -> void:
314
+ if node is AnimationNodeAnimation:
315
+ var plays: AnimationNodeAnimation = node
316
+ if plays.animation == StringName(animation_name):
317
+ found.append([plays, {"animTreePath": tree_path, "state": "." if at.is_empty() else at}])
318
+ return
319
+ var children: Dictionary = {}
320
+ if node is AnimationNodeStateMachine:
321
+ var machine: AnimationNodeStateMachine = node
322
+ for state: StringName in machine.get_node_list():
323
+ children[str(state)] = machine.get_node(state)
324
+ elif node is AnimationNodeBlendTree:
325
+ var blend: AnimationNodeBlendTree = node
326
+ for input: StringName in blend.get_node_list():
327
+ children[str(input)] = blend.get_node(input)
328
+ elif node is AnimationNodeBlendSpace1D:
329
+ var line: AnimationNodeBlendSpace1D = node
330
+ for point: int in line.get_blend_point_count():
331
+ children[str(point)] = line.get_blend_point_node(point)
332
+ elif node is AnimationNodeBlendSpace2D:
333
+ var plane: AnimationNodeBlendSpace2D = node
334
+ for point: int in plane.get_blend_point_count():
335
+ children[str(point)] = plane.get_blend_point_node(point)
336
+ for step: String in children:
337
+ var child: AnimationNode = children[step]
338
+ _collect_playing(child, step if at.is_empty() else at + "/" + step, tree_path, animation_name, found)
@@ -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) + "."