gdharness 1.3.46 → 1.3.48
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 +186 -49
- package/build/godot/addons/gdharness_editor/animation_reads.gd +20 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +6 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd +25 -13
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +20 -6
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +19 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +23 -4
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +9 -7
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +90 -30
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +1 -1
- package/build/godot/operations/animation_reads.gd +20 -0
- package/build/godot/operations/dependencies.gd +65 -9
- package/build/godot/operations/export_presets.gd +32 -5
- 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/resource_files.gd +1 -2
- package/build/godot/tests/listed.gd +25 -0
- package/build/keeper.js +1308 -1026
- package/package.json +1 -1
|
@@ -25,29 +25,41 @@ static func save_keeping_uid(resource: Resource, file_path: String) -> Error:
|
|
|
25
25
|
var marked: Error = ResourceSaver.set_uid(file_path, uid)
|
|
26
26
|
if marked != OK:
|
|
27
27
|
return marked
|
|
28
|
-
return
|
|
28
|
+
return _as_the_editor_writes(file_path)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
##
|
|
32
|
-
##
|
|
33
|
-
##
|
|
34
|
-
|
|
31
|
+
## Writes the text file at [param file_path] as the editor's save would have.
|
|
32
|
+
##
|
|
33
|
+
## Only the editor's file system tells the saver a path's uid, so a headless save writes every
|
|
34
|
+
## reference by path alone, and each breaks when the file it names is moved: the uid is put back on
|
|
35
|
+
## each external resource.
|
|
36
|
+
##
|
|
37
|
+
## The saver leaves out a shader parameter holding the default the renderer gives it, and a headless
|
|
38
|
+
## engine's renderer gives none, so every parameter a ShaderMaterial never set was written as null.
|
|
39
|
+
## Loading null clears the parameter, so the line changes nothing, and the editor drops it again.
|
|
40
|
+
static func _as_the_editor_writes(file_path: String) -> Error:
|
|
35
41
|
if not (file_path.ends_with(".tscn") or file_path.ends_with(".tres")):
|
|
36
42
|
return OK
|
|
37
43
|
var lines: PackedStringArray = FileAccess.get_file_as_string(file_path).split("\n")
|
|
38
44
|
var pattern: RegEx = RegEx.create_from_string(' path="([^"]+)"')
|
|
45
|
+
var unset: RegEx = RegEx.create_from_string("^shader_parameter/\\S+ = null$")
|
|
46
|
+
var kept: PackedStringArray = []
|
|
39
47
|
var changed: bool = false
|
|
40
|
-
for
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
continue
|
|
44
|
-
var named: RegExMatch = pattern.search(line)
|
|
45
|
-
if named == null:
|
|
48
|
+
for line: String in lines:
|
|
49
|
+
if unset.search(line) != null:
|
|
50
|
+
changed = true
|
|
46
51
|
continue
|
|
47
|
-
var
|
|
52
|
+
var named: RegExMatch = (
|
|
53
|
+
pattern.search(line)
|
|
54
|
+
if line.begins_with("[ext_resource ") and not line.contains(' uid="')
|
|
55
|
+
else null
|
|
56
|
+
)
|
|
57
|
+
var uid: String = "" if named == null else reference_uid(named.get_string(1))
|
|
48
58
|
if not uid.is_empty():
|
|
49
|
-
|
|
59
|
+
line = line.replace(' path="', uid + ' path="')
|
|
50
60
|
changed = true
|
|
61
|
+
var _added: bool = kept.append(line)
|
|
62
|
+
lines = kept
|
|
51
63
|
if not changed:
|
|
52
64
|
return OK
|
|
53
65
|
var file: FileAccess = FileAccess.open(file_path, FileAccess.WRITE)
|
|
@@ -137,13 +137,26 @@ func connect_animation_states(args: Dictionary) -> Dictionary:
|
|
|
137
137
|
sm.add_transition(StringName(from_state), StringName(to_state), transition)
|
|
138
138
|
if not sm.has_transition(StringName(from_state), StringName(to_state)):
|
|
139
139
|
return scene.refuse("The state machine did not take the transition.")
|
|
140
|
+
_tell_the_tree(sm)
|
|
140
141
|
var files: Dictionary = found[2]
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
var made: Dictionary = {
|
|
143
|
+
"ok": true,
|
|
144
|
+
"from": from_state,
|
|
145
|
+
"to": to_state,
|
|
146
|
+
"transitionType": transition_type,
|
|
147
|
+
"advanceMode": advance_mode,
|
|
148
|
+
}
|
|
149
|
+
if not advance_condition.is_empty():
|
|
150
|
+
made["advanceCondition"] = advance_condition
|
|
151
|
+
return _finish(scene, Callable(), files, made)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Has the AnimationTree playing [param sm] list its parameters again. Adding or removing a
|
|
155
|
+
## transition does not say the tree changed, so the save kept the parameters from before it: a
|
|
156
|
+
## condition's parameter was written by the next call to change the machine, and a removed one's
|
|
157
|
+
## stayed until then.
|
|
158
|
+
static func _tell_the_tree(sm: AnimationNodeStateMachine) -> void:
|
|
159
|
+
var _told: Error = sm.emit_signal(&"tree_changed")
|
|
147
160
|
|
|
148
161
|
|
|
149
162
|
## Why [param sm] cannot take a transition from [param from_state] to [param to_state], or "".
|
|
@@ -270,6 +283,7 @@ func disconnect_animation_states(args: Dictionary) -> Dictionary:
|
|
|
270
283
|
sm.remove_transition(StringName(from_state), StringName(to_state))
|
|
271
284
|
if sm.has_transition(StringName(from_state), StringName(to_state)):
|
|
272
285
|
return scene.refuse("The state machine did not let go of the transition.")
|
|
286
|
+
_tell_the_tree(sm)
|
|
273
287
|
var files: Dictionary = found[2]
|
|
274
288
|
return _finish(
|
|
275
289
|
scene, Callable(), files, { "ok": true, "removed": { "from": from_state, "to": to_state } }
|
|
@@ -109,6 +109,9 @@ func add_animation_track(args: Dictionary) -> Dictionary:
|
|
|
109
109
|
if anim.track_get_type(track_idx) == Animation.TYPE_VALUE:
|
|
110
110
|
var mode: int = anim.value_track_get_update_mode(track_idx)
|
|
111
111
|
answer["update"] = GdhAnimationReads.UPDATE_MODE_NAMES.get(mode, str(mode))
|
|
112
|
+
var blended: int = anim.track_get_interpolation_type(track_idx)
|
|
113
|
+
answer["interpolation"] = GdhAnimationReads.INTERPOLATION_NAMES.get(blended, str(blended))
|
|
114
|
+
answer["loopWrap"] = anim.track_get_interpolation_loop_wrap(track_idx)
|
|
112
115
|
if built[3]:
|
|
113
116
|
answer["existingTrack"] = true
|
|
114
117
|
var replaced: Array = built[5]
|
|
@@ -165,6 +168,7 @@ func _add_track(
|
|
|
165
168
|
var kind: Animation.TrackType = Animation.TYPE_VALUE
|
|
166
169
|
var path: NodePath = NodePath(target_path)
|
|
167
170
|
var update: int = -1
|
|
171
|
+
var interpolation: int = -1
|
|
168
172
|
if track_type == "property":
|
|
169
173
|
var prop_name: String = str(track.get("property", ""))
|
|
170
174
|
if prop_name.is_empty():
|
|
@@ -175,6 +179,11 @@ func _add_track(
|
|
|
175
179
|
)
|
|
176
180
|
if named != null:
|
|
177
181
|
update = GdhRead.as_int(named)
|
|
182
|
+
var blended: Variant = GdhAnimationReads.INTERPOLATION_NAMES.find_key(
|
|
183
|
+
str(track.get("interpolation", ""))
|
|
184
|
+
)
|
|
185
|
+
if blended != null:
|
|
186
|
+
interpolation = GdhRead.as_int(blended)
|
|
178
187
|
var values: Array = _key_values(target, prop_name, keyframes)
|
|
179
188
|
if not values[0]:
|
|
180
189
|
return _not_added(str(values[1]))
|
|
@@ -196,6 +205,12 @@ func _add_track(
|
|
|
196
205
|
misfit = _key_misfit(target, method_name, arguments, index)
|
|
197
206
|
method_found = target.has_method(method_name)
|
|
198
207
|
kind = Animation.TYPE_METHOD
|
|
208
|
+
# A method key is called at its time and never blended, so neither has anything to set.
|
|
209
|
+
for blending: String in ["interpolation", "loopWrap"]:
|
|
210
|
+
if track.has(blending):
|
|
211
|
+
return _not_added(
|
|
212
|
+
"track.%s is for property tracks; a method key is not blended." % blending
|
|
213
|
+
)
|
|
199
214
|
else:
|
|
200
215
|
return _not_added(
|
|
201
216
|
(
|
|
@@ -250,6 +265,10 @@ func _add_track(
|
|
|
250
265
|
_as_the_editor_makes(anim, track_idx, player, target)
|
|
251
266
|
if update >= 0:
|
|
252
267
|
anim.value_track_set_update_mode(track_idx, update as Animation.UpdateMode)
|
|
268
|
+
if interpolation >= 0:
|
|
269
|
+
anim.track_set_interpolation_type(track_idx, interpolation as Animation.InterpolationType)
|
|
270
|
+
if track.has("loopWrap"):
|
|
271
|
+
anim.track_set_interpolation_loop_wrap(track_idx, GdhRead.as_bool(track["loopWrap"]))
|
|
253
272
|
for index: int in keyframes.size():
|
|
254
273
|
var keyframe: Dictionary = keyframes[index]
|
|
255
274
|
if anim.track_insert_key(track_idx, GdhRead.as_float(keyframe["time"]), keys[index]) < 0:
|
|
@@ -718,6 +718,15 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
718
718
|
if property != "name" and property in node and not named.has(property):
|
|
719
719
|
named.append(property)
|
|
720
720
|
var source: String = scene.values_from(node, old_path)
|
|
721
|
+
# Read before the write, which frees the node.
|
|
722
|
+
var unsaved: Array[String] = []
|
|
723
|
+
for declared: Dictionary in node.get_property_list():
|
|
724
|
+
var declared_name: String = str(declared.get("name", ""))
|
|
725
|
+
if (
|
|
726
|
+
named.has(declared_name)
|
|
727
|
+
and not GdhRead.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE
|
|
728
|
+
):
|
|
729
|
+
unsaved.append(declared_name)
|
|
721
730
|
var carried: GdhPaths = GdhPaths.new()
|
|
722
731
|
var _rewritten: int = carried.carry(
|
|
723
732
|
scene,
|
|
@@ -751,7 +760,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
751
760
|
answer["droppedNodes"] = dropped["nodes"]
|
|
752
761
|
_note_put_back(answer, scene, id, new_path)
|
|
753
762
|
_note_loads_as(answer, scene, new_path)
|
|
754
|
-
_note_left_out(answer, scene.written_pack, new_path, named, source)
|
|
763
|
+
_note_left_out(answer, scene.written_pack, new_path, named, unsaved, source)
|
|
755
764
|
if new_path != old_path:
|
|
756
765
|
answer.merge(GdhKin.follow(scene_path, old_path, new_path, carried))
|
|
757
766
|
return answer
|
|
@@ -793,9 +802,15 @@ static func _note_put_back(answer: Dictionary, scene: GdhSceneFile, id: int, at:
|
|
|
793
802
|
|
|
794
803
|
## Notes under notStored each of [param named] the file saved from [param pack] does not set on the
|
|
795
804
|
## node at [param at]: a set gave it the value the node has without it, so the packer leaves it out,
|
|
796
|
-
## as the editor does, and it follows [param source], or its default, from then on.
|
|
805
|
+
## as the editor does, and it follows [param source], or its default, from then on. [param unsaved]
|
|
806
|
+
## are those of them the node never stores by name.
|
|
797
807
|
static func _note_left_out(
|
|
798
|
-
answer: Dictionary,
|
|
808
|
+
answer: Dictionary,
|
|
809
|
+
pack: PackedScene,
|
|
810
|
+
at: String,
|
|
811
|
+
named: Array[String],
|
|
812
|
+
unsaved: Array[String],
|
|
813
|
+
source: String
|
|
799
814
|
) -> void:
|
|
800
815
|
var stored: Dictionary = {}
|
|
801
816
|
var state: SceneState = pack.get_state()
|
|
@@ -803,9 +818,13 @@ static func _note_left_out(
|
|
|
803
818
|
if str(state.get_node_path(index)).trim_prefix("./") == at:
|
|
804
819
|
for property: int in state.get_node_property_count(index):
|
|
805
820
|
stored[str(state.get_node_property_name(index, property))] = true
|
|
821
|
+
# A property the file never holds by name, as a Control's position, which its offsets hold, is
|
|
822
|
+
# stored when what changed with it is.
|
|
823
|
+
var also: Dictionary = answer.get("alsoChanged", {})
|
|
824
|
+
var held_with_it: bool = also.keys().any(func(other: Variant) -> bool: return stored.has(other))
|
|
806
825
|
var left: Array[String] = []
|
|
807
826
|
for asked: String in named:
|
|
808
|
-
if not stored.has(asked):
|
|
827
|
+
if not stored.has(asked) and not (held_with_it and unsaved.has(asked)):
|
|
809
828
|
left.append(asked)
|
|
810
829
|
if left.is_empty():
|
|
811
830
|
return
|
|
@@ -160,7 +160,7 @@ func _hover(viewport: Viewport, point: Vector2) -> void:
|
|
|
160
160
|
var inside: Vector2 = container.get_global_transform_with_canvas().affine_inverse() * point
|
|
161
161
|
if container.stretch:
|
|
162
162
|
inside /= container.stretch_shrink
|
|
163
|
-
for child: Node in container.get_children():
|
|
163
|
+
for child: Node in container.get_children(true):
|
|
164
164
|
var shown: SubViewport = child as SubViewport
|
|
165
165
|
if shown != null and not shown.gui_disable_input:
|
|
166
166
|
_hover(shown, shown.get_final_transform().affine_inverse() * inside)
|
|
@@ -179,7 +179,7 @@ func _drop(viewport: Viewport, until: int) -> void:
|
|
|
179
179
|
var over: Control = instance_from_id(top) as Control
|
|
180
180
|
var container: SubViewportContainer = over as SubViewportContainer
|
|
181
181
|
if container != null:
|
|
182
|
-
for child: Node in container.get_children():
|
|
182
|
+
for child: Node in container.get_children(true):
|
|
183
183
|
var shown: SubViewport = child as SubViewport
|
|
184
184
|
if shown != null:
|
|
185
185
|
_drop(shown, 0)
|
|
@@ -264,7 +264,7 @@ func _pressed_at(viewport: Viewport, point: Vector2) -> Node:
|
|
|
264
264
|
var inside: Vector2 = container.get_global_transform_with_canvas().affine_inverse() * point
|
|
265
265
|
if container.stretch:
|
|
266
266
|
inside /= container.stretch_shrink
|
|
267
|
-
for child: Node in container.get_children():
|
|
267
|
+
for child: Node in container.get_children(true):
|
|
268
268
|
var shown: SubViewport = child as SubViewport
|
|
269
269
|
if shown != null and not shown.gui_disable_input:
|
|
270
270
|
var within: Node = _pressed_at(
|
|
@@ -277,9 +277,11 @@ func _pressed_at(viewport: Viewport, point: Vector2) -> Node:
|
|
|
277
277
|
|
|
278
278
|
## The controls [param node] holds that the engine keeps as roots of [param node]'s viewport: drawn
|
|
279
279
|
## on their own, or with no control above them. Not into another viewport, a window or a
|
|
280
|
-
## SubViewport, whose controls are that one's.
|
|
280
|
+
## SubViewport, whose controls are that one's. Internal children count, as they do for the engine,
|
|
281
|
+
## though get_children leaves them out unless asked: a layer added with INTERNAL_MODE_BACK was
|
|
282
|
+
## passed over, and the press predicted for the control under it.
|
|
281
283
|
static func _gather_roots(node: Node, roots: Array[Control]) -> void:
|
|
282
|
-
for child: Node in node.get_children():
|
|
284
|
+
for child: Node in node.get_children(true):
|
|
283
285
|
if child is Viewport:
|
|
284
286
|
continue
|
|
285
287
|
var control: Control = child as Control
|
|
@@ -331,8 +333,8 @@ func _at(item: CanvasItem, point: Vector2, holder: Transform2D) -> Control:
|
|
|
331
333
|
var control: Control = item as Control
|
|
332
334
|
var local: Vector2 = matrix.affine_inverse() * point
|
|
333
335
|
if control == null or not control.is_clipping_contents() or _has_point(control, local):
|
|
334
|
-
for index: int in range(item.get_child_count() - 1, -1, -1):
|
|
335
|
-
var child: CanvasItem = item.get_child(index) as CanvasItem
|
|
336
|
+
for index: int in range(item.get_child_count(true) - 1, -1, -1):
|
|
337
|
+
var child: CanvasItem = item.get_child(index, true) as CanvasItem
|
|
336
338
|
if child == null or child.top_level:
|
|
337
339
|
continue
|
|
338
340
|
var found: Control = _at(child, point, matrix)
|
|
@@ -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
|
|
|
@@ -17,6 +17,11 @@ const MOST_FRAMES: int = 600
|
|
|
17
17
|
## How long a wait for words leaves between looks, as a multiple of what the last look took: nine
|
|
18
18
|
## gives looking about a tenth of the time.
|
|
19
19
|
const LOOKS_APART: int = 9
|
|
20
|
+
## The longest a wait for words holds one of the game's frames while it looks, in microseconds: a
|
|
21
|
+
## look over a screen too large to read in that time carries on in the next frame from where it
|
|
22
|
+
## stopped. Read whole, a look over an interface of about 1,000 labels held a frame for 28ms every
|
|
23
|
+
## few dozen, which a game logging its own frame times took for a hitch of its own.
|
|
24
|
+
const LOOK_SLICE_USEC: int = 2000
|
|
20
25
|
## How a property wait compares what it reads with the value: equality by default, and order for a
|
|
21
26
|
## number that steps rather than lands, as a day counter on a fast clock goes from 31 to 34 between
|
|
22
27
|
## two looks and a wait for 32 never sees it.
|
|
@@ -785,9 +790,12 @@ static func _declared_slot(holder: Variant, named: String) -> Dictionary:
|
|
|
785
790
|
## looking takes about a tenth of the time. Every frame on a hall of 3,500 nodes held the game at a
|
|
786
791
|
## sixth of its frame rate, and its clock, which advances by the frame's delta, lost three quarters
|
|
787
792
|
## of its time, so a wait for a date timed out on a clock that turned a day every thirty seconds.
|
|
788
|
-
##
|
|
789
|
-
##
|
|
790
|
-
##
|
|
793
|
+
## Each look is read a slice at a time, [constant LOOK_SLICE_USEC] to a frame, so the frames it runs
|
|
794
|
+
## in keep their length; the last is read whole, since the wait answers on it.
|
|
795
|
+
##
|
|
796
|
+
## The answer says how many frames the wait spanned, how many looks it took, how long they took
|
|
797
|
+
## between them and the longest they held one frame, which is how a caller sees what it cost and
|
|
798
|
+
## whether the game lost its speed, or a frame its length, to the looks or to something else.
|
|
791
799
|
func _wait_until_said(
|
|
792
800
|
node_path: String, said: String, timeout_ms: int, include_hidden: bool
|
|
793
801
|
) -> Dictionary:
|
|
@@ -795,18 +803,29 @@ func _wait_until_said(
|
|
|
795
803
|
var frames: int = 0
|
|
796
804
|
var looks: int = 0
|
|
797
805
|
var looked_usec: int = 0
|
|
806
|
+
var held_usec: int = 0
|
|
798
807
|
var next_look: int = 0
|
|
799
808
|
var looked_at_frame: int = 0
|
|
800
809
|
var found: bool = false
|
|
810
|
+
var look: GdhLook = null
|
|
811
|
+
var look_began: int = 0
|
|
801
812
|
while true:
|
|
802
|
-
if Time.get_ticks_usec() >= next_look:
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
var
|
|
807
|
-
looked_usec +=
|
|
808
|
-
|
|
813
|
+
if look == null and Time.get_ticks_usec() >= next_look:
|
|
814
|
+
look = GdhLook.new(_under(node_path), said, include_hidden)
|
|
815
|
+
look_began = Time.get_ticks_usec()
|
|
816
|
+
if look != null:
|
|
817
|
+
var held: int = look.go_on(LOOK_SLICE_USEC)
|
|
818
|
+
looked_usec += held
|
|
819
|
+
held_usec = maxi(held_usec, held)
|
|
809
820
|
looked_at_frame = frames
|
|
821
|
+
if look.done:
|
|
822
|
+
looks += 1
|
|
823
|
+
found = look.found
|
|
824
|
+
# From where the look began, since the frames it was read across are the game's:
|
|
825
|
+
# counted from its end, a look spread over a third of a second waited out its nine
|
|
826
|
+
# lengths after that, and a wait on 3,500 nodes looked four times in two seconds.
|
|
827
|
+
next_look = look_began + look.took_usec * (LOOKS_APART + 1)
|
|
828
|
+
look = null
|
|
810
829
|
if found or Time.get_ticks_msec() - started >= timeout_ms:
|
|
811
830
|
break
|
|
812
831
|
await _host.get_tree().process_frame
|
|
@@ -815,14 +834,18 @@ func _wait_until_said(
|
|
|
815
834
|
# never having come. Taken once a whole frame has run since the last look, which is two frame
|
|
816
835
|
# starts: a first look that outlasted the timeout otherwise left none, so the wait answered with
|
|
817
836
|
# the game not having run at all, and a command sent during that look not yet read.
|
|
837
|
+
var heard: bool = found
|
|
818
838
|
if not found:
|
|
819
839
|
while frames - looked_at_frame < 2:
|
|
820
840
|
await _host.get_tree().process_frame
|
|
821
841
|
frames += 1
|
|
822
|
-
var last:
|
|
823
|
-
|
|
842
|
+
var last: GdhLook = GdhLook.new(_under(node_path), said, include_hidden)
|
|
843
|
+
var held: int = last.go_on(-1)
|
|
824
844
|
looks += 1
|
|
825
|
-
looked_usec +=
|
|
845
|
+
looked_usec += held
|
|
846
|
+
held_usec = maxi(held_usec, held)
|
|
847
|
+
found = last.found
|
|
848
|
+
heard = last.heard
|
|
826
849
|
|
|
827
850
|
var answer: Dictionary = {
|
|
828
851
|
"type": "condition",
|
|
@@ -834,10 +857,11 @@ func _wait_until_said(
|
|
|
834
857
|
"frames": frames,
|
|
835
858
|
"looks": looks,
|
|
836
859
|
"looking_ms": looked_usec / 1000.0,
|
|
860
|
+
"longest_hold_ms": held_usec / 1000.0,
|
|
837
861
|
}
|
|
838
862
|
# Words that are there and cannot be read look, from a wait that ran out, like words that never
|
|
839
863
|
# came.
|
|
840
|
-
if not found and
|
|
864
|
+
if not found and heard:
|
|
841
865
|
answer["unseen_note"] = (
|
|
842
866
|
(
|
|
843
867
|
"Something under %s says it, but is hidden or drawn outside the viewport or a "
|
|
@@ -849,22 +873,8 @@ func _wait_until_said(
|
|
|
849
873
|
return answer
|
|
850
874
|
|
|
851
875
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
## `says`, a glob over the whole text when it has `*` or `?`: read as literal words, a glob a click
|
|
855
|
-
## had just pressed by was waited on for the whole timeout with its label on screen, and answered as
|
|
856
|
-
## words never arriving.
|
|
857
|
-
func _anything_says(node_path: String, said: String, include_hidden: bool) -> bool:
|
|
858
|
-
var root: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
859
|
-
if root == null:
|
|
860
|
-
return false
|
|
861
|
-
var pending: Array[Node] = [root]
|
|
862
|
-
while not pending.is_empty():
|
|
863
|
-
var node: Node = pending.pop_back()
|
|
864
|
-
if GdhSays.says(node, said) and (include_hidden or GdhQueries.readable(node)):
|
|
865
|
-
return true
|
|
866
|
-
pending.append_array(node.get_children(true))
|
|
867
|
-
return false
|
|
876
|
+
func _under(node_path: String) -> Node:
|
|
877
|
+
return _host.get_tree().root.get_node_or_null(node_path)
|
|
868
878
|
|
|
869
879
|
|
|
870
880
|
## Remembers that a signal fired and what it carried, for a wait that polls rather than
|
|
@@ -942,6 +952,56 @@ class GdhTrace:
|
|
|
942
952
|
dropped += 1
|
|
943
953
|
|
|
944
954
|
|
|
955
|
+
## One look for whether anything under a node says some words, the node itself included, and where
|
|
956
|
+
## the player can read it unless hidden ones are wanted too. Matched as a find and a click match
|
|
957
|
+
## `says`, a glob over the whole text when it has `*` or `?`: read as literal words, a glob a click
|
|
958
|
+
## had just pressed by was waited on for the whole timeout with its label on screen, and answered as
|
|
959
|
+
## words never arriving.
|
|
960
|
+
class GdhLook:
|
|
961
|
+
extends RefCounted
|
|
962
|
+
## Whether a node that counts says the words.
|
|
963
|
+
var found: bool = false
|
|
964
|
+
## Whether any node says them, hidden or not: words that are there and cannot be read look,
|
|
965
|
+
## from a wait that ran out, like words that never came.
|
|
966
|
+
var heard: bool = false
|
|
967
|
+
var done: bool = false
|
|
968
|
+
## How long the look has taken over every frame it has run in.
|
|
969
|
+
var took_usec: int = 0
|
|
970
|
+
var _said: String
|
|
971
|
+
var _include_hidden: bool
|
|
972
|
+
## Untyped, since a node waiting here can be freed between two frames of the look.
|
|
973
|
+
var _pending: Array = []
|
|
974
|
+
|
|
975
|
+
func _init(root: Node, said: String, include_hidden: bool) -> void:
|
|
976
|
+
_said = said
|
|
977
|
+
_include_hidden = include_hidden
|
|
978
|
+
if root != null:
|
|
979
|
+
_pending.append(root)
|
|
980
|
+
|
|
981
|
+
## Reads on until the look is done or [param budget_usec] has passed, -1 for no limit, and
|
|
982
|
+
## answers how long it read.
|
|
983
|
+
func go_on(budget_usec: int) -> int:
|
|
984
|
+
var from: int = Time.get_ticks_usec()
|
|
985
|
+
while not _pending.is_empty():
|
|
986
|
+
if budget_usec >= 0 and Time.get_ticks_usec() - from >= budget_usec:
|
|
987
|
+
break
|
|
988
|
+
var next: Variant = _pending.pop_back()
|
|
989
|
+
if not is_instance_valid(next):
|
|
990
|
+
continue
|
|
991
|
+
var node: Node = next
|
|
992
|
+
if GdhSays.says(node, _said):
|
|
993
|
+
heard = true
|
|
994
|
+
if _include_hidden or GdhQueries.readable(node):
|
|
995
|
+
found = true
|
|
996
|
+
_pending.clear()
|
|
997
|
+
break
|
|
998
|
+
_pending.append_array(node.get_children(true))
|
|
999
|
+
done = _pending.is_empty()
|
|
1000
|
+
var read: int = Time.get_ticks_usec() - from
|
|
1001
|
+
took_usec += read
|
|
1002
|
+
return read
|
|
1003
|
+
|
|
1004
|
+
|
|
945
1005
|
## A signal recorded from when it was armed until it is collected.
|
|
946
1006
|
class GdhWatch:
|
|
947
1007
|
extends RefCounted
|
|
@@ -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)
|
|
@@ -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):
|
|
@@ -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
|