gdharness 1.3.45 → 1.3.47
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 +318 -201
- package/build/godot/addons/gdharness_editor/animation_reads.gd +8 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +190 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd +25 -13
- package/build/godot/addons/gdharness_editor/tool_executor.gd +4 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +20 -6
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +89 -3
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +61 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +21 -2
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +41 -9
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +22 -3
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +7 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +9 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +29 -2
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +90 -30
- package/build/godot/operations/animation_reads.gd +8 -0
- package/build/godot/operations/editor_tools.gd +2 -0
- package/build/godot/operations/export_presets.gd +32 -5
- package/build/godot/operations/patterns.gd +27 -2
- package/build/godot/operations/project_diagnostics.gd +2 -1
- package/build/godot/operations/project_validation.gd +4 -2
- package/build/keeper.js +3 -2
- package/package.json +1 -1
|
@@ -560,11 +560,12 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
560
560
|
GdhPlacing.own(node, root)
|
|
561
561
|
if GdhRead.as_bool(args.get("keepGlobalTransform", true)):
|
|
562
562
|
GdhPlacing.keep_in_place(node, old_parent)
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
563
|
+
# Unasked, a headless write takes the move on a node whose script is not a tool for the script's
|
|
564
|
+
# doing and puts the node back as the file held it. A new parent changes more than where the
|
|
565
|
+
# node is drawn: a container gives a Control its own layout mode.
|
|
566
|
+
var placed: Array[String] = GdhHeadlessCheck.differing(node, stored_before, {})
|
|
567
|
+
if not placed.is_empty():
|
|
568
|
+
scene.asked(node, placed)
|
|
568
569
|
var also_changed: Dictionary = {}
|
|
569
570
|
for property: String in drawn_at:
|
|
570
571
|
var now: Variant = _properties.read_at(node, property)
|
|
@@ -572,6 +573,18 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
572
573
|
also_changed[property] = {
|
|
573
574
|
"was": _readable(node, drawn_at[property]), "now": _readable(node, now),
|
|
574
575
|
}
|
|
576
|
+
var drawn_in: Array[String] = GdhPlacing.placed_in(node)
|
|
577
|
+
for property: String in placed:
|
|
578
|
+
if property in drawn_in or also_changed.has(property):
|
|
579
|
+
continue
|
|
580
|
+
var was: Variant = (
|
|
581
|
+
stored_before[property]
|
|
582
|
+
if stored_before.has(property)
|
|
583
|
+
else GdhHeadlessCheck.default_of(node, property)
|
|
584
|
+
)
|
|
585
|
+
also_changed[property] = {
|
|
586
|
+
"was": _readable(node, was), "now": _readable(node, node.get(property)),
|
|
587
|
+
}
|
|
575
588
|
var sheared: String = ""
|
|
576
589
|
if node is Node3D and GdhPlacing.sheared((node as Node3D).basis):
|
|
577
590
|
var spatial: Node3D = node
|
|
@@ -705,6 +718,15 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
705
718
|
if property != "name" and property in node and not named.has(property):
|
|
706
719
|
named.append(property)
|
|
707
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)
|
|
708
730
|
var carried: GdhPaths = GdhPaths.new()
|
|
709
731
|
var _rewritten: int = carried.carry(
|
|
710
732
|
scene,
|
|
@@ -738,7 +760,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
738
760
|
answer["droppedNodes"] = dropped["nodes"]
|
|
739
761
|
_note_put_back(answer, scene, id, new_path)
|
|
740
762
|
_note_loads_as(answer, scene, new_path)
|
|
741
|
-
_note_left_out(answer, scene.written_pack, new_path, named, source)
|
|
763
|
+
_note_left_out(answer, scene.written_pack, new_path, named, unsaved, source)
|
|
742
764
|
if new_path != old_path:
|
|
743
765
|
answer.merge(GdhKin.follow(scene_path, old_path, new_path, carried))
|
|
744
766
|
return answer
|
|
@@ -780,9 +802,15 @@ static func _note_put_back(answer: Dictionary, scene: GdhSceneFile, id: int, at:
|
|
|
780
802
|
|
|
781
803
|
## Notes under notStored each of [param named] the file saved from [param pack] does not set on the
|
|
782
804
|
## node at [param at]: a set gave it the value the node has without it, so the packer leaves it out,
|
|
783
|
-
## 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.
|
|
784
807
|
static func _note_left_out(
|
|
785
|
-
answer: Dictionary,
|
|
808
|
+
answer: Dictionary,
|
|
809
|
+
pack: PackedScene,
|
|
810
|
+
at: String,
|
|
811
|
+
named: Array[String],
|
|
812
|
+
unsaved: Array[String],
|
|
813
|
+
source: String
|
|
786
814
|
) -> void:
|
|
787
815
|
var stored: Dictionary = {}
|
|
788
816
|
var state: SceneState = pack.get_state()
|
|
@@ -790,9 +818,13 @@ static func _note_left_out(
|
|
|
790
818
|
if str(state.get_node_path(index)).trim_prefix("./") == at:
|
|
791
819
|
for property: int in state.get_node_property_count(index):
|
|
792
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))
|
|
793
825
|
var left: Array[String] = []
|
|
794
826
|
for asked: String in named:
|
|
795
|
-
if not stored.has(asked):
|
|
827
|
+
if not stored.has(asked) and not (held_with_it and unsaved.has(asked)):
|
|
796
828
|
left.append(asked)
|
|
797
829
|
if left.is_empty():
|
|
798
830
|
return
|
|
@@ -92,6 +92,19 @@ func connect_signal(args: Dictionary) -> Dictionary:
|
|
|
92
92
|
if unbinds > 0:
|
|
93
93
|
callable = callable.unbind(unbinds)
|
|
94
94
|
var method_found: bool = target.has_method(method_name)
|
|
95
|
+
if not method_found and (not binds.is_empty() or unbinds > 0):
|
|
96
|
+
var named_to: String = str(target.name) if target == scene.root else target_node_path
|
|
97
|
+
return scene.refuse(
|
|
98
|
+
(
|
|
99
|
+
(
|
|
100
|
+
"%s has no method %s, and the engine connects a signal that binds or unbinds "
|
|
101
|
+
+ "arguments only to a method its target has, so the scene would load without "
|
|
102
|
+
+ "the connection. Add the method to the target's script first, or connect "
|
|
103
|
+
+ "without binds and unbinds."
|
|
104
|
+
)
|
|
105
|
+
% [named_to, method_name]
|
|
106
|
+
)
|
|
107
|
+
)
|
|
95
108
|
var misfit: String = _misfit(source, signal_name, callable, flags)
|
|
96
109
|
if not before.is_empty():
|
|
97
110
|
var held: Callable = before["callable"]
|
|
@@ -102,6 +115,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
|
|
|
102
115
|
|
|
103
116
|
var from: String = scene.path_of(source)
|
|
104
117
|
var to: String = scene.path_of(target)
|
|
118
|
+
var _replaced: Dictionary = scene.drop_unmade(from, signal_name, to, method_name)
|
|
105
119
|
var saved: Dictionary = {}
|
|
106
120
|
var written: Dictionary = scene.write(
|
|
107
121
|
func(loaded: Node) -> String:
|
|
@@ -464,7 +478,11 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
|
|
|
464
478
|
# A connection that was never there saved the scene unchanged and answered as removed, so a
|
|
465
479
|
# typo in either name read as success.
|
|
466
480
|
var held: Dictionary = _saved(scene.root, from, signal_name, to, method_name)
|
|
467
|
-
|
|
481
|
+
# One the engine could not make is in the file alone, and the write drops it.
|
|
482
|
+
var unmade: Dictionary = scene.drop_unmade(from, signal_name, to, method_name)
|
|
483
|
+
if held.is_empty() and not unmade.is_empty():
|
|
484
|
+
held = { "flags": unmade["flags"], "binds": unmade["binds"], "unbinds": unmade["unbinds"] }
|
|
485
|
+
elif held.is_empty():
|
|
468
486
|
# The root's path is ".", which before ".method" read as "..method".
|
|
469
487
|
var said_from: String = str(source.name) if source == scene.root else from
|
|
470
488
|
var said_to: String = str(target.name) if target == scene.root else to
|
|
@@ -492,8 +510,9 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
|
|
|
492
510
|
)
|
|
493
511
|
)
|
|
494
512
|
)
|
|
495
|
-
|
|
496
|
-
|
|
513
|
+
if held.has("callable"):
|
|
514
|
+
var callable: Callable = held["callable"]
|
|
515
|
+
source.disconnect(signal_name, callable)
|
|
497
516
|
|
|
498
517
|
var written: Dictionary = scene.write(
|
|
499
518
|
func(loaded: Node) -> String:
|
|
@@ -303,6 +303,13 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
|
|
|
303
303
|
wanted.append([at, source_id, atlas_coords, alternative])
|
|
304
304
|
if source_id == -1:
|
|
305
305
|
erased += 1
|
|
306
|
+
# An erased cell leaves the layer's data at its next update, which a layer outside the tree
|
|
307
|
+
# never has, so the pack wrote it as a cell naming no tile, where the editor's save leaves it
|
|
308
|
+
# out.
|
|
309
|
+
if layered != null:
|
|
310
|
+
layered.update_internals()
|
|
311
|
+
else:
|
|
312
|
+
single.update_internals()
|
|
306
313
|
|
|
307
314
|
var at_path: String = scene.path_of(node)
|
|
308
315
|
var written: Dictionary = scene.write(
|
|
@@ -62,6 +62,15 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
62
62
|
"type": "error",
|
|
63
63
|
"message": GdhPaths.nothing_there(holder, named, str(reached["called"])),
|
|
64
64
|
}
|
|
65
|
+
if GdhPaths.is_constant(holder, named):
|
|
66
|
+
return {
|
|
67
|
+
"type": "error",
|
|
68
|
+
"message":
|
|
69
|
+
(
|
|
70
|
+
"%s:%s is a constant its script declares, and a constant cannot be set"
|
|
71
|
+
% [reached["called"], named]
|
|
72
|
+
),
|
|
73
|
+
}
|
|
65
74
|
var old_value: Variant = GdhPaths.read_under(holder, named)
|
|
66
75
|
var slot: Dictionary = GdhValues.slot_declared(holder, named)
|
|
67
76
|
var declared: int = slot["type"]
|
|
@@ -503,7 +503,7 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
503
503
|
var object: Object = holder
|
|
504
504
|
if not method.is_empty():
|
|
505
505
|
return call_refused(object, method, "", args.size()).is_empty()
|
|
506
|
-
return _has_property(object, named)
|
|
506
|
+
return _has_property(object, named) or not constant_of(object, named).is_empty()
|
|
507
507
|
return false
|
|
508
508
|
|
|
509
509
|
|
|
@@ -876,7 +876,7 @@ static func read_under(holder: Variant, named: String) -> Variant:
|
|
|
876
876
|
if not method.is_empty():
|
|
877
877
|
var args: Array = arguments_of(named)["args"]
|
|
878
878
|
return object.callv(method, args)
|
|
879
|
-
return object.get(named)
|
|
879
|
+
return constant_of(object, named)[0] if is_constant(object, named) else object.get(named)
|
|
880
880
|
|
|
881
881
|
|
|
882
882
|
## Where [param named] lands in a list of [param size], or -1 for a step that is not in it.
|
|
@@ -1030,3 +1030,30 @@ static func _has_property(holder: Object, named: String) -> bool:
|
|
|
1030
1030
|
if str(entry["name"]) == named:
|
|
1031
1031
|
return true
|
|
1032
1032
|
return false
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
## The constant [param named] that [param holder]'s script or a script it extends declares, in a
|
|
1036
|
+
## list of one, or an empty list. A constant is no property, so the property list never names it,
|
|
1037
|
+
## and `get` answers null for one.
|
|
1038
|
+
static func constant_of(holder: Variant, named: String) -> Array:
|
|
1039
|
+
if not holder is Object:
|
|
1040
|
+
return []
|
|
1041
|
+
var object: Object = holder
|
|
1042
|
+
var attached: Variant = object.get_script()
|
|
1043
|
+
if not attached is Script:
|
|
1044
|
+
return []
|
|
1045
|
+
var script: Script = attached
|
|
1046
|
+
while script != null:
|
|
1047
|
+
var constants: Dictionary = script.get_script_constant_map()
|
|
1048
|
+
if constants.has(named):
|
|
1049
|
+
return [constants[named]]
|
|
1050
|
+
script = script.get_base_script()
|
|
1051
|
+
return []
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
## Whether [param named] reaches a constant on [param holder] rather than a property.
|
|
1055
|
+
static func is_constant(holder: Variant, named: String) -> bool:
|
|
1056
|
+
if not holder is Object:
|
|
1057
|
+
return false
|
|
1058
|
+
var object: Object = holder
|
|
1059
|
+
return not _has_property(object, named) and not constant_of(object, named).is_empty()
|
|
@@ -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
|
|
@@ -11,6 +11,11 @@ const LOOP_MODE_NAMES: Dictionary[int, String] = {
|
|
|
11
11
|
Animation.LOOP_LINEAR: "linear",
|
|
12
12
|
Animation.LOOP_PINGPONG: "pingpong",
|
|
13
13
|
}
|
|
14
|
+
const UPDATE_MODE_NAMES: Dictionary[int, String] = {
|
|
15
|
+
Animation.UPDATE_CONTINUOUS: "continuous",
|
|
16
|
+
Animation.UPDATE_DISCRETE: "discrete",
|
|
17
|
+
Animation.UPDATE_CAPTURE: "capture",
|
|
18
|
+
}
|
|
14
19
|
const TRACK_TYPE_NAMES: Dictionary[int, String] = {
|
|
15
20
|
Animation.TYPE_VALUE: "property",
|
|
16
21
|
Animation.TYPE_POSITION_3D: "position_3d",
|
|
@@ -166,6 +171,9 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
|
|
|
166
171
|
var property: String = str(path.get_concatenated_subnames())
|
|
167
172
|
if not property.is_empty():
|
|
168
173
|
shaped["property"] = property
|
|
174
|
+
if kind == Animation.TYPE_VALUE:
|
|
175
|
+
var mode: int = anim.value_track_get_update_mode(track)
|
|
176
|
+
shaped["update"] = UPDATE_MODE_NAMES.get(mode, str(mode))
|
|
169
177
|
var keyframes: Array[Dictionary] = []
|
|
170
178
|
var methods: Dictionary = {}
|
|
171
179
|
for key: int in anim.track_get_key_count(track):
|
|
@@ -81,6 +81,8 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
81
81
|
var answered: Dictionary = answer
|
|
82
82
|
if not GdhSceneFile.also_saved.is_empty():
|
|
83
83
|
answered["alsoSaved"] = GdhSceneFile.also_saved.duplicate(true)
|
|
84
|
+
if not GdhSceneFile.unmade_note.is_empty():
|
|
85
|
+
answered["unmadeConnections"] = GdhSceneFile.unmade_note
|
|
84
86
|
if not GdhSceneUids.by_path.is_empty():
|
|
85
87
|
answered["namedByPath"] = (
|
|
86
88
|
(
|
|
@@ -121,6 +121,26 @@ static func _runnable(config: ConfigFile, section: String) -> bool:
|
|
|
121
121
|
return GdhRead.as_bool(config.get_value(section, "runnable", false))
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
## The name of the preset [param platform] runs, or "" when it has none: the one its section names,
|
|
125
|
+
## or in a file saved before that section, the last preset of the platform keeping the key, which is
|
|
126
|
+
## the one the editor ends up running. Read from the section alone, a file of that age had no
|
|
127
|
+
## runnable preset, so a preset added for its platform took the place of the one it had.
|
|
128
|
+
static func _runnable_name(config: ConfigFile, platform: String) -> String:
|
|
129
|
+
if config.has_section_key(RUNNABLE_SECTION, platform):
|
|
130
|
+
return str(config.get_value(RUNNABLE_SECTION, platform))
|
|
131
|
+
var named: String = ""
|
|
132
|
+
var at: int = 0
|
|
133
|
+
while config.has_section("preset." + str(at)):
|
|
134
|
+
var section: String = "preset." + str(at)
|
|
135
|
+
if (
|
|
136
|
+
str(config.get_value(section, "platform", "")) == platform
|
|
137
|
+
and GdhRead.as_bool(config.get_value(section, "runnable", false))
|
|
138
|
+
):
|
|
139
|
+
named = str(config.get_value(section, "name", ""))
|
|
140
|
+
at += 1
|
|
141
|
+
return named
|
|
142
|
+
|
|
143
|
+
|
|
124
144
|
## Adds an export preset to export_presets.cfg, written with the keys the editor's Project > Export
|
|
125
145
|
## writes for a new one, and every option in [code]known_options[/code] (what
|
|
126
146
|
## [method export_options] answers for the platform) at its default. The editor also writes the
|
|
@@ -160,7 +180,7 @@ func add_export_preset(params: Dictionary) -> Dictionary:
|
|
|
160
180
|
)
|
|
161
181
|
index += 1
|
|
162
182
|
# A platform with no preset to run takes the new one, as the editor's dialog does.
|
|
163
|
-
var runnable: bool =
|
|
183
|
+
var runnable: bool = _runnable_name(config, platform).is_empty()
|
|
164
184
|
if runnable:
|
|
165
185
|
config.set_value(RUNNABLE_SECTION, platform, preset_name)
|
|
166
186
|
|
|
@@ -609,6 +629,7 @@ func remove_export_preset(params: Dictionary) -> Dictionary:
|
|
|
609
629
|
var index: int = found["index"]
|
|
610
630
|
var section: String = found["section"]
|
|
611
631
|
var platform: String = str(config.get_value(section, "platform", ""))
|
|
632
|
+
var was_runnable: bool = _runnable_name(config, platform) == preset_name
|
|
612
633
|
var last: int = index
|
|
613
634
|
while config.has_section("preset." + str(last + 1)):
|
|
614
635
|
last += 1
|
|
@@ -622,11 +643,17 @@ func remove_export_preset(params: Dictionary) -> Dictionary:
|
|
|
622
643
|
if keeping != OK:
|
|
623
644
|
return _log.failure("Failed to save %s: %s" % [CREDENTIALS_FILE, error_string(keeping)])
|
|
624
645
|
answer["credentials"] = CREDENTIALS_FILE
|
|
625
|
-
if
|
|
626
|
-
config.
|
|
627
|
-
|
|
646
|
+
if was_runnable:
|
|
647
|
+
if config.has_section_key(RUNNABLE_SECTION, platform):
|
|
648
|
+
config.set_value(RUNNABLE_SECTION, platform, null)
|
|
649
|
+
# A file saved before the section can mark another preset of the platform too, which the
|
|
650
|
+
# editor then runs.
|
|
651
|
+
var kept: String = _runnable_name(config, platform)
|
|
652
|
+
answer["runnableNow"] = kept
|
|
653
|
+
if kept.is_empty():
|
|
654
|
+
answer["runnableNow"] = null
|
|
628
655
|
var at: int = 0
|
|
629
|
-
while config.has_section("preset." + str(at)):
|
|
656
|
+
while kept.is_empty() and config.has_section("preset." + str(at)):
|
|
630
657
|
if str(config.get_value("preset." + str(at), "platform", "")) == platform:
|
|
631
658
|
var next: String = str(config.get_value("preset." + str(at), "name", ""))
|
|
632
659
|
config.set_value(RUNNABLE_SECTION, platform, next)
|
|
@@ -96,6 +96,11 @@ static func without_annotations(line: String) -> String:
|
|
|
96
96
|
# brace opens before the `#` and closes on a later line. Held by a case, since the argument for the
|
|
97
97
|
# difference is not visible from the code and reads like an oversight beside its neighbours.
|
|
98
98
|
static func without_comment(line: String) -> String:
|
|
99
|
+
var start: int = _comment_start(line)
|
|
100
|
+
return (line if start == -1 else line.substr(0, start)).strip_edges()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
static func _comment_start(line: String) -> int:
|
|
99
104
|
var quote: String = ""
|
|
100
105
|
for i: int in range(line.length()):
|
|
101
106
|
var ch: String = line[i]
|
|
@@ -104,10 +109,30 @@ static func without_comment(line: String) -> String:
|
|
|
104
109
|
quote = ""
|
|
105
110
|
continue
|
|
106
111
|
if ch == "#":
|
|
107
|
-
return
|
|
112
|
+
return i
|
|
108
113
|
if ch == '"' or ch == "'":
|
|
109
114
|
quote = ch
|
|
110
|
-
return
|
|
115
|
+
return -1
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## The code in front of each TODO or FIXME comment in [param content], one entry a comment.
|
|
119
|
+
##
|
|
120
|
+
## Read from the comments alone, as the editor highlights them, so `#TODO` counts and the same
|
|
121
|
+
## words inside a string or a multi-line string do not.
|
|
122
|
+
static func noted_work(content: String) -> Array[String]:
|
|
123
|
+
var noted: Array[String] = []
|
|
124
|
+
var in_string: bool = false
|
|
125
|
+
for line: String in content.split("\n"):
|
|
126
|
+
if in_string:
|
|
127
|
+
in_string = triple_quotes(line) % 2 == 0
|
|
128
|
+
continue
|
|
129
|
+
var start: int = _comment_start(line)
|
|
130
|
+
var code: String = line if start == -1 else line.substr(0, start)
|
|
131
|
+
if triple_quotes(code) % 2 == 1:
|
|
132
|
+
in_string = true
|
|
133
|
+
elif start != -1 and compiled("^#+\\s*(?:TODO|FIXME)\\b").search(line.substr(start)):
|
|
134
|
+
noted.append(code.strip_edges())
|
|
135
|
+
return noted
|
|
111
136
|
|
|
112
137
|
|
|
113
138
|
## How many triple quotes of either kind [param line] holds. An odd count opens or closes a string
|
|
@@ -5,6 +5,7 @@ const GdhFileWalk = preload("file_walk.gd")
|
|
|
5
5
|
const GdhImportPipeline = preload("import_pipeline.gd")
|
|
6
6
|
const GdhInputActions = preload("input_actions.gd")
|
|
7
7
|
const GdhLog = preload("logger.gd")
|
|
8
|
+
const GdhPatterns = preload("patterns.gd")
|
|
8
9
|
const GdhRead = preload("reading.gd")
|
|
9
10
|
const GdhSceneConnections = preload("scene_connections.gd")
|
|
10
11
|
const GdhScriptCheck = preload("script_check.gd")
|
|
@@ -87,7 +88,7 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
87
88
|
var content: String = file.get_as_text()
|
|
88
89
|
file.close()
|
|
89
90
|
|
|
90
|
-
if
|
|
91
|
+
if not GdhPatterns.noted_work(content).is_empty():
|
|
91
92
|
todo_count += 1
|
|
92
93
|
|
|
93
94
|
# Work noted for later, which no script fails on, so said rather than failed. A count of
|
|
@@ -8,6 +8,7 @@ const GdhFileWalk = preload("file_walk.gd")
|
|
|
8
8
|
const GdhImportPipeline = preload("import_pipeline.gd")
|
|
9
9
|
const GdhInputActions = preload("input_actions.gd")
|
|
10
10
|
const GdhLog = preload("logger.gd")
|
|
11
|
+
const GdhPatterns = preload("patterns.gd")
|
|
11
12
|
const GdhRead = preload("reading.gd")
|
|
12
13
|
const GdhSceneConnections = preload("scene_connections.gd")
|
|
13
14
|
const GdhScriptCheck = preload("script_check.gd")
|
|
@@ -205,11 +206,12 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
205
206
|
var content: String = file.get_as_text()
|
|
206
207
|
file.close()
|
|
207
208
|
|
|
208
|
-
|
|
209
|
+
var noted: Array[String] = GdhPatterns.noted_work(content)
|
|
210
|
+
if not noted.is_empty():
|
|
209
211
|
script_issues.append(
|
|
210
212
|
{ "path": script_path, "issue": "Contains TODO/FIXME comments" }
|
|
211
213
|
)
|
|
212
|
-
if "pass
|
|
214
|
+
if "pass" in noted:
|
|
213
215
|
script_issues.append(
|
|
214
216
|
{ "path": script_path, "issue": "Contains unimplemented functions" }
|
|
215
217
|
)
|
package/build/keeper.js
CHANGED
|
@@ -8895,6 +8895,7 @@ function frameEndLine() {
|
|
|
8895
8895
|
return frameEnd;
|
|
8896
8896
|
}
|
|
8897
8897
|
var READ_MS = 5000;
|
|
8898
|
+
var EVALUATE_MS = 60000;
|
|
8898
8899
|
var REPLIES = new Set([
|
|
8899
8900
|
"debug_enter",
|
|
8900
8901
|
"debug_exit",
|
|
@@ -9478,10 +9479,10 @@ class GameDebugger {
|
|
|
9478
9479
|
}
|
|
9479
9480
|
this.forget("evaluation_return");
|
|
9480
9481
|
this.send("evaluate", [expression, frame], held.thread);
|
|
9481
|
-
const answered = await this.next(GameDebugger.named("evaluation_return"),
|
|
9482
|
+
const answered = await this.next(GameDebugger.named("evaluation_return"), EVALUATE_MS);
|
|
9482
9483
|
if (answered === null || !("data" in answered)) {
|
|
9483
9484
|
return {
|
|
9484
|
-
refused: `The game worked out nothing for ${expression} within ${
|
|
9485
|
+
refused: `The game worked out nothing for ${expression} within ${EVALUATE_MS / 1000}s. The first expression a game evaluates loads the script of every global class in the project first, static initialisers included, so one of those may still be running. Ask again.`
|
|
9485
9486
|
};
|
|
9486
9487
|
}
|
|
9487
9488
|
const [, , type, value, hint] = answered.data;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.47",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|