gdharness 1.3.47 → 1.3.49
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 +589 -229
- package/build/godot/addons/gdharness_editor/animation_reads.gd +20 -0
- package/build/godot/addons/gdharness_editor/declared_values.gd +37 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +6 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +8 -11
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +19 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +42 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +1 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +8 -1
- package/build/godot/addons/gdharness_editor/value_shapes.gd +9 -0
- package/build/godot/addons/gdharness_runtime/declared_values.gd +37 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +32 -9
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +3 -2
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +9 -7
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +72 -11
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +21 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +21 -8
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +3 -2
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +1 -1
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +9 -0
- package/build/godot/operations/animation_reads.gd +20 -0
- package/build/godot/operations/declared_values.gd +37 -0
- package/build/godot/operations/dependencies.gd +65 -9
- package/build/godot/operations/file_reads.gd +6 -0
- package/build/godot/operations/import_pipeline.gd +10 -2
- package/build/godot/operations/project_config.gd +35 -0
- package/build/godot/operations/property_values.gd +8 -11
- package/build/godot/operations/resource_files.gd +1 -2
- package/build/godot/operations/value_shapes.gd +9 -0
- package/build/godot/tests/listed.gd +25 -0
- package/build/keeper.js +1312 -1026
- package/package.json +1 -1
|
@@ -61,9 +61,16 @@ func get_tree(params: Dictionary) -> Dictionary:
|
|
|
61
61
|
return reached
|
|
62
62
|
var root: Node = reached["node"]
|
|
63
63
|
|
|
64
|
+
# A game whose first scene hands over to another shows a tree that names neither, so which
|
|
65
|
+
# scene is showing was guessed from the project's documentation.
|
|
66
|
+
var scene: Node = _host.get_tree().current_scene
|
|
67
|
+
var showing: Variant = null
|
|
68
|
+
if scene != null:
|
|
69
|
+
showing = { "path": str(scene.get_path()), "file": scene.scene_file_path }
|
|
64
70
|
return {
|
|
65
71
|
"type": "tree",
|
|
66
72
|
"root": _serialize_node_tree(root, 0, max_depth, include_properties, named),
|
|
73
|
+
"current_scene": showing,
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
|
|
@@ -600,7 +607,7 @@ func get_rect(params: Dictionary) -> Dictionary:
|
|
|
600
607
|
if node is Control:
|
|
601
608
|
var control: Control = node
|
|
602
609
|
var canvas_rect: Rect2 = control.get_global_rect()
|
|
603
|
-
|
|
610
|
+
var answer: Dictionary = {
|
|
604
611
|
"type": "rect",
|
|
605
612
|
"path": node_path,
|
|
606
613
|
"visible": control.is_visible_in_tree(),
|
|
@@ -614,6 +621,19 @@ func get_rect(params: Dictionary) -> Dictionary:
|
|
|
614
621
|
)
|
|
615
622
|
),
|
|
616
623
|
}
|
|
624
|
+
# Visible and in view read as clickable for a button under a dialog covering the screen,
|
|
625
|
+
# which a click by its words refused. Loaded here, since runtime_targets preloads this
|
|
626
|
+
# script.
|
|
627
|
+
if answer["visible"] and answer["in_view"]:
|
|
628
|
+
var own: Script = get_script()
|
|
629
|
+
var targets: GDScript = load(
|
|
630
|
+
own.resource_path.get_base_dir().path_join("runtime_targets.gd")
|
|
631
|
+
)
|
|
632
|
+
var over: Variant = targets.call("drawn_over", control)
|
|
633
|
+
if over is Node:
|
|
634
|
+
var cover: Node = over
|
|
635
|
+
answer["drawn_over"] = str(cover.get_path())
|
|
636
|
+
return answer
|
|
617
637
|
if node is Node2D:
|
|
618
638
|
var item: Node2D = node
|
|
619
639
|
var canvas_position: Vector2 = item.get_global_transform_with_canvas().origin
|
|
@@ -250,7 +250,7 @@ static func control_saying(
|
|
|
250
250
|
(
|
|
251
251
|
(
|
|
252
252
|
'no %s on screen%s says exactly "%s", so nothing was clicked: %s %s says it as '
|
|
253
|
-
+ 'part of "%s", while %s; pass index %d to press
|
|
253
|
+
+ 'part of "%s", while %s; pass index %d to press %s anyway'
|
|
254
254
|
)
|
|
255
255
|
% [
|
|
256
256
|
"button" if exact_elsewhere.size() > exact_out_of_reach.size() else "control",
|
|
@@ -261,6 +261,7 @@ static func control_saying(
|
|
|
261
261
|
_shortened(lines[index]),
|
|
262
262
|
_out_of_reach_exactly(exact_elsewhere, elsewhere_as),
|
|
263
263
|
index,
|
|
264
|
+
targets[index].get_path(),
|
|
264
265
|
]
|
|
265
266
|
),
|
|
266
267
|
}
|
|
@@ -272,7 +273,7 @@ static func control_saying(
|
|
|
272
273
|
(
|
|
273
274
|
'no control on screen%s says "%s" as a word of its own, so nothing was '
|
|
274
275
|
+ 'clicked: %s %s has it only inside a longer word, "%s"; pass index %d to '
|
|
275
|
-
+ "press
|
|
276
|
+
+ "press %s anyway"
|
|
276
277
|
)
|
|
277
278
|
% [
|
|
278
279
|
under,
|
|
@@ -281,6 +282,7 @@ static func control_saying(
|
|
|
281
282
|
targets[index].get_path(),
|
|
282
283
|
_shortened(lines[index]),
|
|
283
284
|
index,
|
|
285
|
+
targets[index].get_path(),
|
|
284
286
|
]
|
|
285
287
|
),
|
|
286
288
|
}
|
|
@@ -871,8 +873,8 @@ static func _where_it_lands(
|
|
|
871
873
|
var half: Vector2 = (drawn * Rect2(Vector2.ZERO, target.size)).size * 0.5
|
|
872
874
|
var moves: Dictionary[ScrollContainer, Vector2] = {}
|
|
873
875
|
var landed: Vector2 = point
|
|
874
|
-
var walk:
|
|
875
|
-
while walk != null
|
|
876
|
+
var walk: CanvasItem = _drawn_inside(target)
|
|
877
|
+
while walk != null:
|
|
876
878
|
var holder: ScrollContainer = walk as ScrollContainer
|
|
877
879
|
if holder != null:
|
|
878
880
|
var shown: Rect2 = (
|
|
@@ -887,7 +889,7 @@ static func _where_it_lands(
|
|
|
887
889
|
if next != landed:
|
|
888
890
|
moves[holder] = next - landed
|
|
889
891
|
landed = next
|
|
890
|
-
walk = walk
|
|
892
|
+
walk = _drawn_inside(walk)
|
|
891
893
|
return moves
|
|
892
894
|
|
|
893
895
|
|
|
@@ -919,8 +921,8 @@ static func _off_screen(target: Control, aim: Vector2) -> bool:
|
|
|
919
921
|
static func _clipped_away(
|
|
920
922
|
control: Control, point: Vector2, moves: Dictionary[ScrollContainer, Vector2]
|
|
921
923
|
) -> bool:
|
|
922
|
-
var walk:
|
|
923
|
-
while walk != null
|
|
924
|
+
var walk: CanvasItem = _drawn_inside(control)
|
|
925
|
+
while walk != null:
|
|
924
926
|
var holder: Control = walk as Control
|
|
925
927
|
if (
|
|
926
928
|
holder != null
|
|
@@ -928,10 +930,21 @@ static func _clipped_away(
|
|
|
928
930
|
and not _holds(holder, point - _moved_by(holder, moves))
|
|
929
931
|
):
|
|
930
932
|
return true
|
|
931
|
-
walk = walk
|
|
933
|
+
walk = _drawn_inside(walk)
|
|
932
934
|
return false
|
|
933
935
|
|
|
934
936
|
|
|
937
|
+
## The canvas item [param item] is drawn inside, which is what can clip it and what moves it, or
|
|
938
|
+
## null where its drawing starts afresh: under a CanvasLayer, a viewport or a node that is not a
|
|
939
|
+
## canvas item, or when it is top_level. A clipping Control above a CanvasLayer clips nothing on the
|
|
940
|
+
## layer, and judged against one, every button of a reader drawn on a layer under a clipping desk
|
|
941
|
+
## was refused as off the screen where it sat above the desk's top edge.
|
|
942
|
+
static func _drawn_inside(item: CanvasItem) -> CanvasItem:
|
|
943
|
+
if item.top_level:
|
|
944
|
+
return null
|
|
945
|
+
return item.get_parent() as CanvasItem
|
|
946
|
+
|
|
947
|
+
|
|
935
948
|
## The canvas layer [param control] is drawn on, 0 for the viewport's own canvas.
|
|
936
949
|
static func _layer_of(control: Control) -> int:
|
|
937
950
|
var layer: CanvasLayer = control.get_canvas_layer_node()
|
|
@@ -8,6 +8,7 @@ const GdhPaths = preload("runtime_paths.gd")
|
|
|
8
8
|
const GdhQueries = preload("runtime_queries.gd")
|
|
9
9
|
const GdhRead = preload("reading.gd")
|
|
10
10
|
const GdhSays = preload("runtime_says.gd")
|
|
11
|
+
const GdhShapes = preload("value_shapes.gd")
|
|
11
12
|
const GdhValues = preload("runtime_values.gd")
|
|
12
13
|
## The longest one wait may last, whatever the request says: past this the server has long
|
|
13
14
|
## since given up on the reply.
|
|
@@ -490,7 +491,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
490
491
|
"%s.%s is declared to hold an object and the value to wait for is %s, which it can"
|
|
491
492
|
+ " never hold: wait for null, or for the object's own value on a property of it."
|
|
492
493
|
)
|
|
493
|
-
% [node_path, property,
|
|
494
|
+
% [node_path, property, GdhShapes.kind(wanted)]
|
|
494
495
|
)
|
|
495
496
|
if not refused.is_empty():
|
|
496
497
|
return { "type": "error", "message": refused }
|
|
@@ -679,7 +680,7 @@ static func _not_comparable(
|
|
|
679
680
|
current: Variant, wanted: Variant, node_path: String, property: String
|
|
680
681
|
) -> String:
|
|
681
682
|
var types: Array[String] = [
|
|
682
|
-
node_path, property, type_string(typeof(current)),
|
|
683
|
+
node_path, property, type_string(typeof(current)), GdhShapes.kind(wanted),
|
|
683
684
|
]
|
|
684
685
|
# A value that cannot become what the property holds is said as that, which names the remedy,
|
|
685
686
|
# before the comparison it would also fail; an object on either side is said as the comparison.
|
|
@@ -163,7 +163,7 @@ static func pressable_words(from: Node, open: Callable, most: int) -> String:
|
|
|
163
163
|
if offered.size() == most:
|
|
164
164
|
return ", ".join(offered) + ", and more"
|
|
165
165
|
offered.append(quoted)
|
|
166
|
-
var children: Array[Node] = node.get_children()
|
|
166
|
+
var children: Array[Node] = node.get_children(true)
|
|
167
167
|
for index: int in range(children.size() - 1, -1, -1):
|
|
168
168
|
pending.push_front(children[index])
|
|
169
169
|
return ", ".join(offered)
|
|
@@ -234,6 +234,15 @@ static func packed_from(items: Array, type: int, fit: Callable) -> Variant:
|
|
|
234
234
|
return type_convert(built, type)
|
|
235
235
|
|
|
236
236
|
|
|
237
|
+
## What [param value] is, as a refusal names it. A number arrives from JSON as a float whether or
|
|
238
|
+
## not it has a fraction, so [1, 2] given for a list of words was called a list of floats, and 5
|
|
239
|
+
## given for a Vector2 was called a float.
|
|
240
|
+
static func kind(value: Variant) -> String:
|
|
241
|
+
if value is int or value is float:
|
|
242
|
+
return "a number"
|
|
243
|
+
return type_string(typeof(value))
|
|
244
|
+
|
|
245
|
+
|
|
237
246
|
## The sentences a refusal of [param value] where a [param type] goes ends with: which of its keys
|
|
238
247
|
## kept it from being read as one, and how one is written. Empty for a type not built from fields.
|
|
239
248
|
##
|
|
@@ -16,6 +16,22 @@ const UPDATE_MODE_NAMES: Dictionary[int, String] = {
|
|
|
16
16
|
Animation.UPDATE_DISCRETE: "discrete",
|
|
17
17
|
Animation.UPDATE_CAPTURE: "capture",
|
|
18
18
|
}
|
|
19
|
+
const INTERPOLATION_NAMES: Dictionary[int, String] = {
|
|
20
|
+
Animation.INTERPOLATION_NEAREST: "nearest",
|
|
21
|
+
Animation.INTERPOLATION_LINEAR: "linear",
|
|
22
|
+
Animation.INTERPOLATION_CUBIC: "cubic",
|
|
23
|
+
Animation.INTERPOLATION_LINEAR_ANGLE: "linear_angle",
|
|
24
|
+
Animation.INTERPOLATION_CUBIC_ANGLE: "cubic_angle",
|
|
25
|
+
}
|
|
26
|
+
## The track types whose keys are blended between, so how they are and whether a looping animation
|
|
27
|
+
## blends its last key into its first say how the track moves.
|
|
28
|
+
const INTERPOLATED_TYPES: Array[int] = [
|
|
29
|
+
Animation.TYPE_VALUE,
|
|
30
|
+
Animation.TYPE_POSITION_3D,
|
|
31
|
+
Animation.TYPE_ROTATION_3D,
|
|
32
|
+
Animation.TYPE_SCALE_3D,
|
|
33
|
+
Animation.TYPE_BLEND_SHAPE,
|
|
34
|
+
]
|
|
19
35
|
const TRACK_TYPE_NAMES: Dictionary[int, String] = {
|
|
20
36
|
Animation.TYPE_VALUE: "property",
|
|
21
37
|
Animation.TYPE_POSITION_3D: "position_3d",
|
|
@@ -174,6 +190,10 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
|
|
|
174
190
|
if kind == Animation.TYPE_VALUE:
|
|
175
191
|
var mode: int = anim.value_track_get_update_mode(track)
|
|
176
192
|
shaped["update"] = UPDATE_MODE_NAMES.get(mode, str(mode))
|
|
193
|
+
if kind in INTERPOLATED_TYPES:
|
|
194
|
+
var interpolation: int = anim.track_get_interpolation_type(track)
|
|
195
|
+
shaped["interpolation"] = INTERPOLATION_NAMES.get(interpolation, str(interpolation))
|
|
196
|
+
shaped["loopWrap"] = anim.track_get_interpolation_loop_wrap(track)
|
|
177
197
|
var keyframes: Array[Dictionary] = []
|
|
178
198
|
var methods: Dictionary = {}
|
|
179
199
|
for key: int in anim.track_get_key_count(track):
|
|
@@ -113,6 +113,41 @@ static func engine_constants(target: Object, property: String) -> Dictionary:
|
|
|
113
113
|
return values
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
## The number [param word] names as a constant of the global enum the property [param declared] of
|
|
117
|
+
## [param target] holds, such as VERTICAL_ALIGNMENT_BOTTOM, or null when it names none. ClassDB
|
|
118
|
+
## lists no global enum and Expression reads no global constant, so a word spelt from the enum's
|
|
119
|
+
## name and a value the hint gives is compiled, and taken only when the engine reads it as that
|
|
120
|
+
## value: ORIENTATION_VERTICAL is spelt so and is not a constant, which is HORIZONTAL or VERTICAL.
|
|
121
|
+
static func _global_constant(target: Object, declared: Dictionary, word: String) -> Variant:
|
|
122
|
+
var engine_class: String = target.get_class()
|
|
123
|
+
var setter: String = str(
|
|
124
|
+
ClassDB.class_get_property_setter(engine_class, str(declared.get("name", "")))
|
|
125
|
+
)
|
|
126
|
+
var named: String = ""
|
|
127
|
+
for method: Dictionary in ClassDB.class_get_method_list(engine_class):
|
|
128
|
+
var arguments: Array = method.get("args", [])
|
|
129
|
+
if str(method.get("name", "")) == setter and not arguments.is_empty():
|
|
130
|
+
var value_argument: Dictionary = arguments[-1]
|
|
131
|
+
named = str(value_argument.get("class_name", ""))
|
|
132
|
+
break
|
|
133
|
+
if named.is_empty() or "." in named:
|
|
134
|
+
return null
|
|
135
|
+
var wanted: String = word.strip_edges().to_upper()
|
|
136
|
+
var hinted: Dictionary = enum_values(str(declared.get("hint_string", "")))
|
|
137
|
+
for value_word: String in hinted:
|
|
138
|
+
var constant: String = (
|
|
139
|
+
named.to_snake_case().to_upper() + "_" + value_word.to_upper().replace(" ", "_")
|
|
140
|
+
)
|
|
141
|
+
if constant != wanted:
|
|
142
|
+
continue
|
|
143
|
+
var reader: GDScript = GDScript.new()
|
|
144
|
+
reader.source_code = "extends RefCounted\nconst READ = %s\n" % constant
|
|
145
|
+
if (reader.reload() == OK
|
|
146
|
+
and reader.get_script_constant_map().get("READ") == hinted[value_word]):
|
|
147
|
+
return hinted[value_word]
|
|
148
|
+
return null
|
|
149
|
+
|
|
150
|
+
|
|
116
151
|
## [param word] as the number of the value the enum property [param declared] of [param target]
|
|
117
152
|
## names by it, as [true, number], or [false, why] naming the values, with [param holder] naming the
|
|
118
153
|
## target in the refusal. [false, ""] for a word that is a number, which converts as one.
|
|
@@ -132,6 +167,8 @@ static func enum_by_name(
|
|
|
132
167
|
if chosen == null:
|
|
133
168
|
constants = engine_constants(target, property)
|
|
134
169
|
chosen = enum_named(constants, word)
|
|
170
|
+
if chosen == null and constants.is_empty():
|
|
171
|
+
chosen = _global_constant(target, declared, word)
|
|
135
172
|
if chosen != null:
|
|
136
173
|
return [true, chosen]
|
|
137
174
|
if word.strip_edges().is_valid_int():
|
|
@@ -530,6 +530,20 @@ static func _comment_of(line: String) -> String:
|
|
|
530
530
|
|
|
531
531
|
# How a line that names the resource by path uses it.
|
|
532
532
|
func _path_reference_kind(line: String, file_path: String) -> String:
|
|
533
|
+
var placed: String = _placed_kind(line, file_path)
|
|
534
|
+
if not placed.is_empty():
|
|
535
|
+
return placed
|
|
536
|
+
var trimmed: String = GdhPatterns.without_annotations(line)
|
|
537
|
+
if "preload(" in trimmed:
|
|
538
|
+
return "preload"
|
|
539
|
+
if "load(" in trimmed:
|
|
540
|
+
return "load"
|
|
541
|
+
return "path"
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# The kind a reference on [param line] has by the file it is in or by how the line starts, or ""
|
|
545
|
+
# when neither says.
|
|
546
|
+
static func _placed_kind(line: String, file_path: String) -> String:
|
|
533
547
|
match file_path.get_file():
|
|
534
548
|
"project.godot":
|
|
535
549
|
return "setting"
|
|
@@ -546,11 +560,31 @@ func _path_reference_kind(line: String, file_path: String) -> String:
|
|
|
546
560
|
return "ext_resource"
|
|
547
561
|
if trimmed.begins_with("#include"):
|
|
548
562
|
return "include"
|
|
549
|
-
|
|
563
|
+
return ""
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# The kind of the reference [param found] is in [param content], read from [param path], by the
|
|
567
|
+
# call its path is handed to rather than by the line, which can name a second file another way: a
|
|
568
|
+
# path only written in a string, which nothing in the file may load, read as a load.
|
|
569
|
+
static func _forward_kind(content: String, found: RegExMatch, path: String) -> String:
|
|
570
|
+
var matched: String = found.get_string()
|
|
571
|
+
if matched.begins_with("preload("):
|
|
550
572
|
return "preload"
|
|
551
|
-
if "load("
|
|
573
|
+
if matched.begins_with("load("):
|
|
552
574
|
return "load"
|
|
553
|
-
|
|
575
|
+
var start: int = found.get_start()
|
|
576
|
+
var line_start: int = content.rfind("\n", start) + 1
|
|
577
|
+
var line_end: int = content.find("\n", start)
|
|
578
|
+
var line: String = content.substr(
|
|
579
|
+
line_start, (content.length() if line_end == -1 else line_end) - line_start
|
|
580
|
+
)
|
|
581
|
+
var before: String = content.substr(line_start, start - line_start).rstrip("\"'")
|
|
582
|
+
if before.ends_with("preload("):
|
|
583
|
+
return "preload"
|
|
584
|
+
if before.ends_with("load("):
|
|
585
|
+
return "load"
|
|
586
|
+
var placed: String = _placed_kind(line, path)
|
|
587
|
+
return "path" if placed.is_empty() else placed
|
|
554
588
|
|
|
555
589
|
|
|
556
590
|
func _regex_escaped(text: String) -> String:
|
|
@@ -633,8 +667,14 @@ func _depend(dep_info: Dictionary, current_depth: int, walk: GdhDependencyWalk)
|
|
|
633
667
|
# tree answered that file as a dependency of itself.
|
|
634
668
|
if dep_path in walk.path_stack:
|
|
635
669
|
var cycle: Array[String] = walk.path_stack.slice(walk.path_stack.find(dep_path))
|
|
636
|
-
cycle
|
|
637
|
-
|
|
670
|
+
# Kept by a key that is the same wherever the walk entered the cycle: compared as written,
|
|
671
|
+
# one cycle met from two of its files was listed twice, and comparing each against every
|
|
672
|
+
# cycle found so far grew with the square of their number, sixteen thousand on three
|
|
673
|
+
# hundred scripts typed by each other's class names.
|
|
674
|
+
var key: String = _cycle_key(cycle)
|
|
675
|
+
if not walk.cycles_seen.has(key):
|
|
676
|
+
walk.cycles_seen[key] = true
|
|
677
|
+
cycle.append(dep_path)
|
|
638
678
|
walk.circular_references.append(cycle)
|
|
639
679
|
dep_info["circular"] = true
|
|
640
680
|
return false
|
|
@@ -660,6 +700,16 @@ func _depend(dep_info: Dictionary, current_depth: int, walk: GdhDependencyWalk)
|
|
|
660
700
|
return walked["cut"]
|
|
661
701
|
|
|
662
702
|
|
|
703
|
+
## [param cycle], the files of one cycle in the order the walk met them, turned to start at the
|
|
704
|
+
## first of them in sorting order, so the cycle met from any of its files keys the same.
|
|
705
|
+
static func _cycle_key(cycle: Array[String]) -> String:
|
|
706
|
+
var first: int = 0
|
|
707
|
+
for at: int in cycle.size():
|
|
708
|
+
if cycle[at] < cycle[first]:
|
|
709
|
+
first = at
|
|
710
|
+
return "\n".join(PackedStringArray(cycle.slice(first) + cycle.slice(0, first)))
|
|
711
|
+
|
|
712
|
+
|
|
663
713
|
## What [param path] names, each file once, as the entries the tree lists it by; read once a walk.
|
|
664
714
|
func _references(path: String, walk: GdhDependencyWalk) -> Array[Dictionary]:
|
|
665
715
|
if walk.references.has(path):
|
|
@@ -688,12 +738,13 @@ func _references(path: String, walk: GdhDependencyWalk) -> Array[Dictionary]:
|
|
|
688
738
|
for m: RegExMatch in regex.search_all(content):
|
|
689
739
|
var written: String = _resolved(_referenced_path(m.get_string()), path)
|
|
690
740
|
var dep_path: String = str(moved.get(written, written))
|
|
741
|
+
var kind: String = _forward_kind(content, m, path)
|
|
691
742
|
|
|
692
743
|
# A uid the project does not know names no file, and loading it fails: passed over
|
|
693
744
|
# as no res:// path, a script's load of one went unsaid while the game printed
|
|
694
745
|
# "Unrecognized UID" at that line.
|
|
695
746
|
if dep_path.begins_with("uid://"):
|
|
696
|
-
_name(deps, path, dep_path, {})
|
|
747
|
+
_name(deps, path, dep_path, { "kind": kind })
|
|
697
748
|
continue
|
|
698
749
|
if not dep_path.begins_with("res://"):
|
|
699
750
|
continue
|
|
@@ -709,7 +760,9 @@ func _references(path: String, walk: GdhDependencyWalk) -> Array[Dictionary]:
|
|
|
709
760
|
if not walk.include_built_in and dep_path.begins_with("res://."):
|
|
710
761
|
continue
|
|
711
762
|
|
|
712
|
-
var named_by: Dictionary = { "
|
|
763
|
+
var named_by: Dictionary = { "kind": kind }
|
|
764
|
+
if dep_path != written:
|
|
765
|
+
named_by["stale_path"] = written
|
|
713
766
|
if unknown.has(dep_path):
|
|
714
767
|
named_by["unknown_uid"] = unknown[dep_path]
|
|
715
768
|
_name(deps, path, dep_path, named_by)
|
|
@@ -730,8 +783,8 @@ func _references(path: String, walk: GdhDependencyWalk) -> Array[Dictionary]:
|
|
|
730
783
|
|
|
731
784
|
# Adds [param dep_path] to [param deps], what [param path] depends on, once however many times it
|
|
732
785
|
# is named. [param named_by] is the class or autoload name it was named by, as by_class or
|
|
733
|
-
# by_autoload,
|
|
734
|
-
#
|
|
786
|
+
# by_autoload, or for a path its kind, with the path written when the engine loads another as
|
|
787
|
+
# stale_path.
|
|
735
788
|
func _name(deps: Array[Dictionary], path: String, dep_path: String, named_by: Dictionary) -> void:
|
|
736
789
|
if dep_path == path:
|
|
737
790
|
return
|
|
@@ -962,6 +1015,9 @@ class GdhDependencyWalk:
|
|
|
962
1015
|
|
|
963
1016
|
var circular_references: Array
|
|
964
1017
|
|
|
1018
|
+
## Each cycle in circular_references, by [method _cycle_key].
|
|
1019
|
+
var cycles_seen: Dictionary[String, bool] = {}
|
|
1020
|
+
|
|
965
1021
|
func _init(p_max_depth: int, p_include_built_in: bool, p_circular_references: Array) -> void:
|
|
966
1022
|
max_depth = p_max_depth
|
|
967
1023
|
include_built_in = p_include_built_in
|
|
@@ -356,6 +356,12 @@ static func _entry(
|
|
|
356
356
|
var data: Dictionary = {
|
|
357
357
|
"name": node["name"], "type": node["type"], "path": path, "children": children,
|
|
358
358
|
}
|
|
359
|
+
var given_values: Dictionary = node["properties"]
|
|
360
|
+
var attached: Variant = given_values.get("script")
|
|
361
|
+
if attached is Script:
|
|
362
|
+
var script: Script = attached
|
|
363
|
+
if not str(script.get_global_name()).is_empty():
|
|
364
|
+
data["scriptClass"] = str(script.get_global_name())
|
|
359
365
|
# The root's file is the scene being read, so only another node's names a second scene.
|
|
360
366
|
if path != "." and node.has("instanceOf"):
|
|
361
367
|
data["instanceOf"] = node["instanceOf"]
|
|
@@ -212,7 +212,7 @@ func set_import_options(params: Dictionary) -> Dictionary:
|
|
|
212
212
|
var refused: Array[String] = []
|
|
213
213
|
for key: Variant in options:
|
|
214
214
|
var name: String = str(key)
|
|
215
|
-
var held: Variant = config
|
|
215
|
+
var held: Variant = _held(config, name)
|
|
216
216
|
var given: Variant = _values.deserialize_value(options[key])
|
|
217
217
|
var value: Variant = _as_option(_values.fitted(options[key], typeof(held)), held)
|
|
218
218
|
if value is Dictionary:
|
|
@@ -248,7 +248,7 @@ func set_import_options(params: Dictionary) -> Dictionary:
|
|
|
248
248
|
var unchanged_keys: Array[String] = []
|
|
249
249
|
var changes: Dictionary = {}
|
|
250
250
|
for name: String in fitted_values:
|
|
251
|
-
var held: Variant = config
|
|
251
|
+
var held: Variant = _held(config, name)
|
|
252
252
|
var value: Variant = fitted_values[name]
|
|
253
253
|
if held != null and typeof(held) == typeof(value) and held == value:
|
|
254
254
|
unchanged_keys.append(name)
|
|
@@ -275,6 +275,14 @@ func set_import_options(params: Dictionary) -> Dictionary:
|
|
|
275
275
|
return answer
|
|
276
276
|
|
|
277
277
|
|
|
278
|
+
## What the option [param name] holds in [param config]'s params, or null when it holds none. Asked
|
|
279
|
+
## with a default of null, the engine prints an error for a key the file does not have.
|
|
280
|
+
static func _held(config: ConfigFile, name: String) -> Variant:
|
|
281
|
+
if not config.has_section_key("params", name):
|
|
282
|
+
return null
|
|
283
|
+
return config.get_value("params", name)
|
|
284
|
+
|
|
285
|
+
|
|
278
286
|
## [param given] as the option holds it. Every JSON number arrives as a float, and an enum option
|
|
279
287
|
## such as compress/mode written as 1.0 is kept that way by the import, so a whole number becomes
|
|
280
288
|
## an int unless the option already holds a float.
|
|
@@ -413,6 +413,9 @@ func add_autoload(params: Dictionary) -> Dictionary:
|
|
|
413
413
|
return _log.failure(
|
|
414
414
|
GdhFileWalk.new().with_nearest_files("Autoload file does not exist: " + path, path)
|
|
415
415
|
)
|
|
416
|
+
var unloadable: String = _not_an_autoload(path)
|
|
417
|
+
if not unloadable.is_empty():
|
|
418
|
+
return _log.failure(unloadable)
|
|
416
419
|
|
|
417
420
|
# Through ProjectSettings so the file is saved the way the editor saves it, which includes
|
|
418
421
|
# putting Godot's header back and dropping any comment somebody wrote into the file by hand.
|
|
@@ -453,6 +456,38 @@ func add_autoload(params: Dictionary) -> Dictionary:
|
|
|
453
456
|
return answer
|
|
454
457
|
|
|
455
458
|
|
|
459
|
+
## Why the file at [param path] cannot be an autoload, or empty when it can, as the editor's
|
|
460
|
+
## Autoload
|
|
461
|
+
## tab judges it: a scene, or a script whose instances are nodes. Written anyway, a script extending
|
|
462
|
+
## RefCounted saved fine and every start of the game then failed to instantiate it. A script that
|
|
463
|
+
## does not compile is let through, since it may be one being written, and its errors are its own.
|
|
464
|
+
static func _not_an_autoload(path: String) -> String:
|
|
465
|
+
var refused: String = "%s is %s, and an autoload is a scene or a script extending Node"
|
|
466
|
+
if not ResourceLoader.exists(path):
|
|
467
|
+
return (refused + "; nothing was written.") % [path, "a file Godot does not load"]
|
|
468
|
+
var loaded: Resource = load(path)
|
|
469
|
+
if loaded is PackedScene:
|
|
470
|
+
return ""
|
|
471
|
+
if not loaded is Script:
|
|
472
|
+
var kind: String = "nothing Godot could load"
|
|
473
|
+
if loaded != null:
|
|
474
|
+
kind = "a " + loaded.get_class()
|
|
475
|
+
return (refused + "; nothing was written.") % [path, kind]
|
|
476
|
+
var script: Script = loaded
|
|
477
|
+
if not script.can_instantiate():
|
|
478
|
+
return ""
|
|
479
|
+
var base: StringName = script.get_instance_base_type()
|
|
480
|
+
if ClassDB.is_parent_class(base, "Node"):
|
|
481
|
+
return ""
|
|
482
|
+
return (
|
|
483
|
+
(
|
|
484
|
+
"%s extends %s, which is not a Node, and Godot instantiates an autoload under the root "
|
|
485
|
+
+ "as a node, so every start of the game would fail to load it; nothing was written."
|
|
486
|
+
)
|
|
487
|
+
% [path, base]
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
456
491
|
## Why [param name] cannot name an autoload, or empty when it can. The editor's Autoload tab refuses
|
|
457
492
|
## the same names: an autoload named after an engine class or a built-in type takes that name in
|
|
458
493
|
## every script, so an autoload called Input made every Input.is_action_pressed call the autoload.
|
|
@@ -118,7 +118,7 @@ func cannot_hold(
|
|
|
118
118
|
if not GdhSerialisation.acceptable(value, expected_type):
|
|
119
119
|
var shape: String = "%s.%s is %s and the value given is %s, which cannot become one."
|
|
120
120
|
var remedy: String = GdhShapes.remedy(value, expected_type)
|
|
121
|
-
return shape % [holder, property, held,
|
|
121
|
+
return shape % [holder, property, held, GdhShapes.kind(value)] + remedy + cost
|
|
122
122
|
|
|
123
123
|
# And the same question of each element, because a list is accepted for a packed or typed array
|
|
124
124
|
# whatever is in it: a polygon written as [{"x": 1}] passes the check above and lands as one
|
|
@@ -132,7 +132,12 @@ func cannot_hold(
|
|
|
132
132
|
if not GdhSerialisation.acceptable(items[index], element):
|
|
133
133
|
var shape: String = "%s.%s is %s and item %d of the list given is %s, not %s."
|
|
134
134
|
var named: Array = [
|
|
135
|
-
holder,
|
|
135
|
+
holder,
|
|
136
|
+
property,
|
|
137
|
+
held,
|
|
138
|
+
index,
|
|
139
|
+
GdhShapes.kind(items[index]),
|
|
140
|
+
type_string(element),
|
|
136
141
|
]
|
|
137
142
|
return shape % named + GdhShapes.remedy(items[index], element) + cost
|
|
138
143
|
# A list of polygons is a list of lists of points, and each point is read as one too.
|
|
@@ -151,7 +156,7 @@ func cannot_hold(
|
|
|
151
156
|
type_string(inner),
|
|
152
157
|
at,
|
|
153
158
|
index,
|
|
154
|
-
|
|
159
|
+
GdhShapes.kind(points[at]),
|
|
155
160
|
type_string(inner),
|
|
156
161
|
]
|
|
157
162
|
return nested % said + GdhShapes.remedy(points[at], inner) + cost
|
|
@@ -173,14 +178,6 @@ func cannot_hold(
|
|
|
173
178
|
return ""
|
|
174
179
|
|
|
175
180
|
|
|
176
|
-
## What [param value] is, as a refusal names it. A number arrives from JSON as a float whether or
|
|
177
|
-
## not it has a fraction, so [1, 2] given for a list of words was called a list of floats.
|
|
178
|
-
static func _kind(value: Variant) -> String:
|
|
179
|
-
if value is int or value is float:
|
|
180
|
-
return "a number"
|
|
181
|
-
return type_string(typeof(value))
|
|
182
|
-
|
|
183
|
-
|
|
184
181
|
## Writes each of [param properties] onto [param target], stopping at the first it cannot write and
|
|
185
182
|
## answering with why, or "" when every one of them went on and holds what was given.
|
|
186
183
|
##
|
|
@@ -36,10 +36,9 @@ func get_uid(params: Dictionary) -> Dictionary:
|
|
|
36
36
|
var answer: Dictionary = { "file": file_path, "absolute_path": absolute_path }
|
|
37
37
|
var found: Dictionary = uid_of(file_path)
|
|
38
38
|
if found.is_empty():
|
|
39
|
-
answer["
|
|
39
|
+
answer["uid"] = null
|
|
40
40
|
answer["message"] = _why_none(file_path)
|
|
41
41
|
return answer
|
|
42
|
-
answer["exists"] = true
|
|
43
42
|
answer.merge(found)
|
|
44
43
|
return answer
|
|
45
44
|
|
|
@@ -234,6 +234,15 @@ static func packed_from(items: Array, type: int, fit: Callable) -> Variant:
|
|
|
234
234
|
return type_convert(built, type)
|
|
235
235
|
|
|
236
236
|
|
|
237
|
+
## What [param value] is, as a refusal names it. A number arrives from JSON as a float whether or
|
|
238
|
+
## not it has a fraction, so [1, 2] given for a list of words was called a list of floats, and 5
|
|
239
|
+
## given for a Vector2 was called a float.
|
|
240
|
+
static func kind(value: Variant) -> String:
|
|
241
|
+
if value is int or value is float:
|
|
242
|
+
return "a number"
|
|
243
|
+
return type_string(typeof(value))
|
|
244
|
+
|
|
245
|
+
|
|
237
246
|
## The sentences a refusal of [param value] where a [param type] goes ends with: which of its keys
|
|
238
247
|
## kept it from being read as one, and how one is written. Empty for a type not built from fields.
|
|
239
248
|
##
|
|
@@ -357,6 +357,9 @@ class GdhRunner:
|
|
|
357
357
|
var case_began: bool = false
|
|
358
358
|
## The suite running, which a case's own event does not carry.
|
|
359
359
|
var suite_running: String = ""
|
|
360
|
+
## Whether gdUnit4 has minimised the window, or is about to, and it is not back yet: it does as
|
|
361
|
+
## its runner starts, and after every suite.
|
|
362
|
+
var minimise_pending: bool = true
|
|
360
363
|
|
|
361
364
|
## As the run ends, the marker of the walk's newest end, and every script held past the walk, on
|
|
362
365
|
## an engine whose walk can stop part way ([GdhShutdownWalk]).
|
|
@@ -446,7 +449,11 @@ class GdhRunner:
|
|
|
446
449
|
GdUnitEvent.SESSION_START:
|
|
447
450
|
if not GdhIndexedSession.indexes_itself(_test_session):
|
|
448
451
|
_test_session = GdhIndexedSession.of(_test_session)
|
|
452
|
+
var tree: SceneTree = get_tree()
|
|
453
|
+
if not tree.process_frame.is_connected(drawn_window):
|
|
454
|
+
var _watched: int = tree.process_frame.connect(drawn_window)
|
|
449
455
|
GdUnitEvent.TESTSUITE_BEFORE:
|
|
456
|
+
drawn_window()
|
|
450
457
|
suite_running = event.resource_path()
|
|
451
458
|
orphans_at_suite = orphan_ids()
|
|
452
459
|
orphans_named.clear()
|
|
@@ -461,6 +468,9 @@ class GdhRunner:
|
|
|
461
468
|
GdUnitEvent.TESTCASE_AFTER:
|
|
462
469
|
cases_ended += 1
|
|
463
470
|
super(event)
|
|
471
|
+
# gdUnit4 minimises the window once the suite's end is reported.
|
|
472
|
+
if event.type() == GdUnitEvent.TESTSUITE_AFTER:
|
|
473
|
+
minimise_pending = true
|
|
464
474
|
# After gdUnit4's own report of the case, so the line falls under it.
|
|
465
475
|
if event.orphan_nodes() == 0:
|
|
466
476
|
return
|
|
@@ -478,6 +488,21 @@ class GdhRunner:
|
|
|
478
488
|
"after", left.filter(func(id: int) -> bool: return not orphans_from_before.has(id))
|
|
479
489
|
)
|
|
480
490
|
|
|
491
|
+
## The window back from minimised, which gdUnit4 makes it as its runner starts and after every
|
|
492
|
+
## suite, and where Godot draws nothing: a suite given a window to render in waited for a
|
|
493
|
+
## frame_post_draw that never came. Called as a suite begins, ahead of its before(), and every
|
|
494
|
+
## frame after, since on macOS the minimise is an animation that reads as windowed until it
|
|
495
|
+
## ends, and a window not yet minimised cannot be put back. Only once for each minimise of
|
|
496
|
+
## gdUnit4's, so a suite minimising the window itself keeps it minimised.
|
|
497
|
+
func drawn_window() -> void:
|
|
498
|
+
if not minimise_pending or DisplayServer.get_name() == "headless":
|
|
499
|
+
return
|
|
500
|
+
if Engine.is_embedded_in_editor():
|
|
501
|
+
return
|
|
502
|
+
if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_MINIMIZED:
|
|
503
|
+
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
|
504
|
+
minimise_pending = false
|
|
505
|
+
|
|
481
506
|
## The orphans now that were not there in [param before] and no line has named yet.
|
|
482
507
|
func new_orphans(before: Dictionary[int, bool]) -> Array[int]:
|
|
483
508
|
var found: Array[int] = []
|