gdharness 1.3.60 → 1.3.62
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/README.md +20 -20
- package/build/cli.js +2198 -1757
- package/build/godot/addons/auto_reload/auto_reload.gd +3 -3
- package/build/godot/addons/gdharness_editor/bridge_client.gd +4 -4
- package/build/godot/addons/gdharness_editor/declared_values.gd +13 -13
- package/build/godot/addons/gdharness_editor/file_reads.gd +6 -6
- package/build/godot/addons/gdharness_editor/headless_check.gd +9 -6
- package/build/godot/addons/gdharness_editor/property_paths.gd +3 -3
- package/build/godot/addons/gdharness_editor/property_values.gd +10 -10
- package/build/godot/addons/gdharness_editor/reading.gd +22 -18
- package/build/godot/addons/gdharness_editor/scene_file.gd +5 -5
- package/build/godot/addons/gdharness_editor/serialisation.gd +4 -4
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +5 -5
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +6 -6
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +9 -9
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +3 -3
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +5 -5
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +5 -5
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +2 -2
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +1 -1
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +5 -5
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +3 -3
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +3 -3
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +4 -4
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +3 -3
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +10 -10
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +1 -1
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +5 -5
- package/build/godot/addons/gdharness_runtime/declared_values.gd +13 -13
- package/build/godot/addons/gdharness_runtime/reading.gd +22 -18
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +89 -18
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +6 -6
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +10 -10
- package/build/godot/addons/gdharness_runtime/runtime_frames.gd +5 -4
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +3 -4
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +8 -8
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +1 -1
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +3 -3
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +5 -5
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +20 -20
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +19 -13
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +5 -5
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +39 -39
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +3 -3
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +7 -7
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +8 -8
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +5 -5
- package/build/godot/addons/gdharness_runtime/serialisation.gd +4 -4
- package/build/godot/addons/gdharness_tests/bench.gd +1 -1
- package/build/godot/addons/gdharness_tests/share.gd +6 -6
- package/build/godot/addons/gdharness_tests/suites.gd +1 -1
- package/build/godot/operations/audio_buses.gd +4 -4
- package/build/godot/operations/class_cache.gd +3 -3
- package/build/godot/operations/classdb_queries.gd +1 -1
- package/build/godot/operations/declared_values.gd +13 -13
- package/build/godot/operations/dependencies.gd +6 -6
- package/build/godot/operations/export_presets.gd +7 -7
- package/build/godot/operations/file_reads.gd +6 -6
- package/build/godot/operations/gdscript_analysis.gd +6 -6
- package/build/godot/operations/gdscript_authoring.gd +11 -11
- package/build/godot/operations/godot_operations.gd +1 -1
- package/build/godot/operations/import_pipeline.gd +12 -12
- package/build/godot/operations/input_actions.gd +3 -3
- package/build/godot/operations/logger.gd +1 -1
- package/build/godot/operations/plugins.gd +2 -2
- package/build/godot/operations/project_config.gd +11 -11
- package/build/godot/operations/project_validation.gd +1 -1
- package/build/godot/operations/property_paths.gd +3 -3
- package/build/godot/operations/property_values.gd +10 -10
- package/build/godot/operations/reading.gd +22 -18
- package/build/godot/operations/resource_files.gd +1 -1
- package/build/godot/operations/script_check.gd +2 -2
- package/build/godot/operations/serialisation.gd +4 -4
- package/build/godot/tests/listed.gd +9 -9
- package/build/keeper.js +511 -162
- package/package.json +1 -1
|
@@ -20,14 +20,14 @@ static func menu_of(node: Node) -> PopupMenu:
|
|
|
20
20
|
return null
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
## The items asked for, best first: the one at `index
|
|
24
|
-
## exactly and then case-insensitively
|
|
23
|
+
## The items asked for, best first: the one at `index`, else the one whose whole words are `text`,
|
|
24
|
+
## exactly and then case-insensitively, else every item `text` matches by the rules `says` has, a
|
|
25
25
|
## plain word as a contains, a glob, and alternatives split at `|`, the enabled ones when any are.
|
|
26
26
|
## Empty when nothing matches, and more than one when several match equally well, which the caller
|
|
27
27
|
## refuses rather than guesses between.
|
|
28
28
|
##
|
|
29
29
|
## By the same rules as `says` because a caller who has learnt them from a click, a find and a wait
|
|
30
|
-
## writes the same pattern here: "Leave*" was refused against a menu holding "Leave the
|
|
30
|
+
## writes the same pattern here: "Leave*" was refused against a menu holding "Leave the shop", and
|
|
31
31
|
## the item had to be written out in full on a second call. The whole words still come first, so an
|
|
32
32
|
## item named in full is chosen over another whose words contain it.
|
|
33
33
|
##
|
|
@@ -21,7 +21,7 @@ static func read_point(params: Dictionary, x_key: String, y_key: String) -> Vari
|
|
|
21
21
|
## row, where the control draws it.
|
|
22
22
|
##
|
|
23
23
|
## For a control that draws its parts itself and hit-tests them in _gui_input, whose centre was all
|
|
24
|
-
## a click could press
|
|
24
|
+
## a click could press. In a native window, where the game's window pixels do not reach, nothing
|
|
25
25
|
## else could press the rest.
|
|
26
26
|
static func point_in(params: Dictionary, node: Node, node_path: String, has_item: bool) -> Variant:
|
|
27
27
|
if not params.has("x") and not params.has("y"):
|
|
@@ -29,7 +29,7 @@ static func point_in(params: Dictionary, node: Node, node_path: String, has_item
|
|
|
29
29
|
if node is Node3D:
|
|
30
30
|
return (
|
|
31
31
|
(
|
|
32
|
-
"%s is a 3D node, clicked where it is drawn
|
|
32
|
+
"%s is a 3D node, clicked where it is drawn. x and y name a point in a Control's "
|
|
33
33
|
+ "own space"
|
|
34
34
|
)
|
|
35
35
|
% node_path
|
|
@@ -70,7 +70,7 @@ static func aim_of(params: Dictionary, root: Window) -> Variant:
|
|
|
70
70
|
## Where [param local], in the space of the node [param aim] names, arrives: {"viewport",
|
|
71
71
|
## "position"}, the viewport the pointer reaches it through and the point in that viewport's pixels,
|
|
72
72
|
## which is what an event sent there names. A Window's space is its own pixels, as the game's window
|
|
73
|
-
## pixels are the root's
|
|
73
|
+
## pixels are the root's. A canvas item's is its own, the one it draws and reads _gui_input in.
|
|
74
74
|
static func carried(aim: Dictionary, local: Vector2, root: Window) -> Dictionary:
|
|
75
75
|
var node: Node = aim.get("node", root)
|
|
76
76
|
var reached: Dictionary = {}
|
|
@@ -192,7 +192,7 @@ static func outside_noted(
|
|
|
192
192
|
"%s is outside %s viewport, %s in window pixels, so no control there receives"
|
|
193
193
|
% [point, which, shown]
|
|
194
194
|
)
|
|
195
|
-
+ " it
|
|
195
|
+
+ " it. The game's _input and _unhandled_input still do."
|
|
196
196
|
)
|
|
197
197
|
break
|
|
198
198
|
return answer
|
|
@@ -270,7 +270,7 @@ static func takes_no_click(control: Control, node_path: String, paused: bool) ->
|
|
|
270
270
|
if not control.can_process():
|
|
271
271
|
var why: String = GdhTargets.why_it_does_not_process(control, node_path)
|
|
272
272
|
var over: Node = GdhTargets.drawn_over(control)
|
|
273
|
-
var cover: String = "" if over == null else "
|
|
273
|
+
var cover: String = "" if over == null else ", and %s is drawn over it" % over.get_path()
|
|
274
274
|
if paused:
|
|
275
275
|
return (
|
|
276
276
|
(
|
|
@@ -297,10 +297,10 @@ const READS_ONLY: String = (
|
|
|
297
297
|
## read it, "board:size()" or "inbox:keys()", listed in [constant LIST_CALLS] and
|
|
298
298
|
## [constant MAP_CALLS]. An engine method that reads takes literal arguments in the brackets,
|
|
299
299
|
## "get_theme_stylebox(\"panel\"):content_margin_top", since what a control's theme gives it is only
|
|
300
|
-
## reachable by naming what is asked for
|
|
300
|
+
## reachable by naming what is asked for. See [method arguments_of].
|
|
301
301
|
##
|
|
302
302
|
## A step into a vector, a colour, a rectangle or a transform reads one of its components,
|
|
303
|
-
## "size:x" or "global_transform:origin:y", by the names GDScript gives them
|
|
303
|
+
## "size:x" or "global_transform:origin:y", by the names GDScript gives them. See
|
|
304
304
|
## [method GdhSerialisation.components_of]. It answers the calls in [constant VALUE_CALLS] too,
|
|
305
305
|
## "linear_velocity:length()".
|
|
306
306
|
##
|
|
@@ -337,7 +337,7 @@ static func walk_to(node: Object, node_path: String, reaching: String) -> Dictio
|
|
|
337
337
|
## `{"message": ...}` saying why it names none.
|
|
338
338
|
##
|
|
339
339
|
## A colon path read from [param node], the node the call was made on, the way the method's own
|
|
340
|
-
## path is: "
|
|
340
|
+
## path is: "_league:store:_items:-15". One beginning with a slash starts at the node it names
|
|
341
341
|
## instead, "/root/Main/Hud" or "/root/Main:_game:run". A res:// or uid:// path names a resource
|
|
342
342
|
## file, which is loaded the way the game loads one, so a resource the game already holds is handed
|
|
343
343
|
## over as that instance. The records a read answers a node and a filed resource with carry such a
|
|
@@ -387,7 +387,7 @@ static func object_at(root: Node, node: Node, node_path: String, given: Variant)
|
|
|
387
387
|
|
|
388
388
|
|
|
389
389
|
## Why [param record], a dictionary given where an object goes, names none. A resource built into a
|
|
390
|
-
## scene is read as its class beside its properties and has no path
|
|
390
|
+
## scene is read as its class beside its properties and has no path. The runtime builds no resource
|
|
391
391
|
## from those, so the remedy is the file, not the path the other records are told to use.
|
|
392
392
|
static func _no_path_in(record: Dictionary) -> String:
|
|
393
393
|
if record.has("id"):
|
|
@@ -439,7 +439,7 @@ static func _resource_at(path: String) -> Dictionary:
|
|
|
439
439
|
return { "message": "No resource at %s" % path }
|
|
440
440
|
var loaded: Resource = load(path)
|
|
441
441
|
if loaded == null:
|
|
442
|
-
return { "message": "%s did not load
|
|
442
|
+
return { "message": "%s did not load. The game's output says why" % path }
|
|
443
443
|
return { "object": loaded }
|
|
444
444
|
|
|
445
445
|
|
|
@@ -580,7 +580,7 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
580
580
|
## counted as unclean for a read.
|
|
581
581
|
##
|
|
582
582
|
## A step written in double quotes is the key inside them, so a key holding a colon is one step:
|
|
583
|
-
## Game D keys its stock by namespaced ids, "stock:\"base:
|
|
583
|
+
## Game D keys its stock by namespaced ids, "stock:\"base:water\"", and split at that colon the
|
|
584
584
|
## step named a key "base" the map had never had.
|
|
585
585
|
static func _key_in(map: Dictionary, named: String) -> Array:
|
|
586
586
|
var word: String = key_written(named)
|
|
@@ -625,8 +625,8 @@ static func step_naming(key: Variant) -> String:
|
|
|
625
625
|
|
|
626
626
|
## [param reaching] split into its steps at each colon, except one inside a call's brackets or a
|
|
627
627
|
## quoted argument or key: `get_node("/root/Main"):name` is two steps, and so is
|
|
628
|
-
## `stock:"base:
|
|
629
|
-
## nothing.
|
|
628
|
+
## `stock:"base:water"`. Split at every colon a quoted argument holding one became a step that
|
|
629
|
+
## names nothing.
|
|
630
630
|
static func steps_of(reaching: String) -> PackedStringArray:
|
|
631
631
|
var steps: PackedStringArray = []
|
|
632
632
|
var step: String = ""
|
|
@@ -734,7 +734,7 @@ static func _arity(object: Object, method: String) -> Dictionary:
|
|
|
734
734
|
## A path is read, and a wait reads it every frame, so a call in one has to be a read. The engine
|
|
735
735
|
## marks its own reading methods const and `queue_free()`, `free()` and `set_name()` not, measured
|
|
736
736
|
## on 4.7.2, though it leaves some reads unmarked as well, `Engine.get_frames_drawn()` and
|
|
737
|
-
## `get_time_scale()` among them
|
|
737
|
+
## `get_time_scale()` among them, so the refusal says the mark is missing, not that the method
|
|
738
738
|
## changes anything, and points at runtime_invoke call, which calls it. A game's methods carry no
|
|
739
739
|
## such mark, so one declared `-> void` is taken at its word, as doing something and answering
|
|
740
740
|
## nothing, and one left untyped is trusted. Without this a find with property "queue_free()" freed
|
|
@@ -756,7 +756,7 @@ static func call_refused(
|
|
|
756
756
|
var scripted: Dictionary = _script_method(object, method)
|
|
757
757
|
if not scripted.is_empty() and (given > 0 or least > 0):
|
|
758
758
|
var why: String = (
|
|
759
|
-
"since nothing marks one as only reading%s
|
|
759
|
+
"since nothing marks one as only reading%s. runtime_invoke call takes them" % READS_ONLY
|
|
760
760
|
)
|
|
761
761
|
return (
|
|
762
762
|
"%s.%s is the game's own and a path gives a game's method no arguments, %s"
|
|
@@ -784,7 +784,7 @@ static func call_refused(
|
|
|
784
784
|
return ""
|
|
785
785
|
if _native_flags(object, method) & METHOD_FLAG_CONST == 0:
|
|
786
786
|
return (
|
|
787
|
-
"%s.%s is not one the engine marks as only reading%s
|
|
787
|
+
"%s.%s is not one the engine marks as only reading%s. runtime_invoke call calls it"
|
|
788
788
|
% [called_as, method, READS_ONLY]
|
|
789
789
|
)
|
|
790
790
|
return ""
|
|
@@ -817,14 +817,14 @@ static func with_methods_offered(said: String, object: Object, method: String) -
|
|
|
817
817
|
var close: PackedStringArray = GdhRead.nearest(method, PackedStringArray(every))
|
|
818
818
|
if not close.is_empty():
|
|
819
819
|
return (
|
|
820
|
-
"%s
|
|
821
|
-
% [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
820
|
+
"%s The nearest it has %s %s"
|
|
821
|
+
% [GdhRead.ended(said), "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
822
822
|
)
|
|
823
823
|
if own.is_empty():
|
|
824
824
|
return said
|
|
825
825
|
own.sort()
|
|
826
|
-
var offered: String = "
|
|
827
|
-
return "%s
|
|
826
|
+
var offered: String = "Its static functions are" if object is Script else "Its script declares"
|
|
827
|
+
return "%s %s %s" % [GdhRead.ended(said), offered, ", ".join(PackedStringArray(own))]
|
|
828
828
|
|
|
829
829
|
|
|
830
830
|
## The entry a script attached to [param object] declares for [param method], or an empty one when
|
|
@@ -851,7 +851,7 @@ static func _native_flags(object: Object, method: String) -> int:
|
|
|
851
851
|
|
|
852
852
|
## Put [param value] where [param named] points, and answer the holder written into. Only ever
|
|
853
853
|
## called once [method can_read] agrees. Lists and maps are reference types, so writing into one
|
|
854
|
-
## reaches the game's own copy
|
|
854
|
+
## reaches the game's own copy. A vector or a transform is a copy, and the one answered is the
|
|
855
855
|
## changed copy, for [method write_back] to put where it came from.
|
|
856
856
|
static func write(holder: Variant, named: String, value: Variant) -> Variant:
|
|
857
857
|
if holder is Array:
|
|
@@ -1018,7 +1018,7 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
1018
1018
|
var rest: String = ""
|
|
1019
1019
|
if keys.size() > 8:
|
|
1020
1020
|
rest = " and %d more" % [keys.size() - 8]
|
|
1021
|
-
return "%s has no key %s
|
|
1021
|
+
return "%s has no key %s. It is keyed by %s%s" % [called_as, named, ", ".join(some), rest]
|
|
1022
1022
|
if composite(holder):
|
|
1023
1023
|
var kind: String = type_string(typeof(holder))
|
|
1024
1024
|
var sort: String = ("an " if kind.left(1) in ["A", "E", "I", "O", "U"] else "a ") + kind
|
|
@@ -1036,11 +1036,11 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
1036
1036
|
return (
|
|
1037
1037
|
(
|
|
1038
1038
|
"%s is %s, and %s() is not one of the calls a path makes on one, which read it"
|
|
1039
|
-
+ " and take no arguments: %s
|
|
1039
|
+
+ " and take no arguments: %s. Its components are %s"
|
|
1040
1040
|
)
|
|
1041
1041
|
% [called_as, sort, container_call, ", ".join(spelled), has]
|
|
1042
1042
|
)
|
|
1043
|
-
return "%s is %s, so it has no %s
|
|
1043
|
+
return "%s is %s, so it has no %s. It has %s" % [called_as, sort, named, has]
|
|
1044
1044
|
if not holder is Object:
|
|
1045
1045
|
return (
|
|
1046
1046
|
"%s is %s, which has nothing to read %s off"
|
|
@@ -1054,7 +1054,7 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
1054
1054
|
# for, and the caller is one pair of brackets away from what they meant.
|
|
1055
1055
|
if object.has_method(named):
|
|
1056
1056
|
return (
|
|
1057
|
-
"%s has no property %s
|
|
1057
|
+
"%s has no property %s. It has a method of that name, which a path calls as %s()"
|
|
1058
1058
|
% [called_as, named, named]
|
|
1059
1059
|
)
|
|
1060
1060
|
return GdhRead.near_property("%s has no property %s" % [called_as, named], object, named)
|
|
@@ -16,7 +16,7 @@ const FIND_LIMIT: int = 100
|
|
|
16
16
|
## What a find can be narrowed by, which is also what it refuses to answer without. Named once so
|
|
17
17
|
## the refusal lists the same set the walk reads.
|
|
18
18
|
const FILTERS: PackedStringArray = ["class", "script", "name", "group", "says"]
|
|
19
|
-
## The most lines one read answers with, unless asked for fewer. A screen is a few dozen
|
|
19
|
+
## The most lines one read answers with, unless asked for fewer. A screen is a few dozen. A
|
|
20
20
|
## thousand is a tree somebody pointed this at by mistake.
|
|
21
21
|
const READ_LIMIT: int = 500
|
|
22
22
|
## The most either can be asked for. A limit above it is refused rather than cut to it: the ceiling
|
|
@@ -117,7 +117,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
117
117
|
var names: Array[String] = GdhSays.split_at_bars(wanted["name"])
|
|
118
118
|
if not wanted["name"].is_empty() and names.is_empty():
|
|
119
119
|
var bars_only: String = (
|
|
120
|
-
'name "%s" is only bars, so it names no node
|
|
120
|
+
'name "%s" is only bars, so it names no node. Write the names between them'
|
|
121
121
|
)
|
|
122
122
|
return { "type": "error", "message": bars_only % wanted["name"] }
|
|
123
123
|
|
|
@@ -244,7 +244,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
244
244
|
. append(
|
|
245
245
|
(
|
|
246
246
|
(
|
|
247
|
-
"name is matched as a glob against the whole name
|
|
247
|
+
"name is matched as a glob against the whole name. %d %snode %s %s, which "
|
|
248
248
|
+ '"%s"%s would find'
|
|
249
249
|
)
|
|
250
250
|
% [
|
|
@@ -265,7 +265,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
265
265
|
. append(
|
|
266
266
|
(
|
|
267
267
|
(
|
|
268
|
-
"says with a wildcard is matched as a glob against a node\'s whole text
|
|
268
|
+
"says with a wildcard is matched as a glob against a node\'s whole text. "
|
|
269
269
|
+ '"%s"%s would find %d %snode%s'
|
|
270
270
|
)
|
|
271
271
|
% [
|
|
@@ -284,7 +284,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
284
284
|
answer["hidden"] = hidden
|
|
285
285
|
notes.append(
|
|
286
286
|
(
|
|
287
|
-
"%d matching node%s hidden and left out
|
|
287
|
+
"%d matching node%s hidden and left out. includeHidden true answers with them too"
|
|
288
288
|
% [hidden, "" if hidden == 1 else "s"]
|
|
289
289
|
)
|
|
290
290
|
)
|
|
@@ -296,14 +296,14 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
296
296
|
(
|
|
297
297
|
(
|
|
298
298
|
"%d matching node%s drawn outside the viewport or scrolled out of a "
|
|
299
|
-
+ "ScrollContainer and left out
|
|
299
|
+
+ "ScrollContainer and left out. includeHidden true answers with them too"
|
|
300
300
|
)
|
|
301
301
|
% [out_of_view, "" if out_of_view == 1 else "s"]
|
|
302
302
|
)
|
|
303
303
|
)
|
|
304
304
|
)
|
|
305
305
|
if not notes.is_empty():
|
|
306
|
-
answer["note"] = " ".join(notes)
|
|
306
|
+
answer["note"] = ". ".join(notes) + "."
|
|
307
307
|
return answer
|
|
308
308
|
|
|
309
309
|
|
|
@@ -451,7 +451,7 @@ func _matches_apart_from_name(node: Node, wanted: Dictionary[String, String]) ->
|
|
|
451
451
|
## own class_name, or one it extends at any distance.
|
|
452
452
|
##
|
|
453
453
|
## The class_name alone answered 0 for `Card` over a tree of rows whose scripts extend Card two
|
|
454
|
-
## steps down,
|
|
454
|
+
## steps down, InboxRow extends TicketCard extends Card, while `is_class` reaches every native
|
|
455
455
|
## subclass. A caller naming a class means what extends it, whichever side of the line the class
|
|
456
456
|
## is declared on.
|
|
457
457
|
static func _script_is(attached: Script, wanted: String) -> bool:
|
|
@@ -468,15 +468,15 @@ static func _script_is(attached: Script, wanted: String) -> bool:
|
|
|
468
468
|
## The question a caller asks most often, and the one that cost the most to answer: reading a panel
|
|
469
469
|
## was a find for every label with the path of each beside it, a few hundred characters apiece to
|
|
470
470
|
## carry a sentence of six words. Long is the smaller half of it. A find answers off nodes nobody
|
|
471
|
-
## can see, and a panel keeps its empty state in the tree beside its rows, so "Nothing
|
|
472
|
-
## back next to the three things
|
|
471
|
+
## can see, and a panel keeps its empty state in the tree beside its rows, so "Nothing listed" came
|
|
472
|
+
## back next to the three things listed and was believed twice in one session.
|
|
473
473
|
##
|
|
474
474
|
## A hidden node is left out and so is everything under it, because what a player reads is what is
|
|
475
475
|
## drawn. [param include_hidden] asks for the lot instead, which is what a caller checking that
|
|
476
476
|
## something is not showing wants.
|
|
477
477
|
##
|
|
478
478
|
## [param limit] is the first few lines rather than all of them, which is how the top of a screen
|
|
479
|
-
## is read without the
|
|
479
|
+
## is read without the rest under it: the bar along the top of a screen is eleven lines and the
|
|
480
480
|
## panel it sits on is two hundred. `omitted` says how many lines that left behind, so a screen
|
|
481
481
|
## whose interesting half is below the cut says so in a number rather than in a flag.
|
|
482
482
|
func read_text(params: Dictionary) -> Dictionary:
|
|
@@ -519,7 +519,7 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
519
519
|
(
|
|
520
520
|
(
|
|
521
521
|
"%d line%s drawn outside the viewport or scrolled out of a "
|
|
522
|
-
+ "ScrollContainer, where the player cannot read them, left out
|
|
522
|
+
+ "ScrollContainer, where the player cannot read them, left out. "
|
|
523
523
|
+ "includeHidden true answers with them too"
|
|
524
524
|
)
|
|
525
525
|
% [tally["out_of_view"], "" if tally["out_of_view"] == 1 else "s"]
|
|
@@ -545,7 +545,7 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
545
545
|
)
|
|
546
546
|
)
|
|
547
547
|
if not notes.is_empty():
|
|
548
|
-
answer["note"] = " ".join(notes)
|
|
548
|
+
answer["note"] = ". ".join(notes) + "."
|
|
549
549
|
return answer
|
|
550
550
|
|
|
551
551
|
|
|
@@ -954,6 +954,12 @@ func _serialize_node_tree(
|
|
|
954
954
|
_serialize_node_tree(child, depth + 1, max_depth, include_properties, named)
|
|
955
955
|
)
|
|
956
956
|
result["children"] = children
|
|
957
|
+
else:
|
|
958
|
+
# Below the depth asked for, said rather than left out: a container with nothing under it
|
|
959
|
+
# read as empty, and the buttons of a dialog six levels down were never looked for.
|
|
960
|
+
var below: int = node.get_child_count(true)
|
|
961
|
+
if below > 0:
|
|
962
|
+
result["children_not_shown"] = below
|
|
957
963
|
|
|
958
964
|
return result
|
|
959
965
|
|
|
@@ -10,11 +10,11 @@ const ESCAPED_BAR: String = ""
|
|
|
10
10
|
## given and the same words widened, and anything past a handful is a caller that has moved on.
|
|
11
11
|
const SPLITS_KEPT: int = 8
|
|
12
12
|
|
|
13
|
-
## Each `says` split into alternatives, by what was written
|
|
13
|
+
## Each `says` split into alternatives, by what was written. See [method alternatives].
|
|
14
14
|
static var _splits: Dictionary[String, Array] = {}
|
|
15
|
-
## Each `says` as the globs [method matches] tries first, by what was written
|
|
15
|
+
## Each `says` as the globs [method matches] tries first, by what was written. See [method _loose].
|
|
16
16
|
static var _globs: Dictionary[String, Array] = {}
|
|
17
|
-
## Every space separator Unicode has besides the plain space, built once
|
|
17
|
+
## Every space separator Unicode has besides the plain space, built once. See [method spaced].
|
|
18
18
|
static var _spaces: RegEx = null
|
|
19
19
|
|
|
20
20
|
|
|
@@ -71,7 +71,7 @@ static func _loose(wanted: String) -> Array[String]:
|
|
|
71
71
|
|
|
72
72
|
## [param text] with every Unicode space separator written as a plain space, which is what a caller
|
|
73
73
|
## types for any of them. A game keeping a name on one line writes it with a non-breaking space, and
|
|
74
|
-
## "
|
|
74
|
+
## "Teodor Vasquelle" typed with a plain one found nothing while the screen showed exactly those
|
|
75
75
|
## words. One character for one, so a place found in the result is the same place in [param text].
|
|
76
76
|
##
|
|
77
77
|
## Text that is all ASCII holds none of them and is handed back as it is, which is most of a screen.
|
|
@@ -101,7 +101,7 @@ static func spaced(text: String) -> String:
|
|
|
101
101
|
## pattern that matches every node there is.
|
|
102
102
|
##
|
|
103
103
|
## Kept once split, since a find, a click or a wait asks about the same words for every node on the
|
|
104
|
-
## screen: split afresh for each of them, one look over
|
|
104
|
+
## screen: split afresh for each of them, one look over twenty thousand nodes took 210ms
|
|
105
105
|
## rather than 155ms. The array handed back is shared and not to be changed.
|
|
106
106
|
static func alternatives(wanted: String) -> Array[String]:
|
|
107
107
|
var kept: Variant = _splits.get(wanted)
|
|
@@ -48,8 +48,8 @@ const WHOLE: int = 1
|
|
|
48
48
|
## counting from 0 in rank order.
|
|
49
49
|
##
|
|
50
50
|
## A control with something drawn over its centre that a pointer there would reach instead is not
|
|
51
|
-
## on screen for a player, and a click at it lands on the thing on top.
|
|
52
|
-
## over
|
|
51
|
+
## on screen for a player, and a click at it lands on the thing on top. A panel a game shows
|
|
52
|
+
## over a screen covers its drawers without hiding them, so their words matched as well.
|
|
53
53
|
static func control_saying(
|
|
54
54
|
root: Node, root_path: String, wanted: String, which: Variant
|
|
55
55
|
) -> Dictionary:
|
|
@@ -60,7 +60,7 @@ static func control_saying(
|
|
|
60
60
|
# The words of the control that matched, which a button pressed on a label's behalf has none of.
|
|
61
61
|
var said: Array[String] = []
|
|
62
62
|
var exact: Array[bool] = []
|
|
63
|
-
# Whether the words are only inside longer words there, "
|
|
63
|
+
# Whether the words are only inside longer words there, "away" in "Faraway".
|
|
64
64
|
var inside: Array[bool] = []
|
|
65
65
|
var hidden: Array[Node] = []
|
|
66
66
|
# Controls saying exactly the words that a click cannot reach, kept to say what a match on
|
|
@@ -150,8 +150,8 @@ static func control_saying(
|
|
|
150
150
|
if cover == null:
|
|
151
151
|
cover = over
|
|
152
152
|
# Stable, so matches of one rank keep the order they are drawn in. Words only inside longer
|
|
153
|
-
# words come after everything else: ranked as a button saying them as part of more,
|
|
154
|
-
#
|
|
153
|
+
# words come after everything else: ranked as a button saying them as part of more, a card was
|
|
154
|
+
# pressed over a control saying exactly the words, and spent something in the game.
|
|
155
155
|
open.sort_custom(
|
|
156
156
|
func(a: int, b: int) -> bool:
|
|
157
157
|
if inside[a] != inside[b]:
|
|
@@ -203,7 +203,7 @@ static func control_saying(
|
|
|
203
203
|
"type": "error",
|
|
204
204
|
"message":
|
|
205
205
|
(
|
|
206
|
-
"%s, so which to click is not clear
|
|
206
|
+
"%s, so which to click is not clear. Pass index, counting from 0%s: %s"
|
|
207
207
|
% [_described(best, ranks[0], wanted, under), order, _candidates(targets, lines)]
|
|
208
208
|
),
|
|
209
209
|
}
|
|
@@ -225,9 +225,9 @@ static func control_saying(
|
|
|
225
225
|
"index %s picks nothing: %s: %s" % [given, among, _candidates(targets, lines)],
|
|
226
226
|
}
|
|
227
227
|
# A match saying the words as part of more, while another control says exactly them, is a
|
|
228
|
-
# stand-in for that one, not the control meant: a tab reading "
|
|
229
|
-
# and a
|
|
230
|
-
# words on screen in a control that is not a button, a card reading "
|
|
228
|
+
# stand-in for that one, not the control meant: a tab reading "Away" sat in a collapsed drawer
|
|
229
|
+
# and a line saying somebody "went away for good" was pressed, and with the tab's own
|
|
230
|
+
# words on screen in a control that is not a button, a card reading "Faraway, 90" was. Pressed
|
|
231
231
|
# only when asked for by index, which is the caller saying the match is what they meant.
|
|
232
232
|
var exact_elsewhere: Array[Node] = exact_out_of_reach.duplicate()
|
|
233
233
|
var elsewhere_as: Array[String] = out_of_reach_as.duplicate()
|
|
@@ -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
|
|
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",
|
|
@@ -272,7 +272,7 @@ static func control_saying(
|
|
|
272
272
|
(
|
|
273
273
|
(
|
|
274
274
|
'no control on screen%s says "%s" as a word of its own, so nothing was '
|
|
275
|
-
+ 'clicked: %s %s has it only inside a longer word, "%s"
|
|
275
|
+
+ 'clicked: %s %s has it only inside a longer word, "%s". Pass index %d to '
|
|
276
276
|
+ "press %s anyway"
|
|
277
277
|
)
|
|
278
278
|
% [
|
|
@@ -287,8 +287,8 @@ static func control_saying(
|
|
|
287
287
|
),
|
|
288
288
|
}
|
|
289
289
|
# Words in a sentence on a control that lets the pointer through: the press lands on whatever is
|
|
290
|
-
# under it, which is nothing the words name. A line saying what "is
|
|
291
|
-
# match left for "
|
|
290
|
+
# under it, which is nothing the words name. A line saying what "is far away" was the best
|
|
291
|
+
# match left for "Away" once the cards saying "Faraway" were passed over.
|
|
292
292
|
if (
|
|
293
293
|
which == null
|
|
294
294
|
and not wholes[index]
|
|
@@ -301,7 +301,7 @@ static func control_saying(
|
|
|
301
301
|
(
|
|
302
302
|
'no control on screen%s that takes a click says "%s", so nothing was clicked: '
|
|
303
303
|
+ '%s %s says it as part of "%s" and lets the pointer through to what is under '
|
|
304
|
-
+ "it
|
|
304
|
+
+ "it. Pass index %d to click there anyway"
|
|
305
305
|
)
|
|
306
306
|
% [
|
|
307
307
|
under,
|
|
@@ -325,7 +325,7 @@ static func control_saying(
|
|
|
325
325
|
% [targets.size() - 1, _passed_over(ranks[0], ranks.slice(1), false)]
|
|
326
326
|
)
|
|
327
327
|
)
|
|
328
|
-
found["picked"] = "%s
|
|
328
|
+
found["picked"] = "%s, and %s" % [_described(1, ranks[0], wanted, under), others]
|
|
329
329
|
if covered > 0:
|
|
330
330
|
found["covered"] = covered
|
|
331
331
|
var answer: Dictionary = { "path": str(targets[index].get_path()), "found": found }
|
|
@@ -377,9 +377,9 @@ static func pointer_turned_off_by(control: Control) -> Control:
|
|
|
377
377
|
## Whether one of [param wanted]'s alternatives is the whole of [param said], or the whole of one of
|
|
378
378
|
## its lines. A glob is matched against the whole text already, so every match of one is whole.
|
|
379
379
|
##
|
|
380
|
-
## A line counts because a card is a title over a description: "
|
|
381
|
-
## named
|
|
382
|
-
## "
|
|
380
|
+
## A line counts because a card is a title over a description: "GLOW" over "glows 3" is the card
|
|
381
|
+
## named GLOW, and read as its words run together it said GLOW only as part of more, level with
|
|
382
|
+
## "GLOWSHADE" over its own description, and a click on GLOW was refused as not clear.
|
|
383
383
|
static func _says_exactly(written: String, wanted: String) -> bool:
|
|
384
384
|
var said: String = GdhSays.spaced(written)
|
|
385
385
|
for words: String in GdhSays.alternatives(wanted):
|
|
@@ -397,8 +397,8 @@ static func _says_exactly(written: String, wanted: String) -> bool:
|
|
|
397
397
|
|
|
398
398
|
|
|
399
399
|
## Whether [param written] has the plain words [param wanted] asks for only inside longer words,
|
|
400
|
-
## never starting where a word starts: "
|
|
401
|
-
## "Recruit" in "Recruits", and so does anything matched by a pattern.
|
|
400
|
+
## never starting where a word starts: "away" in "Faraway". Words starting a longer one count as
|
|
401
|
+
## said, "Recruit" in "Recruits", and so does anything matched by a pattern.
|
|
402
402
|
static func _inside_a_word(written: String, wanted: String) -> bool:
|
|
403
403
|
var said: String = GdhSays.spaced(written).to_lower()
|
|
404
404
|
var found: bool = false
|
|
@@ -477,7 +477,7 @@ static func _offered_note(from: Node, drawn: Array[Node], areas: Array[Rect2]) -
|
|
|
477
477
|
and _cover_of(button, drawn, areas, aim) == null
|
|
478
478
|
)
|
|
479
479
|
var words: String = GdhWords.pressable_words(from, open, LISTED_CANDIDATES)
|
|
480
|
-
return "" if words.is_empty() else "
|
|
480
|
+
return "" if words.is_empty() else ". The buttons on screen say %s" % words
|
|
481
481
|
|
|
482
482
|
|
|
483
483
|
## What to add to a refusal that found nothing on screen, when words matched hidden controls: how
|
|
@@ -490,9 +490,9 @@ static func _hidden_note(hidden: Array[Node]) -> String:
|
|
|
490
490
|
for node: Node in hidden.slice(0, NAMED_HIDDEN):
|
|
491
491
|
named.append("%s%s" % [node.get_path(), why_hidden(node)])
|
|
492
492
|
if hidden.size() == 1:
|
|
493
|
-
return "
|
|
493
|
+
return ". 1 hidden control says it, %s" % named[0]
|
|
494
494
|
return (
|
|
495
|
-
"
|
|
495
|
+
". %d hidden controls say it, among them %s and %s"
|
|
496
496
|
% [hidden.size(), ", ".join(named.slice(0, -1)), named[-1]]
|
|
497
497
|
)
|
|
498
498
|
|
|
@@ -503,7 +503,7 @@ static func _outside_note(outside: int, root: Node) -> String:
|
|
|
503
503
|
if outside == 0:
|
|
504
504
|
return ""
|
|
505
505
|
var note: String = (
|
|
506
|
-
"
|
|
506
|
+
". %d control%s it outside what the screen shows"
|
|
507
507
|
% [outside, " says" if outside == 1 else "s say"]
|
|
508
508
|
)
|
|
509
509
|
var window: Window = root as Window
|
|
@@ -537,9 +537,9 @@ static func _covered_note(covered: int, cover: Node) -> String:
|
|
|
537
537
|
if covered == 0:
|
|
538
538
|
return ""
|
|
539
539
|
if covered == 1:
|
|
540
|
-
return "
|
|
540
|
+
return ". 1 control says it under %s, which is drawn over it" % cover.get_path()
|
|
541
541
|
return (
|
|
542
|
-
"
|
|
542
|
+
". %d controls say it under what is drawn over them, such as %s"
|
|
543
543
|
% [covered, cover.get_path()]
|
|
544
544
|
)
|
|
545
545
|
|
|
@@ -564,8 +564,8 @@ static func drawn_over(target: Control) -> Node:
|
|
|
564
564
|
|
|
565
565
|
## Why [param node], which is not visible in the tree, is not, as a parenthesis to follow its path:
|
|
566
566
|
## which nodes hide it, nearest first, and what has taken its place. Reading visible off the node
|
|
567
|
-
## itself says true when only an ancestor is hidden, so without this a caller had nothing to unhide
|
|
568
|
-
##
|
|
567
|
+
## itself says true when only an ancestor is hidden, so without this a caller had nothing to unhide.
|
|
568
|
+
## Every hidden one is named because unhiding only the nearest leaves the rest hiding it.
|
|
569
569
|
##
|
|
570
570
|
## What has taken its place is what a click at its centre reaches now, which is the screen a game
|
|
571
571
|
## shows instead. Without a control there, as for a 3D node, it is what is shown beside the
|
|
@@ -586,7 +586,7 @@ static func why_hidden(node: Node) -> String:
|
|
|
586
586
|
var instead: Node = null if control == null else drawn_at(control)
|
|
587
587
|
if instead != null:
|
|
588
588
|
return (
|
|
589
|
-
" (%s
|
|
589
|
+
" (%s, and a click where it would be reaches %s)"
|
|
590
590
|
% [", and ".join(named), instead.get_path()]
|
|
591
591
|
)
|
|
592
592
|
var outermost: Node = hiding.back()
|
|
@@ -602,7 +602,7 @@ static func why_hidden(node: Node) -> String:
|
|
|
602
602
|
## What a click at the centre of [param target], which is hidden, would reach instead, or null.
|
|
603
603
|
##
|
|
604
604
|
## The topmost thing there that takes the pointer, whatever order it is drawn in relative to the
|
|
605
|
-
## target, since the target is not drawn at all
|
|
605
|
+
## target, since the target is not drawn at all, and never a node holding the target, which covers
|
|
606
606
|
## its place by containing it and is not what took it.
|
|
607
607
|
static func drawn_at(target: Control) -> Node:
|
|
608
608
|
var drawn: Array[Node] = _pointer_takers(target.get_tree().root)
|
|
@@ -636,20 +636,20 @@ static func _drawn_above(control: Control, other: Control) -> bool:
|
|
|
636
636
|
return layer > other_layer or (layer == other_layer and control.is_greater_than(other))
|
|
637
637
|
|
|
638
638
|
|
|
639
|
-
## "
|
|
640
|
-
## three at most and
|
|
639
|
+
## `", and A, B and C are shown beside N"` for [param beside] and the hidden node [param hidden].
|
|
640
|
+
## It names three at most and counts the rest.
|
|
641
641
|
static func _shown_beside(beside: Array[String], hidden: Node) -> String:
|
|
642
642
|
if beside.is_empty():
|
|
643
643
|
return ""
|
|
644
644
|
var at: String = str(hidden.get_path())
|
|
645
645
|
if beside.size() == 1:
|
|
646
|
-
return "
|
|
646
|
+
return ", and %s is shown beside %s" % [beside[0], at]
|
|
647
647
|
var listed: Array[String] = beside.slice(0, 3)
|
|
648
648
|
var rest: int = beside.size() - listed.size()
|
|
649
649
|
if rest > 0:
|
|
650
|
-
return "
|
|
650
|
+
return ", and %s and %d more are shown beside %s" % [", ".join(listed), rest, at]
|
|
651
651
|
var last: String = listed.pop_back()
|
|
652
|
-
return "
|
|
652
|
+
return ", and %s and %s are shown beside %s" % [", ".join(listed), last, at]
|
|
653
653
|
|
|
654
654
|
|
|
655
655
|
## The nodes whose visible is false and keeps [param node] from being drawn, nearest first. Walked
|
|
@@ -728,7 +728,7 @@ static func _pointer_takers(root: Node) -> Array[Node]:
|
|
|
728
728
|
## a control's however it is turned or scaled.
|
|
729
729
|
##
|
|
730
730
|
## Worked out once for a click rather than once for each match. A plain word on a screen of prose
|
|
731
|
-
## matches a hundred labels, and asking all four thousand controls
|
|
731
|
+
## matches a hundred labels, and asking all four thousand controls on screen for their transform
|
|
732
732
|
## for each of them took a click by words 80 ms, where the matching alone takes half that.
|
|
733
733
|
static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
|
|
734
734
|
var areas: Array[Rect2] = []
|
|
@@ -856,7 +856,7 @@ static func _holds(control: Control, point: Vector2) -> bool:
|
|
|
856
856
|
# A control scaled to nothing holds no point, and inverting its transform is an engine error.
|
|
857
857
|
if is_zero_approx(placed.determinant()):
|
|
858
858
|
return false
|
|
859
|
-
# The rectangle, which is the engine's own answer unless a script overrides `_has_point
|
|
859
|
+
# The rectangle, which is the engine's own answer unless a script overrides `_has_point`. The
|
|
860
860
|
# engine does not offer the overridden answer to scripts.
|
|
861
861
|
return Rect2(Vector2.ZERO, control.size).has_point(placed.affine_inverse() * point)
|
|
862
862
|
|
|
@@ -964,7 +964,7 @@ static func _candidates(targets: Array[Control], said: Array[String]) -> String:
|
|
|
964
964
|
|
|
965
965
|
## What to add to a refusal that found nothing on screen, when an open menu has an item saying the
|
|
966
966
|
## words: a menu draws its items rather than holding a control for each, so a click finds none,
|
|
967
|
-
## while a wait for the words finds the item. A wait met on "Leave the
|
|
967
|
+
## while a wait for the words finds the item. A wait met on "Leave the shop" was followed by a click
|
|
968
968
|
## on it refused as nothing on screen saying it, with no word of the call that chooses it.
|
|
969
969
|
static func _open_menu_note(root: Node, wanted: String) -> String:
|
|
970
970
|
var pending: Array[Node] = [root]
|
|
@@ -981,7 +981,7 @@ static func _open_menu_note(root: Node, wanted: String) -> String:
|
|
|
981
981
|
)
|
|
982
982
|
return (
|
|
983
983
|
(
|
|
984
|
-
'
|
|
984
|
+
'. The open menu %s has an item saying it, "%s", which a click cannot '
|
|
985
985
|
+ 'reach: choose it with runtime_input choose, path %s and text "%s"'
|
|
986
986
|
)
|
|
987
987
|
% [menu.get_path(), text, named.get_path(), text]
|
|
@@ -83,7 +83,7 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
83
83
|
if ended_editing(focused, before):
|
|
84
84
|
answer["editing_ended"] = true
|
|
85
85
|
# A control drawing its own field takes characters in its gui_input, so typing at one is not
|
|
86
|
-
# refused
|
|
86
|
+
# refused, but a focused button answered that the text went into it, which read as typed.
|
|
87
87
|
if into != null and before.is_empty():
|
|
88
88
|
answer["note"] = (
|
|
89
89
|
(
|
|
@@ -98,7 +98,7 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
## What [param focused] says and whether it is being edited, read before keys go in, for
|
|
101
|
-
## [method field_noted] and [method ended_editing] to compare with
|
|
101
|
+
## [method field_noted] and [method ended_editing] to compare with. Empty where the focus is no
|
|
102
102
|
## field. A [TextEdit] has no editing state of its own and counts as always edited.
|
|
103
103
|
static func field_before(focused: Control) -> Dictionary:
|
|
104
104
|
var field: LineEdit = focused as LineEdit
|
|
@@ -333,7 +333,7 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
333
333
|
# whatever is behind it, and before the item is looked for, because opening is when a game fills
|
|
334
334
|
# a menu: one rebuilt on about_to_popup was read as it stood before, measured on 4.7.2, and the
|
|
335
335
|
# index found in the old items chose another item of the new ones. An OptionButton opens its
|
|
336
|
-
# own
|
|
336
|
+
# own. A bare PopupMenu is popped where it already sits, which leaves a menu already open where
|
|
337
337
|
# it is.
|
|
338
338
|
var opened: bool = GdhMenus.open_the_menu(node, menu)
|
|
339
339
|
await _host.get_tree().process_frame
|