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) + "."
|
|
@@ -9,7 +9,13 @@ const GdhResourceFiles = preload("resource_files.gd")
|
|
|
9
9
|
## What a reverse search reads unless told otherwise: the resource and code files, and the settings
|
|
10
10
|
## files that name resources, "godot" for project.godot, "cfg" for plugin.cfg and export_presets.cfg,
|
|
11
11
|
## and "import" for the sidecars that name import scripts and external materials.
|
|
12
|
-
const
|
|
12
|
+
const PROJECT_FILE: String = "res://project.godot"
|
|
13
|
+
const DEFAULT_USAGE_KINDS: Array[String] = [
|
|
14
|
+
"tscn", "tres", "gd", "gdshader", "gdshaderinc", "godot", "cfg", "import"
|
|
15
|
+
]
|
|
16
|
+
## The files written in the shader language, whose comments are // and /* */ rather than #, which in a
|
|
17
|
+
## shader starts a preprocessor directive.
|
|
18
|
+
const SHADER_KINDS: Array[String] = ["gdshader", "gdshaderinc"]
|
|
13
19
|
## One string literal of either quote, its contents captured by the one it opened with.
|
|
14
20
|
const STRING_LITERAL: String = "\"((?:[^\"\\\\]|\\\\.)*)\"|'((?:[^'\\\\]|\\\\.)*)'"
|
|
15
21
|
|
|
@@ -19,6 +25,9 @@ const REFERENCE_PATTERNS: Array[String] = [
|
|
|
19
25
|
'preload\\("([^"]+)"\\)',
|
|
20
26
|
'load\\("([^"]+)"\\)',
|
|
21
27
|
'ext_resource.*path="([^"]+)"',
|
|
28
|
+
# The shader preprocessor reads a relative include against the including file, and only the bare
|
|
29
|
+
# res:// form was read, so a shader including its sibling depended on nothing.
|
|
30
|
+
'#include\\s+"([^"]+)"',
|
|
22
31
|
]
|
|
23
32
|
|
|
24
33
|
var _log: GdhLog
|
|
@@ -39,11 +48,17 @@ class GdhDependencyWalk:
|
|
|
39
48
|
var visited: Dictionary = {}
|
|
40
49
|
var path_stack: Array[String] = []
|
|
41
50
|
var circular_references: Array
|
|
51
|
+
## Whether a file the depth stops at is read for how many files it names, which the walk doing
|
|
52
|
+
## that reading does not do again for those.
|
|
53
|
+
var counts_beyond: bool
|
|
42
54
|
|
|
43
|
-
func _init(
|
|
55
|
+
func _init(
|
|
56
|
+
p_max_depth: int, p_include_built_in: bool, p_circular_references: Array, p_counts_beyond: bool = true
|
|
57
|
+
) -> void:
|
|
44
58
|
max_depth = p_max_depth
|
|
45
59
|
include_built_in = p_include_built_in
|
|
46
60
|
circular_references = p_circular_references
|
|
61
|
+
counts_beyond = p_counts_beyond
|
|
47
62
|
|
|
48
63
|
|
|
49
64
|
func _init(p_log: GdhLog) -> void:
|
|
@@ -127,6 +142,10 @@ func missing_references() -> Array[Dictionary]:
|
|
|
127
142
|
var naming: Array = missing.get_or_add(path, [])
|
|
128
143
|
if not file in naming:
|
|
129
144
|
naming.append(file)
|
|
145
|
+
for path: String in _named_by_settings():
|
|
146
|
+
var naming: Array = missing.get_or_add(path, [])
|
|
147
|
+
if not PROJECT_FILE in naming:
|
|
148
|
+
naming.append(PROJECT_FILE)
|
|
130
149
|
# Sorted, since the walk meets files in the order the filesystem lists them, which differs
|
|
131
150
|
# between platforms.
|
|
132
151
|
var paths: Array = missing.keys()
|
|
@@ -139,6 +158,23 @@ func missing_references() -> Array[Dictionary]:
|
|
|
139
158
|
return found
|
|
140
159
|
|
|
141
160
|
|
|
161
|
+
## The files project.godot's settings name that are not on disk: an autoload, a registered
|
|
162
|
+
## translation, an icon, an enabled plugin. Not the main scene, which has a check of its own, and
|
|
163
|
+
## the engine reports each of the rest on every boot.
|
|
164
|
+
static func _named_by_settings() -> Array[String]:
|
|
165
|
+
var named: Array[String] = []
|
|
166
|
+
for line: String in FileAccess.get_file_as_string(PROJECT_FILE).split("\n"):
|
|
167
|
+
var held: String = line.strip_edges()
|
|
168
|
+
if held.begins_with(";") or held.begins_with("run/main_scene="):
|
|
169
|
+
continue
|
|
170
|
+
for found: RegExMatch in GdhPatterns.compiled('"\\*?(res://[^"]+)"').search_all(held):
|
|
171
|
+
var path: String = found.get_string(1)
|
|
172
|
+
if FileAccess.file_exists(path) or DirAccess.dir_exists_absolute(path):
|
|
173
|
+
continue
|
|
174
|
+
named.append(path)
|
|
175
|
+
return named
|
|
176
|
+
|
|
177
|
+
|
|
142
178
|
## One line for [param entry], one of [method missing_references].
|
|
143
179
|
static func missing_line(entry: Dictionary) -> String:
|
|
144
180
|
var referring: Array = entry.get("referenced_by", [])
|
|
@@ -197,11 +233,18 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
197
233
|
var file: FileAccess = FileAccess.open(file_path, FileAccess.READ)
|
|
198
234
|
if not file:
|
|
199
235
|
continue
|
|
200
|
-
var
|
|
236
|
+
var text: String = file.get_as_text()
|
|
201
237
|
file.close()
|
|
202
|
-
|
|
203
|
-
# name
|
|
204
|
-
|
|
238
|
+
var lines: PackedStringArray = text.split("\n")
|
|
239
|
+
# A class is used by name only in a script's code. In a scene or a resource the same word is a
|
|
240
|
+
# node's name or a caption, and each was counted as a use of the class; a shader has no use
|
|
241
|
+
# for one at all.
|
|
242
|
+
var code: bool = file_path.get_extension() == "gd"
|
|
243
|
+
# In a shader # opens a directive, so an #include was answered as a comment, and its
|
|
244
|
+
# comments are // and /* */.
|
|
245
|
+
var shader_lines: PackedStringArray = []
|
|
246
|
+
if file_path.get_extension() in SHADER_KINDS:
|
|
247
|
+
shader_lines = _shader_code(text).split("\n")
|
|
205
248
|
|
|
206
249
|
var references: Array[Dictionary] = []
|
|
207
250
|
for i: int in range(lines.size()):
|
|
@@ -222,6 +265,10 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
222
265
|
continue
|
|
223
266
|
var trimmed: String = line.strip_edges()
|
|
224
267
|
var kind: String = ""
|
|
268
|
+
var extra: Dictionary = {}
|
|
269
|
+
var loaded: String = _loaded_by_uid(line) if trimmed.begins_with("[ext_resource") else ""
|
|
270
|
+
var written: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(line)
|
|
271
|
+
var written_path: String = "" if written == null else _resolved(written.get_string(1), file_path)
|
|
225
272
|
# A mention in a comment is not a use, and both were answered as one. The question a
|
|
226
273
|
# reverse walk is asked is whether anything still uses a class, and a project that
|
|
227
274
|
# documents itself names its collaborators in `##` links: four references to one class,
|
|
@@ -232,8 +279,16 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
232
279
|
kind = "doc"
|
|
233
280
|
elif (code and trimmed.begins_with("#")) or name_in_comment:
|
|
234
281
|
kind = "comment"
|
|
282
|
+
elif not shader_lines.is_empty() and not _names_target(shader_lines[i], file_path, target):
|
|
283
|
+
kind = "comment"
|
|
284
|
+
elif by_its_path and not loaded.is_empty() and loaded != target["path"]:
|
|
285
|
+
# The engine loads the file the uid names, so the path written here is used by nothing.
|
|
286
|
+
kind = "stale_path"
|
|
287
|
+
extra["loads"] = loaded
|
|
235
288
|
elif by_its_path:
|
|
236
289
|
kind = _path_reference_kind(line, file_path)
|
|
290
|
+
if not loaded.is_empty() and written_path != loaded:
|
|
291
|
+
extra["stale_path"] = written_path
|
|
237
292
|
elif by_its_autoload:
|
|
238
293
|
kind = "autoload"
|
|
239
294
|
else:
|
|
@@ -242,9 +297,11 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
242
297
|
if GdhPatterns.without_annotations(line).begins_with("extends ")
|
|
243
298
|
else "class_name"
|
|
244
299
|
)
|
|
245
|
-
|
|
300
|
+
var found: Dictionary = {"line": i + 1, "kind": kind, "text": trimmed}
|
|
301
|
+
found.merge(extra)
|
|
302
|
+
references.append(found)
|
|
246
303
|
by_kind[kind] = GdhRead.as_int(by_kind.get(kind, 0)) + 1
|
|
247
|
-
if kind
|
|
304
|
+
if not kind in ["doc", "comment", "stale_path"]:
|
|
248
305
|
in_code += 1
|
|
249
306
|
|
|
250
307
|
if not references.is_empty():
|
|
@@ -388,6 +445,8 @@ func _path_reference_kind(line: String, file_path: String) -> String:
|
|
|
388
445
|
return "extends"
|
|
389
446
|
if trimmed.begins_with("[ext_resource"):
|
|
390
447
|
return "ext_resource"
|
|
448
|
+
if trimmed.begins_with("#include"):
|
|
449
|
+
return "include"
|
|
391
450
|
if "preload(" in trimmed:
|
|
392
451
|
return "preload"
|
|
393
452
|
if "load(" in trimmed:
|
|
@@ -429,13 +488,10 @@ func _gather_missing(deps: Array[Dictionary], parent: String, into: Dictionary)
|
|
|
429
488
|
_gather_missing(deeper, path, into)
|
|
430
489
|
|
|
431
490
|
|
|
432
|
-
# A circular entry is not counted: it marks that the entry holding it was reached again, which that
|
|
433
|
-
# entry already counts as one reference.
|
|
434
491
|
func _count_recursive(deps: Array[Dictionary]) -> int:
|
|
435
492
|
var count: int = 0
|
|
436
493
|
for dep: Dictionary in deps:
|
|
437
|
-
|
|
438
|
-
count += 1
|
|
494
|
+
count += 1
|
|
439
495
|
if dep.has("dependencies"):
|
|
440
496
|
var deeper: Array[Dictionary] = dep["dependencies"]
|
|
441
497
|
count += _count_recursive(deeper)
|
|
@@ -448,13 +504,6 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
448
504
|
if current_depth >= walk.max_depth:
|
|
449
505
|
return deps
|
|
450
506
|
|
|
451
|
-
if path in walk.path_stack:
|
|
452
|
-
var cycle: Array[String] = walk.path_stack.slice(walk.path_stack.find(path))
|
|
453
|
-
cycle.append(path)
|
|
454
|
-
if not cycle in walk.circular_references:
|
|
455
|
-
walk.circular_references.append(cycle)
|
|
456
|
-
return [{"path": path, "circular": true}]
|
|
457
|
-
|
|
458
507
|
# Reused only when it was walked with as much depth left as now. A resource first reached deep in
|
|
459
508
|
# the walk has a list cut short at the limit, and handing that to a nearer reach answered with a
|
|
460
509
|
# chain that stopped where nothing stopped it.
|
|
@@ -476,13 +525,15 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
476
525
|
# about `["res://addons/x/plugin.cfg"]` was a dependency of every project with the addon.
|
|
477
526
|
if path.get_extension() == "gd":
|
|
478
527
|
content = _code_lines(content)
|
|
528
|
+
elif path.get_extension() in SHADER_KINDS:
|
|
529
|
+
content = _shader_code(content)
|
|
479
530
|
var moved: Dictionary = _moved_by_uid(content)
|
|
480
531
|
|
|
481
532
|
for pattern: String in REFERENCE_PATTERNS:
|
|
482
533
|
var regex: RegEx = GdhPatterns.compiled(pattern)
|
|
483
534
|
for m: RegExMatch in regex.search_all(content):
|
|
484
|
-
var
|
|
485
|
-
dep_path = str(moved.get(
|
|
535
|
+
var written: String = _resolved(_referenced_path(m.get_string()), path)
|
|
536
|
+
var dep_path: String = str(moved.get(written, written))
|
|
486
537
|
|
|
487
538
|
if not dep_path.begins_with("res://"):
|
|
488
539
|
continue
|
|
@@ -497,7 +548,14 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
497
548
|
if not walk.include_built_in and dep_path.begins_with("res://."):
|
|
498
549
|
continue
|
|
499
550
|
|
|
500
|
-
_depend(
|
|
551
|
+
_depend(
|
|
552
|
+
deps,
|
|
553
|
+
path,
|
|
554
|
+
dep_path,
|
|
555
|
+
{"stale_path": written} if dep_path != written else {},
|
|
556
|
+
current_depth,
|
|
557
|
+
walk
|
|
558
|
+
)
|
|
501
559
|
|
|
502
560
|
# A script uses another by its class_name as surely as by a preload, and only the reverse walk
|
|
503
561
|
# looked for that: asked what a script depends on, the answer left out the script it typed a
|
|
@@ -518,7 +576,8 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
518
576
|
|
|
519
577
|
# Adds [param dep_path] to [param deps], what [param path] depends on, walked as far as the depth left
|
|
520
578
|
# allows, once however many times it is named. [param named_by] is the class or autoload name it was
|
|
521
|
-
# named by, as by_class or by_autoload,
|
|
579
|
+
# named by, as by_class or by_autoload, and the path written when the engine loads another, as
|
|
580
|
+
# stale_path; empty for a path that is the file.
|
|
522
581
|
func _depend(
|
|
523
582
|
deps: Array[Dictionary],
|
|
524
583
|
path: String,
|
|
@@ -534,10 +593,27 @@ func _depend(
|
|
|
534
593
|
return
|
|
535
594
|
var dep_info: Dictionary = {"path": dep_path, "exists": FileAccess.file_exists(dep_path)}
|
|
536
595
|
dep_info.merge(named_by)
|
|
596
|
+
# Marked where the reference closing the cycle is. Marked inside the file reached again, the tree
|
|
597
|
+
# answered that file as a dependency of itself.
|
|
598
|
+
if dep_path in walk.path_stack:
|
|
599
|
+
var cycle: Array[String] = walk.path_stack.slice(walk.path_stack.find(dep_path))
|
|
600
|
+
cycle.append(dep_path)
|
|
601
|
+
if not cycle in walk.circular_references:
|
|
602
|
+
walk.circular_references.append(cycle)
|
|
603
|
+
dep_info["circular"] = true
|
|
604
|
+
deps.append(dep_info)
|
|
605
|
+
return
|
|
537
606
|
if dep_info["exists"] and current_depth + 1 < walk.max_depth:
|
|
538
607
|
var sub_deps: Array[Dictionary] = _analyze_resource(dep_path, current_depth + 1, walk)
|
|
539
608
|
if sub_deps.size() > 0:
|
|
540
609
|
dep_info["dependencies"] = sub_deps
|
|
610
|
+
elif dep_info["exists"] and walk.counts_beyond:
|
|
611
|
+
# A file the depth stopped at read the same as one naming nothing, and a missing file past it
|
|
612
|
+
# went unsaid.
|
|
613
|
+
var beyond: GdhDependencyWalk = GdhDependencyWalk.new(1, walk.include_built_in, [], false)
|
|
614
|
+
var named: int = _analyze_resource(dep_path, 0, beyond).size()
|
|
615
|
+
if named > 0:
|
|
616
|
+
dep_info["beyond_depth"] = named
|
|
541
617
|
deps.append(dep_info)
|
|
542
618
|
|
|
543
619
|
|
|
@@ -611,34 +687,71 @@ static func _resolved(written: String, from: String) -> String:
|
|
|
611
687
|
return from.get_base_dir().path_join(written).simplify_path()
|
|
612
688
|
|
|
613
689
|
|
|
614
|
-
# The
|
|
615
|
-
# The ext_resource
|
|
616
|
-
#
|
|
617
|
-
#
|
|
618
|
-
# one included, so the paths are mapped before any of them runs.
|
|
690
|
+
# The ext_resource paths in [param content] whose uid leads elsewhere, each with the file the uid
|
|
691
|
+
# names. The engine loads an ext_resource by its uid whenever the uid is known, whatever the path
|
|
692
|
+
# says, so a file moved outside the editor still loads, and the walk called it missing; every pattern
|
|
693
|
+
# reads the old path, the bare res:// one included, so the paths are mapped before any of them runs.
|
|
619
694
|
static func _moved_by_uid(content: String) -> Dictionary:
|
|
620
695
|
var moved: Dictionary = {}
|
|
621
696
|
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
622
|
-
var
|
|
623
|
-
var at: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(
|
|
624
|
-
|
|
625
|
-
if at == null or uid == null or FileAccess.file_exists(at.get_string(1)):
|
|
626
|
-
continue
|
|
627
|
-
var id: int = ResourceUID.text_to_id(uid.get_string(1))
|
|
628
|
-
if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id):
|
|
629
|
-
continue
|
|
630
|
-
var found: String = ResourceUID.get_id_path(id)
|
|
631
|
-
if FileAccess.file_exists(found):
|
|
697
|
+
var found: String = _loaded_by_uid(line.get_string())
|
|
698
|
+
var at: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(line.get_string())
|
|
699
|
+
if at != null and not found.is_empty() and found != at.get_string(1):
|
|
632
700
|
moved[at.get_string(1)] = found
|
|
633
701
|
return moved
|
|
634
702
|
|
|
635
703
|
|
|
704
|
+
# The file an ext_resource [param entry] is loaded from by its uid, or empty when it carries none
|
|
705
|
+
# the project knows, and the engine falls back to the path.
|
|
706
|
+
static func _loaded_by_uid(entry: String) -> String:
|
|
707
|
+
var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
|
|
708
|
+
if uid == null:
|
|
709
|
+
return ""
|
|
710
|
+
var id: int = ResourceUID.text_to_id(uid.get_string(1))
|
|
711
|
+
if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id):
|
|
712
|
+
return ""
|
|
713
|
+
var found: String = ResourceUID.get_id_path(id)
|
|
714
|
+
return found if FileAccess.file_exists(found) else ""
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
# [param content], a shader's code, with its // and /* */ comments emptied and its lines kept, so a
|
|
718
|
+
# path written in one is no include.
|
|
719
|
+
static func _shader_code(content: String) -> String:
|
|
720
|
+
var kept: Array[String] = []
|
|
721
|
+
var length: int = content.length()
|
|
722
|
+
var copied: int = 0
|
|
723
|
+
var at: int = 0
|
|
724
|
+
while at < length:
|
|
725
|
+
var pair: String = content.substr(at, 2)
|
|
726
|
+
if content[at] == '"':
|
|
727
|
+
# Stepped over whole, since an include's res:// holds the // that opens a comment.
|
|
728
|
+
var close: int = content.find('"', at + 1)
|
|
729
|
+
var line_end: int = content.find("\n", at + 1)
|
|
730
|
+
var ends: int = close + 1 if close != -1 else length
|
|
731
|
+
at = line_end if line_end != -1 and line_end < ends else ends
|
|
732
|
+
elif pair == "//" or pair == "/*":
|
|
733
|
+
kept.append(content.substr(copied, at - copied))
|
|
734
|
+
var block: bool = pair == "/*"
|
|
735
|
+
var close: int = content.find("*/" if block else "\n", at + 2)
|
|
736
|
+
var resumes: int = length if close == -1 else (close + 2 if block else close)
|
|
737
|
+
if block:
|
|
738
|
+
kept.append("\n".repeat(content.substr(at, resumes - at).count("\n")))
|
|
739
|
+
at = resumes
|
|
740
|
+
copied = at
|
|
741
|
+
else:
|
|
742
|
+
at += 1
|
|
743
|
+
kept.append(content.substr(copied))
|
|
744
|
+
return "".join(PackedStringArray(kept))
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# The path inside a preload(), load(), #include or ext_resource match; a bare res:// match is the path.
|
|
748
|
+
# An ext_resource is read first, since its type or its path can hold the word load.
|
|
636
749
|
func _referenced_path(matched: String) -> String:
|
|
637
750
|
var expression: String = ""
|
|
638
|
-
if
|
|
639
|
-
expression = '"([^"]+)"'
|
|
640
|
-
elif "ext_resource" in matched:
|
|
751
|
+
if matched.begins_with("ext_resource"):
|
|
641
752
|
expression = 'path="([^"]+)"'
|
|
753
|
+
elif "load" in matched or matched.begins_with("#include"):
|
|
754
|
+
expression = '"([^"]+)"'
|
|
642
755
|
else:
|
|
643
756
|
return matched
|
|
644
757
|
var inner: RegEx = GdhPatterns.compiled(expression)
|